@brainervirus/workit-cursor 3.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +4 -4
- package/commands/wk-bdd.md +1 -3
- package/commands/wk-continue.md +5 -0
- package/commands/wk-debug.md +1 -3
- package/commands/wk-deslop.md +1 -3
- package/commands/wk-fanout.md +5 -0
- package/commands/wk-implement.md +1 -3
- package/commands/wk-review.md +1 -3
- package/commands/wk-shape.md +5 -0
- package/commands/wk-ship.md +5 -0
- package/commands/wk-test-audit.md +1 -3
- package/commands/wk-verify-app.md +5 -0
- package/dist/cursor-session-start.js +2312 -1205
- package/dist/mcp-server.js +2382 -1258
- package/dist/workit-hook.js +2402 -1276
- package/package.json +3 -3
- package/rules/workit-contract.mdc +12 -3
- package/skills/workit-bdd/SKILL.md +35 -29
- package/skills/workit-continue/SKILL.md +53 -0
- package/skills/workit-debug/SKILL.md +41 -36
- package/skills/workit-deslop/SKILL.md +35 -25
- package/skills/workit-fanout/SKILL.md +62 -0
- package/skills/workit-fanout/references/brief.md +56 -0
- package/skills/workit-implement/SKILL.md +48 -44
- package/skills/workit-review/SKILL.md +42 -51
- package/skills/workit-review/references/impact.md +24 -0
- package/skills/workit-shape/SKILL.md +71 -0
- package/skills/workit-shape/references/diagrams.md +17 -0
- package/skills/workit-shape/references/knowledge.md +58 -0
- package/skills/workit-shape/references/mockups.md +15 -0
- package/skills/workit-shape/references/slicing.md +42 -0
- package/skills/workit-ship/SKILL.md +52 -0
- package/skills/workit-test-audit/SKILL.md +10 -2
- package/skills/workit-verify-app/SKILL.md +63 -0
- package/skills/workit-verify-app/references/template.md +49 -0
- package/commands/wk-babysit.md +0 -7
- package/commands/wk-blast-radius.md +0 -7
- package/commands/wk-challenge.md +0 -7
- package/commands/wk-diagram.md +0 -7
- package/commands/wk-green-run.md +0 -7
- package/commands/wk-handoff.md +0 -7
- package/commands/wk-mockup.md +0 -7
- package/commands/wk-plan.md +0 -7
- package/commands/wk-steer.md +0 -7
- package/commands/wk-tdd.md +0 -7
- package/skills/workit-babysit/SKILL.md +0 -37
- package/skills/workit-behavioral-tdd/SKILL.md +0 -56
- package/skills/workit-blast-radius/SKILL.md +0 -26
- package/skills/workit-challenge/SKILL.md +0 -47
- package/skills/workit-diagram/SKILL.md +0 -27
- package/skills/workit-green-run/SKILL.md +0 -24
- package/skills/workit-handoff/SKILL.md +0 -37
- package/skills/workit-mockup/SKILL.md +0 -23
- package/skills/workit-plan/SKILL.md +0 -45
- package/skills/workit-steer/SKILL.md +0 -39
|
@@ -7873,7 +7873,7 @@ var SCHEMA_VERSION = 1, POLICY_VERSION = "1.0.0", OPERATION_FAMILIES, uuidPatter
|
|
|
7873
7873
|
if (critical.length > 0)
|
|
7874
7874
|
return { ...first, critical };
|
|
7875
7875
|
return { success: true, data: parsed.data, stripped: [...new Set(stripped)] };
|
|
7876
|
-
}, workspaceRecordSchema, capabilitySchema, requirementEvaluationSchema, taskViewSchema, taskSummarySchema, taskListItemSchema, exportBundleSchema, callerSchema, base, revisions, taskId, operation = (shape) => object({ ...base, ...shape }).strict(), taskOperations, policyOperations, evidenceOperations, findingOperations, decisionOperations, workerOperations, writerOperations, stateOperations, operationSchemas,
|
|
7876
|
+
}, workspaceRecordSchema, capabilitySchema, requirementEvaluationSchema, taskViewSchema, taskSummarySchema, taskListItemSchema, exportBundleSchema, callerSchema, base, revisions, taskId, operation = (shape) => object({ ...base, ...shape }).strict(), taskOperations, policyOperations, evidenceOperations, findingOperations, decisionOperations, workerOperations, writerOperations, stateOperations, operationSchemas, withImplicitTask = (options) => Object.values(options).map((option) => ("taskId" in option.shape) ? option.extend({ taskId: id.optional() }) : option), advertised = (options) => discriminatedUnion("action", withImplicitTask(options)), advertisedOperationSchemas, compiledOperationSchemas, success = (revisionValue, workspaceRevisionValue, data) => ({
|
|
7877
7877
|
ok: true,
|
|
7878
7878
|
schemaVersion: 1,
|
|
7879
7879
|
revision: revisionValue,
|
|
@@ -8548,16 +8548,6 @@ var init_task_contract = __esm(() => {
|
|
|
8548
8548
|
expectedWorkspaceRevision: revision.nullable().optional(),
|
|
8549
8549
|
bundle: exportBundleSchema,
|
|
8550
8550
|
authorityRefs: array(refSchema)
|
|
8551
|
-
}),
|
|
8552
|
-
recover: operation({
|
|
8553
|
-
action: literal("recover"),
|
|
8554
|
-
taskId: id.optional(),
|
|
8555
|
-
expectedWorkspaceRevision: revision.optional(),
|
|
8556
|
-
target: _enum(["task", "workspace"]),
|
|
8557
|
-
expectedBytes: digest,
|
|
8558
|
-
snapshotDigest: digest,
|
|
8559
|
-
reason: text,
|
|
8560
|
-
authorityRefs: array(refSchema)
|
|
8561
8551
|
})
|
|
8562
8552
|
};
|
|
8563
8553
|
operationSchemas = {
|
|
@@ -8570,10 +8560,15 @@ var init_task_contract = __esm(() => {
|
|
|
8570
8560
|
writer: discriminatedUnion("action", Object.values(writerOperations)),
|
|
8571
8561
|
state: discriminatedUnion("action", Object.values(stateOperations))
|
|
8572
8562
|
};
|
|
8573
|
-
({ recover: _unadvertisedRecover, ...advertisedStateOperations } = stateOperations);
|
|
8574
8563
|
advertisedOperationSchemas = {
|
|
8575
|
-
|
|
8576
|
-
|
|
8564
|
+
task: advertised(taskOperations),
|
|
8565
|
+
policy: advertised(policyOperations),
|
|
8566
|
+
evidence: advertised(evidenceOperations),
|
|
8567
|
+
finding: advertised(findingOperations),
|
|
8568
|
+
decision: advertised(decisionOperations),
|
|
8569
|
+
worker: advertised(workerOperations),
|
|
8570
|
+
writer: advertised(writerOperations),
|
|
8571
|
+
state: advertised(stateOperations)
|
|
8577
8572
|
};
|
|
8578
8573
|
compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
|
|
8579
8574
|
});
|
|
@@ -9375,21 +9370,6 @@ var init_branch = __esm(() => {
|
|
|
9375
9370
|
init_vcs_config();
|
|
9376
9371
|
});
|
|
9377
9372
|
|
|
9378
|
-
// packages/workit-core/src/core/package-root.ts
|
|
9379
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
9380
|
-
import path4 from "node:path";
|
|
9381
|
-
import { fileURLToPath } from "node:url";
|
|
9382
|
-
var packageRoot = () => {
|
|
9383
|
-
let dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
9384
|
-
while (true) {
|
|
9385
|
-
const parent = path4.dirname(dir);
|
|
9386
|
-
if (existsSync2(path4.join(dir, "package.json")) || parent === dir)
|
|
9387
|
-
return dir;
|
|
9388
|
-
dir = parent;
|
|
9389
|
-
}
|
|
9390
|
-
};
|
|
9391
|
-
var init_package_root = () => {};
|
|
9392
|
-
|
|
9393
9373
|
// node_modules/@openclaw/fs-safe/dist/errors.js
|
|
9394
9374
|
function categorizeFsSafeError(code) {
|
|
9395
9375
|
return OPERATIONAL_CODES.has(code) ? "operational" : "policy";
|
|
@@ -9500,7 +9480,7 @@ var init_read_open_flags = () => {};
|
|
|
9500
9480
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
9501
9481
|
import { randomBytes } from "node:crypto";
|
|
9502
9482
|
import fsSync from "node:fs";
|
|
9503
|
-
import
|
|
9483
|
+
import path4 from "node:path";
|
|
9504
9484
|
function parseSidecarLockSnapshot(snapshot, parser) {
|
|
9505
9485
|
return snapshot && { ...snapshot, payload: parseSidecarLockPayload(snapshot.raw, parser) };
|
|
9506
9486
|
}
|
|
@@ -9528,13 +9508,13 @@ ${ownershipToken}
|
|
|
9528
9508
|
return { raw, ownershipToken };
|
|
9529
9509
|
}
|
|
9530
9510
|
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
9531
|
-
const resolved =
|
|
9532
|
-
const lexicalRelative =
|
|
9533
|
-
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${
|
|
9534
|
-
if (!relative || relative === ".." || relative.startsWith(`..${
|
|
9511
|
+
const resolved = path4.resolve(lockPath);
|
|
9512
|
+
const lexicalRelative = path4.relative(lockRoot.rootDir, resolved);
|
|
9513
|
+
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(lexicalRelative) ? lexicalRelative : path4.relative(lockRoot.rootReal, resolved);
|
|
9514
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path4.sep}`) || path4.isAbsolute(relative)) {
|
|
9535
9515
|
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
9536
9516
|
}
|
|
9537
|
-
return relative.split(
|
|
9517
|
+
return relative.split(path4.sep).join(path4.posix.sep);
|
|
9538
9518
|
}
|
|
9539
9519
|
function parseSidecarLockPayload(raw, parser) {
|
|
9540
9520
|
if (parser) {
|
|
@@ -9704,7 +9684,7 @@ function sleepSync(ms) {
|
|
|
9704
9684
|
|
|
9705
9685
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
9706
9686
|
import fs4 from "node:fs";
|
|
9707
|
-
import
|
|
9687
|
+
import path5 from "node:path";
|
|
9708
9688
|
function getSyncHeldLocks() {
|
|
9709
9689
|
const globalWithState = globalThis;
|
|
9710
9690
|
if (!globalWithState[SYNC_HELD_LOCKS_KEY]) {
|
|
@@ -9782,26 +9762,26 @@ function canonicalLockParentSync(parent) {
|
|
|
9782
9762
|
return process.platform === "win32" ? fs4.realpathSync.native(parent) : fs4.realpathSync(parent);
|
|
9783
9763
|
}
|
|
9784
9764
|
function normalizeTargetPath(targetPath) {
|
|
9785
|
-
const resolved =
|
|
9786
|
-
fs4.mkdirSync(
|
|
9765
|
+
const resolved = path5.resolve(targetPath);
|
|
9766
|
+
fs4.mkdirSync(path5.dirname(resolved), { recursive: true });
|
|
9787
9767
|
try {
|
|
9788
|
-
return
|
|
9768
|
+
return path5.join(canonicalLockParentSync(path5.dirname(resolved)), path5.basename(resolved));
|
|
9789
9769
|
} catch {
|
|
9790
9770
|
return resolved;
|
|
9791
9771
|
}
|
|
9792
9772
|
}
|
|
9793
9773
|
function boundedLockPath(lockPath, lockRoot) {
|
|
9794
|
-
const resolved =
|
|
9774
|
+
const resolved = path5.resolve(lockPath);
|
|
9795
9775
|
if (!lockRoot)
|
|
9796
9776
|
return resolved;
|
|
9797
9777
|
relativeSidecarLockPath(lockRoot, resolved);
|
|
9798
|
-
const parent =
|
|
9778
|
+
const parent = path5.dirname(resolved);
|
|
9799
9779
|
const parentReal = canonicalLockParentSync(parent);
|
|
9800
|
-
const parentRelative =
|
|
9801
|
-
if (parentRelative === ".." || parentRelative.startsWith(`..${
|
|
9780
|
+
const parentRelative = path5.relative(lockRoot.rootReal, parentReal);
|
|
9781
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path5.sep}`) || path5.isAbsolute(parentRelative)) {
|
|
9802
9782
|
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
9803
9783
|
}
|
|
9804
|
-
return
|
|
9784
|
+
return path5.join(parentReal, path5.basename(resolved));
|
|
9805
9785
|
}
|
|
9806
9786
|
function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
9807
9787
|
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
@@ -10016,9 +9996,300 @@ var init_file_lock = __esm(() => {
|
|
|
10016
9996
|
init_file_lock_sync();
|
|
10017
9997
|
});
|
|
10018
9998
|
|
|
10019
|
-
// packages/workit-core/src/core/
|
|
9999
|
+
// packages/workit-core/src/core/package-root.ts
|
|
10000
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
10001
|
+
import path6 from "node:path";
|
|
10002
|
+
import { fileURLToPath } from "node:url";
|
|
10003
|
+
var packageRoot = () => {
|
|
10004
|
+
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
10005
|
+
while (true) {
|
|
10006
|
+
const parent = path6.dirname(dir);
|
|
10007
|
+
if (existsSync2(path6.join(dir, "package.json")) || parent === dir)
|
|
10008
|
+
return dir;
|
|
10009
|
+
dir = parent;
|
|
10010
|
+
}
|
|
10011
|
+
};
|
|
10012
|
+
var init_package_root = () => {};
|
|
10013
|
+
|
|
10014
|
+
// packages/workit-core/src/store/paths.ts
|
|
10020
10015
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
10016
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
10021
10017
|
import fs5 from "node:fs";
|
|
10018
|
+
import path7 from "node:path";
|
|
10019
|
+
function fastGit(root) {
|
|
10020
|
+
if (DISCOVERY_ENV.some((name) => process.env[name]))
|
|
10021
|
+
return null;
|
|
10022
|
+
let dir = real(root);
|
|
10023
|
+
let device = null;
|
|
10024
|
+
for (;; ) {
|
|
10025
|
+
let stat;
|
|
10026
|
+
try {
|
|
10027
|
+
stat = fs5.statSync(dir);
|
|
10028
|
+
} catch {
|
|
10029
|
+
return null;
|
|
10030
|
+
}
|
|
10031
|
+
if (device !== null && stat.dev !== device)
|
|
10032
|
+
return { kind: "none" };
|
|
10033
|
+
device = stat.dev;
|
|
10034
|
+
const dotGit = path7.join(dir, ".git");
|
|
10035
|
+
let entry = null;
|
|
10036
|
+
try {
|
|
10037
|
+
entry = fs5.statSync(dotGit);
|
|
10038
|
+
} catch {}
|
|
10039
|
+
let gitDir = null;
|
|
10040
|
+
let top = null;
|
|
10041
|
+
if (entry) {
|
|
10042
|
+
top = dir;
|
|
10043
|
+
if (entry.isDirectory())
|
|
10044
|
+
gitDir = dotGit;
|
|
10045
|
+
else if (entry.isFile()) {
|
|
10046
|
+
let text = "";
|
|
10047
|
+
try {
|
|
10048
|
+
text = fs5.readFileSync(dotGit, "utf8");
|
|
10049
|
+
} catch {
|
|
10050
|
+
return null;
|
|
10051
|
+
}
|
|
10052
|
+
const match = /^gitdir: (.+)$/mu.exec(text);
|
|
10053
|
+
if (!match)
|
|
10054
|
+
return null;
|
|
10055
|
+
gitDir = path7.resolve(dir, match[1].trim());
|
|
10056
|
+
} else
|
|
10057
|
+
return null;
|
|
10058
|
+
if (!fs5.existsSync(path7.join(gitDir, "HEAD")))
|
|
10059
|
+
return null;
|
|
10060
|
+
} else if (isGitDir(dir))
|
|
10061
|
+
gitDir = dir;
|
|
10062
|
+
if (gitDir) {
|
|
10063
|
+
let common = gitDir;
|
|
10064
|
+
try {
|
|
10065
|
+
common = path7.resolve(gitDir, fs5.readFileSync(path7.join(gitDir, "commondir"), "utf8").trim());
|
|
10066
|
+
} catch {}
|
|
10067
|
+
if (!fs5.existsSync(path7.join(common, "objects")))
|
|
10068
|
+
return null;
|
|
10069
|
+
return {
|
|
10070
|
+
kind: "repo",
|
|
10071
|
+
common: real(common),
|
|
10072
|
+
gitDir: real(gitDir),
|
|
10073
|
+
top: top === null ? null : real(top),
|
|
10074
|
+
reftable: reftableRefs(common)
|
|
10075
|
+
};
|
|
10076
|
+
}
|
|
10077
|
+
const parent = path7.dirname(dir);
|
|
10078
|
+
if (parent === dir)
|
|
10079
|
+
return { kind: "none" };
|
|
10080
|
+
dir = parent;
|
|
10081
|
+
}
|
|
10082
|
+
}
|
|
10083
|
+
function resolveStore(root) {
|
|
10084
|
+
root = path7.resolve(root);
|
|
10085
|
+
let directory = false;
|
|
10086
|
+
try {
|
|
10087
|
+
directory = fs5.statSync(root).isDirectory();
|
|
10088
|
+
} catch {}
|
|
10089
|
+
if (!directory)
|
|
10090
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10091
|
+
const fast = fastGit(root);
|
|
10092
|
+
if (fast?.kind === "none")
|
|
10093
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10094
|
+
if (fast?.kind === "repo")
|
|
10095
|
+
return {
|
|
10096
|
+
dir: path7.join(fast.common, "workit"),
|
|
10097
|
+
shared: true,
|
|
10098
|
+
top: fast.top,
|
|
10099
|
+
git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
|
|
10100
|
+
};
|
|
10101
|
+
const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
10102
|
+
const common = run.stdout.trim();
|
|
10103
|
+
if (run.ok && common) {
|
|
10104
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
10105
|
+
return {
|
|
10106
|
+
dir: path7.join(real(path7.resolve(root, common)), "workit"),
|
|
10107
|
+
shared: true,
|
|
10108
|
+
top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
|
|
10109
|
+
};
|
|
10110
|
+
}
|
|
10111
|
+
if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
|
|
10112
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10113
|
+
return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
|
|
10114
|
+
`)[0] || "no output"}); fix the repository (git status should work here)`);
|
|
10115
|
+
}
|
|
10116
|
+
function resolveTaskKey(root, location) {
|
|
10117
|
+
if (!location.shared)
|
|
10118
|
+
return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
|
|
10119
|
+
const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
|
|
10120
|
+
if (location.git && !location.git.reftable) {
|
|
10121
|
+
let head = "";
|
|
10122
|
+
try {
|
|
10123
|
+
head = fs5.readFileSync(path7.join(location.git.gitDir, "HEAD"), "utf8").trim();
|
|
10124
|
+
} catch {}
|
|
10125
|
+
const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
|
|
10126
|
+
if (ref && ref[1] !== ".invalid")
|
|
10127
|
+
return branchKey(ref[1]);
|
|
10128
|
+
if (/^[0-9a-f]{40,64}$/u.test(head)) {
|
|
10129
|
+
const rebasing = rebasingBranch(location.git.gitDir);
|
|
10130
|
+
if (rebasing)
|
|
10131
|
+
return branchKey(rebasing);
|
|
10132
|
+
return {
|
|
10133
|
+
key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
|
|
10134
|
+
kind: "detached",
|
|
10135
|
+
branch: null
|
|
10136
|
+
};
|
|
10137
|
+
}
|
|
10138
|
+
}
|
|
10139
|
+
const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
|
|
10140
|
+
const name = branch.ok ? branch.stdout.trim() : "";
|
|
10141
|
+
if (name && name !== ".invalid")
|
|
10142
|
+
return branchKey(name);
|
|
10143
|
+
const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
|
|
10144
|
+
if (gitDir.ok && gitDir.stdout.trim()) {
|
|
10145
|
+
const rebasing = rebasingBranch(gitDir.stdout.trim());
|
|
10146
|
+
if (rebasing)
|
|
10147
|
+
return branchKey(rebasing);
|
|
10148
|
+
}
|
|
10149
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
10150
|
+
const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
|
|
10151
|
+
return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
|
|
10152
|
+
}
|
|
10153
|
+
function checkoutRootOf(root, location) {
|
|
10154
|
+
const resolved = path7.resolve(root);
|
|
10155
|
+
let canonical = resolved;
|
|
10156
|
+
try {
|
|
10157
|
+
canonical = fs5.realpathSync(resolved);
|
|
10158
|
+
} catch {
|
|
10159
|
+
return resolved;
|
|
10160
|
+
}
|
|
10161
|
+
const found = location ?? resolveStore(canonical);
|
|
10162
|
+
if (found instanceof Error || !found.shared || !found.top)
|
|
10163
|
+
return canonical;
|
|
10164
|
+
if (real(canonical) === found.top)
|
|
10165
|
+
return canonical;
|
|
10166
|
+
try {
|
|
10167
|
+
return fs5.realpathSync(found.top);
|
|
10168
|
+
} catch {
|
|
10169
|
+
return canonical;
|
|
10170
|
+
}
|
|
10171
|
+
}
|
|
10172
|
+
function branchRenames(location) {
|
|
10173
|
+
if (!location.shared)
|
|
10174
|
+
return [];
|
|
10175
|
+
const files = [];
|
|
10176
|
+
const walk = (dir) => {
|
|
10177
|
+
let entries;
|
|
10178
|
+
try {
|
|
10179
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
10180
|
+
} catch {
|
|
10181
|
+
return;
|
|
10182
|
+
}
|
|
10183
|
+
for (const entry of entries)
|
|
10184
|
+
if (entry.isDirectory())
|
|
10185
|
+
walk(path7.join(dir, entry.name));
|
|
10186
|
+
else
|
|
10187
|
+
files.push(path7.join(dir, entry.name));
|
|
10188
|
+
};
|
|
10189
|
+
walk(path7.join(path7.dirname(location.dir), "logs", "refs", "heads"));
|
|
10190
|
+
if (location.git)
|
|
10191
|
+
files.push(path7.join(location.git.gitDir, "logs", "HEAD"));
|
|
10192
|
+
const seen = new Set;
|
|
10193
|
+
const renames = [];
|
|
10194
|
+
for (const file of files) {
|
|
10195
|
+
let text = "";
|
|
10196
|
+
try {
|
|
10197
|
+
text = fs5.readFileSync(file, "utf8");
|
|
10198
|
+
} catch {
|
|
10199
|
+
continue;
|
|
10200
|
+
}
|
|
10201
|
+
for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
|
|
10202
|
+
const rename = { from: match[2], to: match[3], at: Number(match[1]) };
|
|
10203
|
+
const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
|
|
10204
|
+
if (seen.has(id))
|
|
10205
|
+
continue;
|
|
10206
|
+
seen.add(id);
|
|
10207
|
+
renames.push(rename);
|
|
10208
|
+
}
|
|
10209
|
+
}
|
|
10210
|
+
return renames.toSorted((left, right) => left.at - right.at);
|
|
10211
|
+
}
|
|
10212
|
+
function followRenames(key, boundAt, renames) {
|
|
10213
|
+
let current = key;
|
|
10214
|
+
let since = Math.floor(Date.parse(boundAt) / 1000);
|
|
10215
|
+
for (const rename of renames)
|
|
10216
|
+
if (rename.from === current && rename.at >= since) {
|
|
10217
|
+
current = rename.to;
|
|
10218
|
+
since = rename.at;
|
|
10219
|
+
}
|
|
10220
|
+
return current;
|
|
10221
|
+
}
|
|
10222
|
+
var NOT_A_REPO, gitRun = (cwd, args) => {
|
|
10223
|
+
const run = spawnSync2("git", args, {
|
|
10224
|
+
cwd,
|
|
10225
|
+
encoding: "utf8",
|
|
10226
|
+
timeout: 20000,
|
|
10227
|
+
killSignal: "SIGKILL",
|
|
10228
|
+
windowsHide: true,
|
|
10229
|
+
env: {
|
|
10230
|
+
...process.env,
|
|
10231
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
10232
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
10233
|
+
LC_ALL: "C",
|
|
10234
|
+
LANGUAGE: "C"
|
|
10235
|
+
}
|
|
10236
|
+
});
|
|
10237
|
+
const error = run.error;
|
|
10238
|
+
return {
|
|
10239
|
+
ok: run.status === 0,
|
|
10240
|
+
stdout: run.stdout ?? "",
|
|
10241
|
+
stderr: error ? String(error.message) : run.stderr ?? "",
|
|
10242
|
+
missing: error?.code === "ENOENT"
|
|
10243
|
+
};
|
|
10244
|
+
}, shortHash = (value) => createHash4("sha256").update(value).digest("hex").slice(0, 12), real = (file) => {
|
|
10245
|
+
try {
|
|
10246
|
+
return fs5.realpathSync.native(file);
|
|
10247
|
+
} catch {
|
|
10248
|
+
return file;
|
|
10249
|
+
}
|
|
10250
|
+
}, DISCOVERY_ENV, isGitDir = (dir) => {
|
|
10251
|
+
try {
|
|
10252
|
+
return fs5.statSync(path7.join(dir, "HEAD")).isFile() && fs5.statSync(path7.join(dir, "objects")).isDirectory() && fs5.statSync(path7.join(dir, "refs")).isDirectory();
|
|
10253
|
+
} catch {
|
|
10254
|
+
return false;
|
|
10255
|
+
}
|
|
10256
|
+
}, reftableRefs = (common) => {
|
|
10257
|
+
try {
|
|
10258
|
+
return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs5.readFileSync(path7.join(common, "config"), "utf8"));
|
|
10259
|
+
} catch {
|
|
10260
|
+
return false;
|
|
10261
|
+
}
|
|
10262
|
+
}, rebasingBranch = (gitDir) => {
|
|
10263
|
+
for (const state of ["rebase-merge", "rebase-apply"]) {
|
|
10264
|
+
try {
|
|
10265
|
+
const ref = fs5.readFileSync(path7.join(gitDir, state, "head-name"), "utf8").trim();
|
|
10266
|
+
const match = /^refs\/heads\/(.+)$/u.exec(ref);
|
|
10267
|
+
if (match)
|
|
10268
|
+
return match[1];
|
|
10269
|
+
} catch {}
|
|
10270
|
+
}
|
|
10271
|
+
return null;
|
|
10272
|
+
}, checkoutSlug = (root) => {
|
|
10273
|
+
let canonical = real(root);
|
|
10274
|
+
if (process.platform === "win32")
|
|
10275
|
+
canonical = canonical.toLowerCase();
|
|
10276
|
+
const base = path7.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
|
|
10277
|
+
return `${base}-${shortHash(canonical)}`;
|
|
10278
|
+
};
|
|
10279
|
+
var init_paths = __esm(() => {
|
|
10280
|
+
NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
|
|
10281
|
+
DISCOVERY_ENV = [
|
|
10282
|
+
"GIT_DIR",
|
|
10283
|
+
"GIT_COMMON_DIR",
|
|
10284
|
+
"GIT_WORK_TREE",
|
|
10285
|
+
"GIT_CEILING_DIRECTORIES",
|
|
10286
|
+
"GIT_DISCOVERY_ACROSS_FILESYSTEM"
|
|
10287
|
+
];
|
|
10288
|
+
});
|
|
10289
|
+
|
|
10290
|
+
// packages/workit-core/src/core/store-lock.ts
|
|
10291
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
10292
|
+
import fs6 from "node:fs";
|
|
10022
10293
|
import { hostname } from "node:os";
|
|
10023
10294
|
var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, RECLAIM_GUARD_TTL_MS = 30000, defaultLockTimeoutMs = 250, defaultLockTimeout = () => defaultLockTimeoutMs, parseMetadataLock = (raw) => {
|
|
10024
10295
|
let value;
|
|
@@ -10037,9 +10308,6 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
|
|
|
10037
10308
|
} catch {
|
|
10038
10309
|
return null;
|
|
10039
10310
|
}
|
|
10040
|
-
}, sameMetadataLock = (left, right) => {
|
|
10041
|
-
const parsed = metadataLockSchema.safeParse(left);
|
|
10042
|
-
return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
|
|
10043
10311
|
}, parseProcStatStart = (stat) => {
|
|
10044
10312
|
const close = stat.lastIndexOf(")");
|
|
10045
10313
|
if (close < 0)
|
|
@@ -10048,14 +10316,14 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
|
|
|
10048
10316
|
}, processStartOf = (pid) => {
|
|
10049
10317
|
if (process.platform === "linux") {
|
|
10050
10318
|
try {
|
|
10051
|
-
return parseProcStatStart(
|
|
10319
|
+
return parseProcStatStart(fs6.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
10052
10320
|
} catch {
|
|
10053
10321
|
return null;
|
|
10054
10322
|
}
|
|
10055
10323
|
}
|
|
10056
10324
|
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
10057
10325
|
try {
|
|
10058
|
-
const run =
|
|
10326
|
+
const run = spawnSync3("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
10059
10327
|
encoding: "utf8",
|
|
10060
10328
|
timeout: 1000
|
|
10061
10329
|
});
|
|
@@ -10068,7 +10336,7 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
|
|
|
10068
10336
|
return null;
|
|
10069
10337
|
}, readTrimmed = (file) => {
|
|
10070
10338
|
try {
|
|
10071
|
-
return
|
|
10339
|
+
return fs6.readFileSync(file, "utf8").trim() || null;
|
|
10072
10340
|
} catch {
|
|
10073
10341
|
return null;
|
|
10074
10342
|
}
|
|
@@ -10077,7 +10345,7 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
|
|
|
10077
10345
|
return cachedLockHost;
|
|
10078
10346
|
let pidns = null;
|
|
10079
10347
|
try {
|
|
10080
|
-
pidns = /\[(\d+)\]/.exec(
|
|
10348
|
+
pidns = /\[(\d+)\]/.exec(fs6.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
|
|
10081
10349
|
} catch {}
|
|
10082
10350
|
const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
10083
10351
|
cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
|
|
@@ -10114,7 +10382,7 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
|
|
|
10114
10382
|
return { state: "live", reason: `held by running pid ${pid}` };
|
|
10115
10383
|
}, ageOf = (file, nowMs) => {
|
|
10116
10384
|
try {
|
|
10117
|
-
return nowMs -
|
|
10385
|
+
return nowMs - fs6.lstatSync(file).mtimeMs;
|
|
10118
10386
|
} catch {
|
|
10119
10387
|
return null;
|
|
10120
10388
|
}
|
|
@@ -10124,7 +10392,7 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
|
|
|
10124
10392
|
if (age === null || age <= RECLAIM_GUARD_TTL_MS)
|
|
10125
10393
|
return false;
|
|
10126
10394
|
try {
|
|
10127
|
-
|
|
10395
|
+
fs6.rmdirSync(guard);
|
|
10128
10396
|
return true;
|
|
10129
10397
|
} catch {
|
|
10130
10398
|
return false;
|
|
@@ -10133,6 +10401,7 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
|
|
|
10133
10401
|
var init_store_lock = __esm(() => {
|
|
10134
10402
|
init_zod();
|
|
10135
10403
|
init_task_contract();
|
|
10404
|
+
init_paths();
|
|
10136
10405
|
metadataLockSchema = object({
|
|
10137
10406
|
pid: number2().int().nonnegative().safe(),
|
|
10138
10407
|
processStart: string2().nullable(),
|
|
@@ -10143,129 +10412,622 @@ var init_store_lock = __esm(() => {
|
|
|
10143
10412
|
FOREIGN_LOCK_TTL_MS = 10 * 60000;
|
|
10144
10413
|
});
|
|
10145
10414
|
|
|
10415
|
+
// packages/workit-core/src/store/event-log.ts
|
|
10416
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10417
|
+
import fs7 from "node:fs";
|
|
10418
|
+
import path8 from "node:path";
|
|
10419
|
+
function readLog(file, from = 0, expectSeq = null) {
|
|
10420
|
+
let fd;
|
|
10421
|
+
try {
|
|
10422
|
+
fd = fs7.openSync(file, "r");
|
|
10423
|
+
} catch (error) {
|
|
10424
|
+
if (error.code === "ENOENT")
|
|
10425
|
+
return null;
|
|
10426
|
+
throw error;
|
|
10427
|
+
}
|
|
10428
|
+
let bytes;
|
|
10429
|
+
let size;
|
|
10430
|
+
try {
|
|
10431
|
+
size = fs7.fstatSync(fd).size;
|
|
10432
|
+
if (from > size)
|
|
10433
|
+
throw new LogDamage(`${file}: shorter than its snapshot`);
|
|
10434
|
+
bytes = Buffer.alloc(size - from);
|
|
10435
|
+
let read = 0;
|
|
10436
|
+
while (read < bytes.length) {
|
|
10437
|
+
const got = fs7.readSync(fd, bytes, read, bytes.length - read, from + read);
|
|
10438
|
+
if (got === 0)
|
|
10439
|
+
break;
|
|
10440
|
+
read += got;
|
|
10441
|
+
}
|
|
10442
|
+
bytes = bytes.subarray(0, read);
|
|
10443
|
+
size = from + read;
|
|
10444
|
+
} finally {
|
|
10445
|
+
fs7.closeSync(fd);
|
|
10446
|
+
}
|
|
10447
|
+
const events = [];
|
|
10448
|
+
let start = 0;
|
|
10449
|
+
let lastStart = from;
|
|
10450
|
+
let seq = expectSeq;
|
|
10451
|
+
for (;; ) {
|
|
10452
|
+
const newline = bytes.indexOf(10, start);
|
|
10453
|
+
if (newline < 0)
|
|
10454
|
+
break;
|
|
10455
|
+
const line = bytes.subarray(start, newline).toString("utf8");
|
|
10456
|
+
if (line.trim() !== "") {
|
|
10457
|
+
const event = parseEvent(line, file, from + start);
|
|
10458
|
+
if (seq !== null && event.seq !== seq)
|
|
10459
|
+
throw new LogDamage(`${file}: event seq ${event.seq} where ${seq} was expected`);
|
|
10460
|
+
seq = event.seq + 1;
|
|
10461
|
+
events.push(event);
|
|
10462
|
+
lastStart = from + start;
|
|
10463
|
+
}
|
|
10464
|
+
start = newline + 1;
|
|
10465
|
+
}
|
|
10466
|
+
return { events, end: from + start, lastStart, size };
|
|
10467
|
+
}
|
|
10468
|
+
function appendEvent(file, event, validEnd) {
|
|
10469
|
+
const line = Buffer.from(`${JSON.stringify(event)}
|
|
10470
|
+
`, "utf8");
|
|
10471
|
+
const created = !fs7.existsSync(file);
|
|
10472
|
+
if (!created && fs7.statSync(file).size !== validEnd) {
|
|
10473
|
+
const cut = fs7.openSync(file, "r+");
|
|
10474
|
+
try {
|
|
10475
|
+
fs7.ftruncateSync(cut, validEnd);
|
|
10476
|
+
fs7.fsyncSync(cut);
|
|
10477
|
+
} finally {
|
|
10478
|
+
fs7.closeSync(cut);
|
|
10479
|
+
}
|
|
10480
|
+
}
|
|
10481
|
+
const fd = fs7.openSync(file, "a", 384);
|
|
10482
|
+
try {
|
|
10483
|
+
let written = 0;
|
|
10484
|
+
while (written < line.length)
|
|
10485
|
+
written += fs7.writeSync(fd, line, written, line.length - written);
|
|
10486
|
+
fs7.fsyncSync(fd);
|
|
10487
|
+
} finally {
|
|
10488
|
+
fs7.closeSync(fd);
|
|
10489
|
+
}
|
|
10490
|
+
if (created)
|
|
10491
|
+
fsyncDirectory(path8.dirname(file));
|
|
10492
|
+
return validEnd;
|
|
10493
|
+
}
|
|
10494
|
+
function replaceFile(file, bytes, durable) {
|
|
10495
|
+
const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
10496
|
+
try {
|
|
10497
|
+
const fd = fs7.openSync(temporary, "wx", 384);
|
|
10498
|
+
try {
|
|
10499
|
+
fs7.writeSync(fd, bytes);
|
|
10500
|
+
if (durable)
|
|
10501
|
+
fs7.fsyncSync(fd);
|
|
10502
|
+
} finally {
|
|
10503
|
+
fs7.closeSync(fd);
|
|
10504
|
+
}
|
|
10505
|
+
renameWithRetry(temporary, file);
|
|
10506
|
+
if (durable)
|
|
10507
|
+
fsyncDirectory(path8.dirname(file));
|
|
10508
|
+
} catch (error) {
|
|
10509
|
+
try {
|
|
10510
|
+
fs7.unlinkSync(temporary);
|
|
10511
|
+
} catch {}
|
|
10512
|
+
throw error;
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
var EVENT_VERSION = 1, LogDamage, isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), parseEvent = (line, file, offset) => {
|
|
10516
|
+
let value;
|
|
10517
|
+
try {
|
|
10518
|
+
value = JSON.parse(line);
|
|
10519
|
+
} catch {
|
|
10520
|
+
throw new LogDamage(`${file}: unreadable event at byte ${offset}`);
|
|
10521
|
+
}
|
|
10522
|
+
if (!isObject2(value) || typeof value.v !== "number")
|
|
10523
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
10524
|
+
if (value.v > EVENT_VERSION)
|
|
10525
|
+
throw new LogDamage(`${file}: event format v${value.v} was written by a newer Workit; upgrade Workit to read this task`, true);
|
|
10526
|
+
if (typeof value.seq !== "number" || !Number.isInteger(value.seq) || typeof value.type !== "string" || typeof value.id !== "string" || typeof value.task !== "string" || !isObject2(value.data))
|
|
10527
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
10528
|
+
return value;
|
|
10529
|
+
}, fsyncDirectory = (dir) => {
|
|
10530
|
+
try {
|
|
10531
|
+
const fd = fs7.openSync(dir, "r");
|
|
10532
|
+
try {
|
|
10533
|
+
fs7.fsyncSync(fd);
|
|
10534
|
+
} finally {
|
|
10535
|
+
fs7.closeSync(fd);
|
|
10536
|
+
}
|
|
10537
|
+
} catch (error) {
|
|
10538
|
+
if (process.platform !== "win32")
|
|
10539
|
+
throw error;
|
|
10540
|
+
}
|
|
10541
|
+
}, TRANSIENT_WINDOWS_CODES, isTransientWindowsError = (error) => {
|
|
10542
|
+
if (process.platform !== "win32")
|
|
10543
|
+
return false;
|
|
10544
|
+
const code = error?.code;
|
|
10545
|
+
return typeof code === "string" && TRANSIENT_WINDOWS_CODES.has(code);
|
|
10546
|
+
}, retryTransient = (run) => {
|
|
10547
|
+
if (process.platform !== "win32")
|
|
10548
|
+
return run();
|
|
10549
|
+
for (let attempt = 0;; attempt += 1) {
|
|
10550
|
+
try {
|
|
10551
|
+
return run();
|
|
10552
|
+
} catch (error) {
|
|
10553
|
+
if (attempt >= 20 || !isTransientWindowsError(error))
|
|
10554
|
+
throw error;
|
|
10555
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 * (attempt + 1));
|
|
10556
|
+
}
|
|
10557
|
+
}
|
|
10558
|
+
}, renameWithRetry = (from, to) => retryTransient(() => fs7.renameSync(from, to));
|
|
10559
|
+
var init_event_log = __esm(() => {
|
|
10560
|
+
LogDamage = class LogDamage extends Error {
|
|
10561
|
+
upgrade;
|
|
10562
|
+
constructor(message, upgrade = false) {
|
|
10563
|
+
super(message);
|
|
10564
|
+
this.upgrade = upgrade;
|
|
10565
|
+
}
|
|
10566
|
+
};
|
|
10567
|
+
TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
10568
|
+
});
|
|
10569
|
+
|
|
10570
|
+
// packages/workit-core/src/store/patch.ts
|
|
10571
|
+
function diff(before, after, path = []) {
|
|
10572
|
+
if (equal(before, after))
|
|
10573
|
+
return [];
|
|
10574
|
+
if (isPlainObject2(before) && isPlainObject2(after)) {
|
|
10575
|
+
const ops = [];
|
|
10576
|
+
for (const key of Object.keys(before))
|
|
10577
|
+
if (before[key] !== undefined && after[key] === undefined)
|
|
10578
|
+
ops.push({ op: "del", path: [...path, key] });
|
|
10579
|
+
for (const key of Object.keys(after)) {
|
|
10580
|
+
if (after[key] === undefined)
|
|
10581
|
+
continue;
|
|
10582
|
+
if (before[key] === undefined)
|
|
10583
|
+
ops.push({ op: "set", path: [...path, key], value: after[key] });
|
|
10584
|
+
else
|
|
10585
|
+
ops.push(...diff(before[key], after[key], [...path, key]));
|
|
10586
|
+
}
|
|
10587
|
+
return ops;
|
|
10588
|
+
}
|
|
10589
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
10590
|
+
const ops = [];
|
|
10591
|
+
if (after.length < before.length)
|
|
10592
|
+
ops.push({ op: "cut", path, length: after.length });
|
|
10593
|
+
const shared = Math.min(before.length, after.length);
|
|
10594
|
+
let changed = 0;
|
|
10595
|
+
for (let index = 0;index < shared; index += 1)
|
|
10596
|
+
if (!equal(before[index], after[index]))
|
|
10597
|
+
changed += 1;
|
|
10598
|
+
if (shared > 0 && changed > shared / 2 && changed > 4)
|
|
10599
|
+
return [{ op: "set", path, value: after }];
|
|
10600
|
+
for (let index = 0;index < shared; index += 1)
|
|
10601
|
+
ops.push(...diff(before[index], after[index], [...path, index]));
|
|
10602
|
+
if (after.length > before.length)
|
|
10603
|
+
ops.push({ op: "push", path, values: after.slice(before.length) });
|
|
10604
|
+
return ops;
|
|
10605
|
+
}
|
|
10606
|
+
return [{ op: "set", path, value: after }];
|
|
10607
|
+
}
|
|
10608
|
+
function apply(target, ops) {
|
|
10609
|
+
let root = target;
|
|
10610
|
+
for (const op of ops) {
|
|
10611
|
+
if (op.path.length === 0) {
|
|
10612
|
+
if (op.op === "set")
|
|
10613
|
+
root = clone2(op.value);
|
|
10614
|
+
else if (op.op === "push" && Array.isArray(root))
|
|
10615
|
+
root.push(...clone2(op.values));
|
|
10616
|
+
else if (op.op === "cut" && Array.isArray(root))
|
|
10617
|
+
root.length = op.length;
|
|
10618
|
+
else
|
|
10619
|
+
throw new Error(`patch: ${op.op} at the root is invalid`);
|
|
10620
|
+
continue;
|
|
10621
|
+
}
|
|
10622
|
+
let parent = root;
|
|
10623
|
+
for (const key of op.path.slice(0, -1)) {
|
|
10624
|
+
if (typeof parent !== "object" || parent === null)
|
|
10625
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
10626
|
+
parent = parent[key];
|
|
10627
|
+
}
|
|
10628
|
+
if (typeof parent !== "object" || parent === null)
|
|
10629
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
10630
|
+
const key = op.path.at(-1);
|
|
10631
|
+
const container = parent;
|
|
10632
|
+
if (op.op === "set")
|
|
10633
|
+
container[key] = clone2(op.value);
|
|
10634
|
+
else if (op.op === "del")
|
|
10635
|
+
delete container[key];
|
|
10636
|
+
else {
|
|
10637
|
+
const array = container[key];
|
|
10638
|
+
if (!Array.isArray(array))
|
|
10639
|
+
throw new Error(`patch: ${op.path.join(".")} is not an array`);
|
|
10640
|
+
if (op.op === "push")
|
|
10641
|
+
array.push(...clone2(op.values));
|
|
10642
|
+
else
|
|
10643
|
+
array.length = op.length;
|
|
10644
|
+
}
|
|
10645
|
+
}
|
|
10646
|
+
return root;
|
|
10647
|
+
}
|
|
10648
|
+
var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), equal = (left, right) => {
|
|
10649
|
+
if (left === right)
|
|
10650
|
+
return true;
|
|
10651
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null)
|
|
10652
|
+
return false;
|
|
10653
|
+
if (Array.isArray(left) !== Array.isArray(right))
|
|
10654
|
+
return false;
|
|
10655
|
+
if (Array.isArray(left)) {
|
|
10656
|
+
const other = right;
|
|
10657
|
+
return left.length === other.length && left.every((item, index) => equal(item, other[index]));
|
|
10658
|
+
}
|
|
10659
|
+
const leftKeys = Object.keys(left).filter((key) => left[key] !== undefined);
|
|
10660
|
+
const rightKeys = Object.keys(right).filter((key) => right[key] !== undefined);
|
|
10661
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => equal(left[key], right[key]));
|
|
10662
|
+
}, clone2 = (value) => structuredClone(value), isPath = (value) => Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isInteger(key) && key >= 0), isPatch = (value) => Array.isArray(value) && value.every((op) => {
|
|
10663
|
+
if (!isPlainObject2(op) || !isPath(op.path))
|
|
10664
|
+
return false;
|
|
10665
|
+
if (op.op === "set")
|
|
10666
|
+
return "value" in op;
|
|
10667
|
+
if (op.op === "del")
|
|
10668
|
+
return op.path.length > 0;
|
|
10669
|
+
if (op.op === "push")
|
|
10670
|
+
return Array.isArray(op.values);
|
|
10671
|
+
if (op.op === "cut")
|
|
10672
|
+
return typeof op.length === "number" && Number.isInteger(op.length) && op.length >= 0;
|
|
10673
|
+
return false;
|
|
10674
|
+
});
|
|
10675
|
+
|
|
10676
|
+
// packages/workit-core/src/store/reduce.ts
|
|
10677
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
10678
|
+
function reduce(state, event, blobs) {
|
|
10679
|
+
const data = event.data;
|
|
10680
|
+
const known = KNOWN_TYPES.has(event.type);
|
|
10681
|
+
const carries = "record" in data || "ops" in data || "key" in data;
|
|
10682
|
+
if (!known && !carries) {
|
|
10683
|
+
if (event.critical === true)
|
|
10684
|
+
throw new LogDamage(`event ${event.type} was written by a newer Workit and must be understood; upgrade Workit to read this task`, true);
|
|
10685
|
+
return state;
|
|
10686
|
+
}
|
|
10687
|
+
let next = state;
|
|
10688
|
+
if ("record" in data) {
|
|
10689
|
+
if (!isObject3(data.record))
|
|
10690
|
+
throw new LogDamage(`event ${event.seq} carries a malformed record`);
|
|
10691
|
+
next = {
|
|
10692
|
+
record: decode2(data.record, blobs),
|
|
10693
|
+
key: null,
|
|
10694
|
+
legacy: null
|
|
10695
|
+
};
|
|
10696
|
+
} else if (next === null) {
|
|
10697
|
+
throw new LogDamage(`the log does not start with an opening event (found ${event.type})`);
|
|
10698
|
+
} else
|
|
10699
|
+
next = { ...next };
|
|
10700
|
+
if ("ops" in data) {
|
|
10701
|
+
if (!isPatch(data.ops))
|
|
10702
|
+
throw new LogDamage(`event ${event.seq} carries a malformed patch`);
|
|
10703
|
+
try {
|
|
10704
|
+
next.record = apply(next.record, decode2(data.ops, blobs));
|
|
10705
|
+
} catch (error) {
|
|
10706
|
+
throw new LogDamage(`event ${event.seq} does not apply: ${String(error)}`);
|
|
10707
|
+
}
|
|
10708
|
+
}
|
|
10709
|
+
if ("key" in data)
|
|
10710
|
+
next.key = isKey(data.key) ? data.key : null;
|
|
10711
|
+
if ("legacy" in data && isObject3(data.legacy))
|
|
10712
|
+
next.legacy = {
|
|
10713
|
+
path: String(data.legacy.path ?? ""),
|
|
10714
|
+
digest: String(data.legacy.digest ?? "")
|
|
10715
|
+
};
|
|
10716
|
+
return next;
|
|
10717
|
+
}
|
|
10718
|
+
function eventType(before, after) {
|
|
10719
|
+
const count = (value) => Array.isArray(value) ? value.length : 0;
|
|
10720
|
+
if (before.status !== after.status)
|
|
10721
|
+
return after.status === "closed" ? "task.closed" : after.status === "paused" ? "task.paused" : "task.resumed";
|
|
10722
|
+
if (count(after.evidence) > count(before.evidence)) {
|
|
10723
|
+
const last = after.evidence.at(-1);
|
|
10724
|
+
return last?.data?.observation ? "check.ran" : "evidence.recorded";
|
|
10725
|
+
}
|
|
10726
|
+
if (count(after.findings) > count(before.findings))
|
|
10727
|
+
return "finding.recorded";
|
|
10728
|
+
if (count(after.decisions) > count(before.decisions))
|
|
10729
|
+
return "decision.recorded";
|
|
10730
|
+
if (JSON.stringify(before.findings) !== JSON.stringify(after.findings))
|
|
10731
|
+
return "finding.updated";
|
|
10732
|
+
if (JSON.stringify(before.decisions) !== JSON.stringify(after.decisions))
|
|
10733
|
+
return "decision.updated";
|
|
10734
|
+
if (JSON.stringify(before.workers) !== JSON.stringify(after.workers))
|
|
10735
|
+
return "worker.updated";
|
|
10736
|
+
if (JSON.stringify(before.intent) !== JSON.stringify(after.intent))
|
|
10737
|
+
return "task.revised";
|
|
10738
|
+
if (JSON.stringify(before.assessments) !== JSON.stringify(after.assessments) || JSON.stringify(before.policy) !== JSON.stringify(after.policy))
|
|
10739
|
+
return "task.judged";
|
|
10740
|
+
if (JSON.stringify(before.progress) !== JSON.stringify(after.progress))
|
|
10741
|
+
return "task.noted";
|
|
10742
|
+
return "task.updated";
|
|
10743
|
+
}
|
|
10744
|
+
var OPENING_TYPES, KNOWN_TYPES, isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), isKey = (value) => isObject3(value) && typeof value.key === "string" && (value.kind === "branch" || value.kind === "detached" || value.kind === "dir") && (value.branch === null || typeof value.branch === "string"), BLOB_REF = "$blob", 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, blobDigest = (value) => createHash5("sha256").update(JSON.stringify(value)).digest("hex"), toBlob = (value, blobs) => {
|
|
10745
|
+
if (!isObject3(value))
|
|
10746
|
+
return value;
|
|
10747
|
+
const digest = blobDigest(value);
|
|
10748
|
+
blobs.write(digest, value);
|
|
10749
|
+
return { [BLOB_REF]: digest };
|
|
10750
|
+
}, encodeRecord = (record, blobs) => Array.isArray(record.candidates) ? { ...record, candidates: record.candidates.map((item) => toBlob(item, blobs)) } : record, encodeOps = (ops, blobs) => ops.map((op) => {
|
|
10751
|
+
if (op.path[0] !== "candidates")
|
|
10752
|
+
return op;
|
|
10753
|
+
if (op.op === "push" && op.path.length === 1)
|
|
10754
|
+
return { ...op, values: op.values.map((item) => toBlob(item, blobs)) };
|
|
10755
|
+
if (op.op === "set" && op.path.length === 1 && Array.isArray(op.value))
|
|
10756
|
+
return { ...op, value: op.value.map((item) => toBlob(item, blobs)) };
|
|
10757
|
+
if (op.op === "set" && op.path.length === 2)
|
|
10758
|
+
return { ...op, value: toBlob(op.value, blobs) };
|
|
10759
|
+
return op;
|
|
10760
|
+
}), decode2 = (value, blobs) => {
|
|
10761
|
+
const digest = blobRef(value);
|
|
10762
|
+
if (digest)
|
|
10763
|
+
return blobs.read(digest);
|
|
10764
|
+
if (Array.isArray(value))
|
|
10765
|
+
return value.map((item) => decode2(item, blobs));
|
|
10766
|
+
if (isObject3(value)) {
|
|
10767
|
+
const out = {};
|
|
10768
|
+
for (const [key, item] of Object.entries(value))
|
|
10769
|
+
out[key] = decode2(item, blobs);
|
|
10770
|
+
return out;
|
|
10771
|
+
}
|
|
10772
|
+
return value;
|
|
10773
|
+
};
|
|
10774
|
+
var init_reduce = __esm(() => {
|
|
10775
|
+
init_event_log();
|
|
10776
|
+
OPENING_TYPES = new Set([
|
|
10777
|
+
"task.opened",
|
|
10778
|
+
"task.imported",
|
|
10779
|
+
"migrated.from_v2",
|
|
10780
|
+
"task.checkpoint"
|
|
10781
|
+
]);
|
|
10782
|
+
KNOWN_TYPES = new Set([
|
|
10783
|
+
...OPENING_TYPES,
|
|
10784
|
+
"task.bound",
|
|
10785
|
+
"task.noted",
|
|
10786
|
+
"task.revised",
|
|
10787
|
+
"task.judged",
|
|
10788
|
+
"task.paused",
|
|
10789
|
+
"task.resumed",
|
|
10790
|
+
"task.closed",
|
|
10791
|
+
"task.updated",
|
|
10792
|
+
"check.ran",
|
|
10793
|
+
"evidence.recorded",
|
|
10794
|
+
"finding.recorded",
|
|
10795
|
+
"finding.updated",
|
|
10796
|
+
"decision.recorded",
|
|
10797
|
+
"decision.updated",
|
|
10798
|
+
"worker.updated"
|
|
10799
|
+
]);
|
|
10800
|
+
});
|
|
10801
|
+
|
|
10802
|
+
// packages/workit-core/src/store/notes.ts
|
|
10803
|
+
var SLOT, reportMigration = (report) => {
|
|
10804
|
+
try {
|
|
10805
|
+
const reporter = globalThis[SLOT];
|
|
10806
|
+
if (typeof reporter === "function")
|
|
10807
|
+
reporter(report);
|
|
10808
|
+
} catch {}
|
|
10809
|
+
}, migrationReporterInstalled = () => typeof globalThis[SLOT] === "function";
|
|
10810
|
+
var init_notes = __esm(() => {
|
|
10811
|
+
SLOT = Symbol.for("workit.migrationReporter");
|
|
10812
|
+
});
|
|
10813
|
+
|
|
10146
10814
|
// packages/workit-core/src/core/task-store.ts
|
|
10147
|
-
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
10148
10815
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10149
|
-
import
|
|
10150
|
-
import
|
|
10816
|
+
import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
|
|
10817
|
+
import * as fs8 from "node:fs";
|
|
10818
|
+
import path9 from "node:path";
|
|
10151
10819
|
|
|
10152
10820
|
class TaskStore {
|
|
10153
10821
|
root;
|
|
10822
|
+
origin;
|
|
10154
10823
|
lockTimeoutMs;
|
|
10824
|
+
actor;
|
|
10825
|
+
migrateOnRead;
|
|
10826
|
+
located;
|
|
10827
|
+
migrationChecked = false;
|
|
10828
|
+
compactDue = new Set;
|
|
10155
10829
|
constructor(root, options = {}) {
|
|
10156
|
-
|
|
10830
|
+
const resolved = path9.resolve(root);
|
|
10831
|
+
this.origin = fs8.existsSync(resolved) ? fs8.realpathSync(resolved) : resolved;
|
|
10832
|
+
this.located = resolveStore(this.origin);
|
|
10833
|
+
this.root = checkoutRootOf(this.origin, this.located);
|
|
10157
10834
|
this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
|
|
10835
|
+
this.actor = options.actor ?? envActor();
|
|
10836
|
+
this.migrateOnRead = options.migrateOnRead;
|
|
10837
|
+
}
|
|
10838
|
+
location() {
|
|
10839
|
+
if (this.located !== undefined && !(this.located instanceof Error) && !this.located.shared && fs8.existsSync(path9.join(this.root, ".git"))) {
|
|
10840
|
+
this.located = undefined;
|
|
10841
|
+
this.migrationChecked = false;
|
|
10842
|
+
}
|
|
10843
|
+
if (this.located === undefined)
|
|
10844
|
+
this.located = resolveStore(this.root);
|
|
10845
|
+
return this.located instanceof Error ? failure2("storage_error", this.located.message, { path: this.root }) : success(null, null, this.located);
|
|
10846
|
+
}
|
|
10847
|
+
currentKey() {
|
|
10848
|
+
const location = this.location();
|
|
10849
|
+
if (!location.ok)
|
|
10850
|
+
return location;
|
|
10851
|
+
return success(null, null, resolveTaskKey(this.root, location.data));
|
|
10158
10852
|
}
|
|
10159
10853
|
readTask(taskId) {
|
|
10160
10854
|
if (!validId(taskId))
|
|
10161
10855
|
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
10162
|
-
const
|
|
10163
|
-
if (!
|
|
10856
|
+
const ready = this.ready();
|
|
10857
|
+
if (!ready.ok)
|
|
10858
|
+
return ready;
|
|
10859
|
+
const loaded = this.load(taskId);
|
|
10860
|
+
if (!loaded.ok)
|
|
10861
|
+
return loaded;
|
|
10862
|
+
if (!loaded.data)
|
|
10164
10863
|
return failure2("not_found", "task not found", { taskId });
|
|
10165
|
-
if (result.result.ok && result.result.data.id !== taskId)
|
|
10166
|
-
return failure2("recovery_required", "task filename and record ID differ", { taskId });
|
|
10167
|
-
if (result.result.ok) {
|
|
10168
|
-
const workspace = this.readWorkspace();
|
|
10169
|
-
if (!workspace.ok)
|
|
10170
|
-
return workspace;
|
|
10171
|
-
if (!workspace.data || result.result.data.workspaceId !== workspace.data.id)
|
|
10172
|
-
return failure2("recovery_required", "task workspace binding is invalid", { taskId });
|
|
10173
|
-
}
|
|
10174
|
-
return result.result;
|
|
10175
|
-
}
|
|
10176
|
-
listTasks() {
|
|
10177
|
-
if (!fs6.existsSync(this.tasksDir))
|
|
10178
|
-
return success(null, null, []);
|
|
10179
|
-
let names;
|
|
10180
|
-
try {
|
|
10181
|
-
names = fs6.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
|
|
10182
|
-
} catch (error) {
|
|
10183
|
-
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
10184
|
-
path: this.tasksDir
|
|
10185
|
-
});
|
|
10186
|
-
}
|
|
10187
10864
|
const workspace = this.readWorkspace();
|
|
10188
10865
|
if (!workspace.ok)
|
|
10189
10866
|
return workspace;
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10867
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
10868
|
+
return bound ?? success(null, null, loaded.data.record);
|
|
10869
|
+
}
|
|
10870
|
+
listTasks() {
|
|
10871
|
+
const listed = this.listTaskIndex();
|
|
10872
|
+
if (!listed.ok)
|
|
10873
|
+
return listed;
|
|
10194
10874
|
const tasks = [];
|
|
10195
|
-
for (const
|
|
10196
|
-
const
|
|
10197
|
-
if (!
|
|
10198
|
-
|
|
10199
|
-
if (
|
|
10200
|
-
|
|
10201
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
10202
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
10203
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
10204
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
10205
|
-
tasks.push(item.result.data);
|
|
10875
|
+
for (const entry of listed.data.toSorted((left, right) => left.id.localeCompare(right.id))) {
|
|
10876
|
+
const loaded = this.load(entry.id);
|
|
10877
|
+
if (!loaded.ok)
|
|
10878
|
+
return loaded;
|
|
10879
|
+
if (loaded.data)
|
|
10880
|
+
tasks.push(loaded.data.record);
|
|
10206
10881
|
}
|
|
10207
10882
|
return success(null, null, tasks);
|
|
10208
10883
|
}
|
|
10209
10884
|
listTaskIndex() {
|
|
10210
|
-
|
|
10885
|
+
const all = this.listStoreIndex();
|
|
10886
|
+
if (!all.ok)
|
|
10887
|
+
return all;
|
|
10888
|
+
const workspace = this.readWorkspace();
|
|
10889
|
+
if (!workspace.ok)
|
|
10890
|
+
return workspace;
|
|
10891
|
+
if (!workspace.data)
|
|
10211
10892
|
return success(null, null, []);
|
|
10893
|
+
const id = workspace.data.id;
|
|
10894
|
+
return success(null, null, all.data.filter((entry) => entry.workspaceId === id));
|
|
10895
|
+
}
|
|
10896
|
+
listStoreIndex() {
|
|
10897
|
+
const ready = this.ready();
|
|
10898
|
+
if (!ready.ok)
|
|
10899
|
+
return ready;
|
|
10900
|
+
const tasksDir = this.paths().tasks;
|
|
10212
10901
|
let names;
|
|
10213
10902
|
try {
|
|
10214
|
-
names =
|
|
10903
|
+
names = fs8.existsSync(tasksDir) ? fs8.readdirSync(tasksDir).filter((name) => validId(name)) : [];
|
|
10215
10904
|
} catch (error) {
|
|
10216
10905
|
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
10217
|
-
path:
|
|
10906
|
+
path: tasksDir
|
|
10218
10907
|
});
|
|
10219
10908
|
}
|
|
10220
|
-
const
|
|
10221
|
-
if (!workspace.ok)
|
|
10222
|
-
return workspace;
|
|
10223
|
-
if (!workspace.data)
|
|
10224
|
-
return failure2("recovery_required", "task workspace binding is invalid", {
|
|
10225
|
-
path: this.workspacePath
|
|
10226
|
-
});
|
|
10227
|
-
const stored = this.readIndex(workspace.data.id);
|
|
10909
|
+
const stored = this.readIndex();
|
|
10228
10910
|
const entries = [];
|
|
10229
10911
|
let changed = Object.keys(stored).length !== names.length;
|
|
10230
10912
|
for (const name of names.toSorted()) {
|
|
10231
|
-
const
|
|
10232
|
-
const signature = fileSignature(file);
|
|
10913
|
+
const signature = fileSignature(this.eventsPath(name));
|
|
10233
10914
|
if (signature === null) {
|
|
10234
10915
|
changed = true;
|
|
10235
10916
|
continue;
|
|
10236
10917
|
}
|
|
10237
|
-
const cached = stored[name
|
|
10918
|
+
const cached = stored[name];
|
|
10238
10919
|
if (cached && cached.file === signature && !racySignature(signature)) {
|
|
10239
10920
|
entries.push(cached);
|
|
10240
10921
|
continue;
|
|
10241
10922
|
}
|
|
10242
|
-
const
|
|
10243
|
-
if (!
|
|
10923
|
+
const loaded = this.load(name);
|
|
10924
|
+
if (!loaded.ok)
|
|
10925
|
+
return loaded;
|
|
10926
|
+
if (!loaded.data)
|
|
10244
10927
|
continue;
|
|
10245
|
-
|
|
10246
|
-
return item.result;
|
|
10247
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
10248
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
10249
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
10250
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
10251
|
-
const entry = indexEntry(item.result.data, signature);
|
|
10928
|
+
const entry = indexEntry(loaded.data.record, loaded.data.key, loaded.data.legacy !== null, signature);
|
|
10252
10929
|
if (!cached || canonicalJson(cached) !== canonicalJson(entry))
|
|
10253
10930
|
changed = true;
|
|
10254
10931
|
entries.push(entry);
|
|
10255
10932
|
}
|
|
10256
10933
|
if (changed)
|
|
10257
|
-
this.writeIndex(
|
|
10934
|
+
this.writeIndex(entries);
|
|
10258
10935
|
return success(null, null, entries);
|
|
10259
10936
|
}
|
|
10260
10937
|
readWorkspace() {
|
|
10261
|
-
const
|
|
10262
|
-
if (!
|
|
10263
|
-
return
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10938
|
+
const ready = this.ready();
|
|
10939
|
+
if (!ready.ok)
|
|
10940
|
+
return ready;
|
|
10941
|
+
const file = this.paths().workspace;
|
|
10942
|
+
let bytes;
|
|
10943
|
+
try {
|
|
10944
|
+
bytes = retryTransient(() => fs8.readFileSync(file, "utf8"));
|
|
10945
|
+
} catch (error) {
|
|
10946
|
+
if (error.code === "ENOENT")
|
|
10947
|
+
return success(null, null, null);
|
|
10948
|
+
return failure2("recovery_required", `workspace record cannot be read: ${String(error)}`, {
|
|
10949
|
+
path: file
|
|
10267
10950
|
});
|
|
10268
|
-
|
|
10951
|
+
}
|
|
10952
|
+
const parsed = this.parseRecord(bytes, workspaceRecordSchema, "workspace");
|
|
10953
|
+
if (!parsed.ok)
|
|
10954
|
+
return parsed;
|
|
10955
|
+
if (parsed.data.root !== this.root && !sameDirectoryIdentity(parsed.data.root, this.root))
|
|
10956
|
+
return failure2("recovery_required", "workspace root binding is invalid", { path: file });
|
|
10957
|
+
return parsed;
|
|
10958
|
+
}
|
|
10959
|
+
implicitTask(input) {
|
|
10960
|
+
const ready = this.ready(input.create !== false);
|
|
10961
|
+
if (!ready.ok)
|
|
10962
|
+
return ready;
|
|
10963
|
+
const key = this.currentKey();
|
|
10964
|
+
if (!key.ok)
|
|
10965
|
+
return key;
|
|
10966
|
+
const found = this.openTaskForKey(key.data.key);
|
|
10967
|
+
if (!found.ok)
|
|
10968
|
+
return found;
|
|
10969
|
+
const workspace = this.readWorkspace();
|
|
10970
|
+
if (!workspace.ok)
|
|
10971
|
+
return workspace;
|
|
10972
|
+
if (found.data && workspace.data && found.data.workspaceId === workspace.data.id && (found.data.key === key.data.key || input.create === false)) {
|
|
10973
|
+
const task = this.readTask(found.data.id);
|
|
10974
|
+
return task.ok ? success(task.revision, null, { task: task.data, created: false, key: key.data }) : task;
|
|
10975
|
+
}
|
|
10976
|
+
if (input.create === false) {
|
|
10977
|
+
if (!found.data)
|
|
10978
|
+
return success(null, null, null);
|
|
10979
|
+
const loaded = this.load(found.data.id);
|
|
10980
|
+
if (!loaded.ok)
|
|
10981
|
+
return loaded;
|
|
10982
|
+
return loaded.data ? success(null, null, { task: loaded.data.record, created: false, key: key.data }) : success(null, null, null);
|
|
10983
|
+
}
|
|
10984
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => this.implicitLocked(input, key.data)));
|
|
10985
|
+
}
|
|
10986
|
+
implicitLocked(input, key) {
|
|
10987
|
+
{
|
|
10988
|
+
const again = this.openTaskForKey(key.key);
|
|
10989
|
+
if (!again.ok)
|
|
10990
|
+
return again;
|
|
10991
|
+
if (again.data) {
|
|
10992
|
+
const moved = this.bindLocked(again.data.id, key, input.now);
|
|
10993
|
+
return moved.ok ? success(moved.data.revision, null, { task: moved.data, created: false, key }) : moved;
|
|
10994
|
+
}
|
|
10995
|
+
const label = key.kind === "branch" ? `branch ${key.branch}` : key.kind === "detached" ? "a detached HEAD" : path9.basename(this.root) || this.root;
|
|
10996
|
+
const intent = {
|
|
10997
|
+
objective: input.objective?.trim() || `Work on ${label}`,
|
|
10998
|
+
scope: { description: `the checkout (${label})`, paths: ["."], exclusions: [] },
|
|
10999
|
+
authorityRefs: []
|
|
11000
|
+
};
|
|
11001
|
+
const created = this.createLocked({ intent, provenance: input.provenance, expectedWorkspaceRevision: null, now: input.now }, false, key, false);
|
|
11002
|
+
return created.ok ? success(created.data.revision, null, { task: created.data, created: true, key }) : created;
|
|
11003
|
+
}
|
|
11004
|
+
}
|
|
11005
|
+
adoptTask(taskId) {
|
|
11006
|
+
if (!validId(taskId))
|
|
11007
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
11008
|
+
const ready = this.ready(true);
|
|
11009
|
+
if (!ready.ok)
|
|
11010
|
+
return ready;
|
|
11011
|
+
const key = this.currentKey();
|
|
11012
|
+
if (!key.ok)
|
|
11013
|
+
return key;
|
|
11014
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => {
|
|
11015
|
+
const holder = this.openTaskForKey(key.data.key);
|
|
11016
|
+
if (!holder.ok)
|
|
11017
|
+
return holder;
|
|
11018
|
+
if (holder.data && holder.data.id !== taskId)
|
|
11019
|
+
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 });
|
|
11020
|
+
return this.bindLocked(taskId, key.data);
|
|
11021
|
+
}));
|
|
11022
|
+
}
|
|
11023
|
+
keyReport() {
|
|
11024
|
+
const key = this.currentKey();
|
|
11025
|
+
if (!key.ok)
|
|
11026
|
+
return key;
|
|
11027
|
+
const bound = this.openTasksForKey(key.data.key);
|
|
11028
|
+
if (!bound.ok)
|
|
11029
|
+
return bound;
|
|
11030
|
+
return success(null, null, { key: key.data, bound: bound.data });
|
|
10269
11031
|
}
|
|
10270
11032
|
create(input, provenance, expectedWorkspaceRevision) {
|
|
10271
11033
|
const value = "intent" in input ? input : {
|
|
@@ -10273,82 +11035,16 @@ class TaskStore {
|
|
|
10273
11035
|
provenance,
|
|
10274
11036
|
expectedWorkspaceRevision: expectedWorkspaceRevision ?? null
|
|
10275
11037
|
};
|
|
10276
|
-
|
|
10277
|
-
|
|
10278
|
-
|
|
10279
|
-
|
|
10280
|
-
if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
|
|
10281
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
10282
|
-
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
10283
|
-
actualWorkspaceRevision: current.data?.revision ?? null
|
|
10284
|
-
});
|
|
10285
|
-
}
|
|
10286
|
-
if (!value.provenance)
|
|
10287
|
-
return failure2("invalid_input", "provenance is required");
|
|
10288
|
-
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
10289
|
-
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
10290
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
10291
|
-
if (current.data && !previousWorkspaceBytes)
|
|
10292
|
-
return failure2("storage_error", "workspace snapshot disappeared during creation");
|
|
10293
|
-
const workspace = current.data ? {
|
|
10294
|
-
...current.data,
|
|
10295
|
-
revision: newRevision(),
|
|
10296
|
-
root: this.root,
|
|
10297
|
-
runtime: stampNew(current.data)
|
|
10298
|
-
} : {
|
|
10299
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10300
|
-
id: newId(),
|
|
10301
|
-
revision: newRevision(),
|
|
10302
|
-
root: this.root,
|
|
10303
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10304
|
-
writer: null
|
|
10305
|
-
};
|
|
10306
|
-
const timestamp = value.now ?? now();
|
|
10307
|
-
const task = {
|
|
10308
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10309
|
-
id: newId(),
|
|
10310
|
-
workspaceId: workspace.id,
|
|
10311
|
-
revision: newRevision(),
|
|
10312
|
-
createdAt: timestamp,
|
|
10313
|
-
updatedAt: timestamp,
|
|
10314
|
-
origin: null,
|
|
10315
|
-
intent: {
|
|
10316
|
-
id: newId(),
|
|
10317
|
-
recordedAt: timestamp,
|
|
10318
|
-
provenance: value.provenance,
|
|
10319
|
-
data: value.intent
|
|
10320
|
-
},
|
|
10321
|
-
constraints: [],
|
|
10322
|
-
status: "active",
|
|
10323
|
-
closure: null,
|
|
10324
|
-
progress: { summary: "", nextAction: null, blockers: [] },
|
|
10325
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10326
|
-
assessments: [],
|
|
10327
|
-
policy: null,
|
|
10328
|
-
policyChanges: [],
|
|
10329
|
-
candidates: [],
|
|
10330
|
-
evidence: [],
|
|
10331
|
-
decisions: [],
|
|
10332
|
-
findings: [],
|
|
10333
|
-
workers: []
|
|
10334
|
-
};
|
|
10335
|
-
const writtenWorkspace = this.replaceSnapshot(this.workspacePath, workspace, previousWorkspaceBytes);
|
|
10336
|
-
if (!writtenWorkspace.ok)
|
|
10337
|
-
return writtenWorkspace;
|
|
10338
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), task, null);
|
|
10339
|
-
if (!writtenTask.ok) {
|
|
10340
|
-
if (current.data && previousWorkspaceBytes) {
|
|
10341
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
10342
|
-
if (!restored.ok)
|
|
10343
|
-
return failure2("external_outcome_unknown", "task creation failed and workspace restoration is uncertain", { operation: "create", outcome: "unknown" });
|
|
10344
|
-
}
|
|
10345
|
-
return failure2("external_outcome_unknown", "workspace created but task write is uncertain", { operation: "create", outcome: "unknown" });
|
|
10346
|
-
}
|
|
10347
|
-
return success(task.revision, workspace.revision, task);
|
|
10348
|
-
});
|
|
11038
|
+
const ready = this.ready(true);
|
|
11039
|
+
if (!ready.ok)
|
|
11040
|
+
return ready;
|
|
11041
|
+
return this.withLock(this.paths().checkoutLock, () => this.createLocked(value, true, null));
|
|
10349
11042
|
}
|
|
10350
11043
|
importTask(input) {
|
|
10351
|
-
|
|
11044
|
+
const ready = this.ready(true);
|
|
11045
|
+
if (!ready.ok)
|
|
11046
|
+
return ready;
|
|
11047
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
10352
11048
|
const current = this.readWorkspace();
|
|
10353
11049
|
if (!current.ok)
|
|
10354
11050
|
return current;
|
|
@@ -10380,23 +11076,8 @@ class TaskStore {
|
|
|
10380
11076
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
10381
11077
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
10382
11078
|
});
|
|
10383
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
10384
|
-
if (current.data && !previousWorkspaceBytes)
|
|
10385
|
-
return failure2("storage_error", "workspace snapshot disappeared during import");
|
|
10386
11079
|
const timestamp = input.now ?? now();
|
|
10387
|
-
const workspace = current.data
|
|
10388
|
-
...current.data,
|
|
10389
|
-
revision: newRevision(),
|
|
10390
|
-
root: this.root,
|
|
10391
|
-
runtime: stampNew(current.data)
|
|
10392
|
-
} : {
|
|
10393
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10394
|
-
id: input.workspaceId ?? newId(),
|
|
10395
|
-
revision: newRevision(),
|
|
10396
|
-
root: this.root,
|
|
10397
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10398
|
-
writer: null
|
|
10399
|
-
};
|
|
11080
|
+
const workspace = this.nextWorkspace(current.data, input.workspaceId);
|
|
10400
11081
|
const task = {
|
|
10401
11082
|
...input.task,
|
|
10402
11083
|
workspaceId: workspace.id,
|
|
@@ -10411,62 +11092,46 @@ class TaskStore {
|
|
|
10411
11092
|
const validTask = taskRecordSchema.safeParse(task);
|
|
10412
11093
|
if (!validTask.success)
|
|
10413
11094
|
return failure2("invalid_input", "imported task does not satisfy its schema");
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), validTask.data, null);
|
|
10418
|
-
if (!writtenTask.ok) {
|
|
10419
|
-
if (current.data && previousWorkspaceBytes) {
|
|
10420
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
10421
|
-
if (!restored.ok)
|
|
10422
|
-
return failure2("external_outcome_unknown", "import failed and workspace restoration is uncertain", { operation: "import", outcome: "unknown" });
|
|
10423
|
-
}
|
|
10424
|
-
return failure2("external_outcome_unknown", `workspace created but task import is uncertain: ${writtenTask.error}`, {
|
|
10425
|
-
operation: "import",
|
|
10426
|
-
outcome: "unknown",
|
|
10427
|
-
path: writtenTask.details.path
|
|
10428
|
-
});
|
|
10429
|
-
}
|
|
10430
|
-
return success(validTask.data.revision, workspace.revision, validTask.data);
|
|
11095
|
+
return this.writeNewTask(current.data, workspace, validTask.data, "task.imported", null, {
|
|
11096
|
+
operation: "import"
|
|
11097
|
+
});
|
|
10431
11098
|
});
|
|
10432
11099
|
}
|
|
10433
11100
|
mutateTask(taskId, expected, update, timestamp) {
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
const
|
|
10441
|
-
if (!
|
|
10442
|
-
return
|
|
11101
|
+
if (!validId(taskId))
|
|
11102
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
11103
|
+
const ready = this.ready(true);
|
|
11104
|
+
if (!ready.ok)
|
|
11105
|
+
return ready;
|
|
11106
|
+
const result = this.withLock(this.taskLockPath(taskId), () => {
|
|
11107
|
+
const loaded = this.loadBound(taskId);
|
|
11108
|
+
if (!loaded.ok)
|
|
11109
|
+
return loaded;
|
|
11110
|
+
if (loaded.data.record.revision !== expected)
|
|
11111
|
+
return this.conflict(expected, loaded.data.record.revision);
|
|
10443
11112
|
const context = { now: timestamp ?? now(), revision: newRevision() };
|
|
10444
11113
|
let changed;
|
|
10445
11114
|
try {
|
|
10446
|
-
changed = update(
|
|
11115
|
+
changed = update(structuredClone(loaded.data.record), Object.freeze({ ...context }));
|
|
10447
11116
|
} catch (error) {
|
|
10448
11117
|
return failure2("storage_error", `task mutation failed: ${String(error)}`);
|
|
10449
11118
|
}
|
|
10450
11119
|
if (!changed.ok)
|
|
10451
11120
|
return changed;
|
|
10452
|
-
const
|
|
10453
|
-
|
|
10454
|
-
id: taskId,
|
|
10455
|
-
workspaceId: current.data.workspaceId,
|
|
10456
|
-
createdAt: current.data.createdAt,
|
|
10457
|
-
revision: context.revision,
|
|
10458
|
-
updatedAt: context.now,
|
|
10459
|
-
runtime: stampNew(current.data)
|
|
10460
|
-
};
|
|
10461
|
-
const valid = taskRecordSchema.safeParse(record);
|
|
10462
|
-
if (!valid.success)
|
|
11121
|
+
const valid = this.nextTask(loaded.data.record, changed.data, context);
|
|
11122
|
+
if (!valid)
|
|
10463
11123
|
return failure2("invalid_input", "task mutation produced an invalid record");
|
|
10464
|
-
const written = this.
|
|
10465
|
-
return written.ok ? success(valid.
|
|
11124
|
+
const written = this.appendPatch(loaded.data, valid);
|
|
11125
|
+
return written.ok ? success(valid.revision, null, valid) : written;
|
|
10466
11126
|
});
|
|
11127
|
+
this.compactPending();
|
|
11128
|
+
return result;
|
|
10467
11129
|
}
|
|
10468
11130
|
mutateWorkspace(expected, update) {
|
|
10469
|
-
|
|
11131
|
+
const ready = this.ready(true);
|
|
11132
|
+
if (!ready.ok)
|
|
11133
|
+
return ready;
|
|
11134
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
10470
11135
|
const current = this.readWorkspace();
|
|
10471
11136
|
if (!current.ok)
|
|
10472
11137
|
return current;
|
|
@@ -10474,9 +11139,6 @@ class TaskStore {
|
|
|
10474
11139
|
return failure2("not_found", "workspace not found");
|
|
10475
11140
|
if (current.data.revision !== expected)
|
|
10476
11141
|
return this.workspaceConflict(expected, current.data.revision);
|
|
10477
|
-
const previousBytes = this.snapshotBytes(this.workspacePath);
|
|
10478
|
-
if (!previousBytes)
|
|
10479
|
-
return failure2("storage_error", "workspace snapshot disappeared during mutation");
|
|
10480
11142
|
const context = { now: now(), revision: newRevision() };
|
|
10481
11143
|
let changed;
|
|
10482
11144
|
try {
|
|
@@ -10486,23 +11148,27 @@ class TaskStore {
|
|
|
10486
11148
|
}
|
|
10487
11149
|
if (!changed.ok)
|
|
10488
11150
|
return changed;
|
|
10489
|
-
const
|
|
11151
|
+
const valid = workspaceRecordSchema.safeParse({
|
|
10490
11152
|
...changed.data,
|
|
10491
11153
|
id: current.data.id,
|
|
10492
11154
|
root: this.root,
|
|
10493
11155
|
revision: context.revision,
|
|
10494
11156
|
runtime: stampNew(current.data)
|
|
10495
|
-
};
|
|
10496
|
-
const valid = workspaceRecordSchema.safeParse(record);
|
|
11157
|
+
});
|
|
10497
11158
|
if (!valid.success)
|
|
10498
11159
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
10499
|
-
const written = this.
|
|
11160
|
+
const written = this.writeWorkspace(valid.data);
|
|
10500
11161
|
return written.ok ? success(valid.data.revision, valid.data.revision, valid.data) : written;
|
|
10501
11162
|
});
|
|
10502
11163
|
}
|
|
10503
11164
|
mutateTaskAndWorkspace(input) {
|
|
10504
|
-
|
|
10505
|
-
|
|
11165
|
+
if (!validId(input.taskId))
|
|
11166
|
+
return failure2("invalid_input", "task ID is invalid", { taskId: input.taskId });
|
|
11167
|
+
const ready = this.ready(true);
|
|
11168
|
+
if (!ready.ok)
|
|
11169
|
+
return ready;
|
|
11170
|
+
const result = this.withLock(this.paths().checkoutLock, () => this.withLock(this.taskLockPath(input.taskId), () => {
|
|
11171
|
+
const task = this.loadBound(input.taskId);
|
|
10506
11172
|
if (!task.ok)
|
|
10507
11173
|
return task;
|
|
10508
11174
|
const workspace = this.readWorkspace();
|
|
@@ -10513,14 +11179,10 @@ class TaskStore {
|
|
|
10513
11179
|
const expected = input.expectedRevision;
|
|
10514
11180
|
if (!expected)
|
|
10515
11181
|
return failure2("invalid_input", "task revision is required");
|
|
10516
|
-
if (task.data.revision !== expected)
|
|
10517
|
-
return this.conflict(expected, task.data.revision);
|
|
11182
|
+
if (task.data.record.revision !== expected)
|
|
11183
|
+
return this.conflict(expected, task.data.record.revision);
|
|
10518
11184
|
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
10519
11185
|
return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
|
|
10520
|
-
const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
|
|
10521
|
-
const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
|
|
10522
|
-
if (!previousWorkspaceBytes || !previousTaskBytes)
|
|
10523
|
-
return failure2("storage_error", "snapshot disappeared during coupled mutation");
|
|
10524
11186
|
const workspaceContext = { now: input.now ?? now(), revision: newRevision() };
|
|
10525
11187
|
let changedWorkspace;
|
|
10526
11188
|
try {
|
|
@@ -10539,286 +11201,69 @@ class TaskStore {
|
|
|
10539
11201
|
});
|
|
10540
11202
|
if (!nextWorkspace.success)
|
|
10541
11203
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
10542
|
-
const reserved = this.
|
|
11204
|
+
const reserved = this.writeWorkspace(nextWorkspace.data);
|
|
10543
11205
|
if (!reserved.ok)
|
|
10544
11206
|
return reserved;
|
|
11207
|
+
const uncertain = (message) => {
|
|
11208
|
+
this.markUncertain(nextWorkspace.data);
|
|
11209
|
+
return failure2("external_outcome_unknown", message, {
|
|
11210
|
+
operation: "coupled_mutation",
|
|
11211
|
+
outcome: "unknown"
|
|
11212
|
+
});
|
|
11213
|
+
};
|
|
10545
11214
|
const taskContext = { now: input.now ?? now(), revision: newRevision() };
|
|
10546
11215
|
let changedTask;
|
|
10547
11216
|
try {
|
|
10548
|
-
changedTask = input.task(task.data, Object.freeze({ ...taskContext }));
|
|
11217
|
+
changedTask = input.task(structuredClone(task.data.record), Object.freeze({ ...taskContext }));
|
|
10549
11218
|
} catch (error) {
|
|
10550
|
-
|
|
10551
|
-
return failure2("external_outcome_unknown", `workspace reserved but task mutation threw: ${String(error)}`, { operation: "coupled_mutation", outcome: "unknown" });
|
|
10552
|
-
}
|
|
10553
|
-
if (!changedTask.ok) {
|
|
10554
|
-
this.markUncertain(nextWorkspace.data);
|
|
10555
|
-
return failure2("external_outcome_unknown", "workspace reserved but task update is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
10556
|
-
}
|
|
10557
|
-
const nextTask = taskRecordSchema.safeParse({
|
|
10558
|
-
...changedTask.data,
|
|
10559
|
-
id: input.taskId,
|
|
10560
|
-
workspaceId: task.data.workspaceId,
|
|
10561
|
-
createdAt: task.data.createdAt,
|
|
10562
|
-
revision: taskContext.revision,
|
|
10563
|
-
updatedAt: taskContext.now,
|
|
10564
|
-
runtime: stampNew(task.data)
|
|
10565
|
-
});
|
|
10566
|
-
if (!nextTask.success) {
|
|
10567
|
-
this.markUncertain(nextWorkspace.data);
|
|
10568
|
-
return failure2("external_outcome_unknown", "workspace reserved but task update is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
10569
|
-
}
|
|
10570
|
-
const written = this.replaceSnapshot(this.taskPath(input.taskId), nextTask.data, previousTaskBytes);
|
|
10571
|
-
if (!written.ok) {
|
|
10572
|
-
this.markUncertain(nextWorkspace.data);
|
|
10573
|
-
return failure2("external_outcome_unknown", "workspace reserved but task replacement is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
11219
|
+
return uncertain(`workspace reserved but task mutation threw: ${String(error)}`);
|
|
10574
11220
|
}
|
|
10575
|
-
|
|
10576
|
-
task
|
|
11221
|
+
if (!changedTask.ok)
|
|
11222
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
11223
|
+
const nextTask = this.nextTask(task.data.record, changedTask.data, taskContext);
|
|
11224
|
+
if (!nextTask)
|
|
11225
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
11226
|
+
const written = this.appendPatch(task.data, nextTask);
|
|
11227
|
+
if (!written.ok)
|
|
11228
|
+
return uncertain("workspace reserved but task append is uncertain");
|
|
11229
|
+
return success(nextTask.revision, nextWorkspace.data.revision, {
|
|
11230
|
+
task: nextTask,
|
|
10577
11231
|
workspace: nextWorkspace.data
|
|
10578
11232
|
});
|
|
10579
|
-
});
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
if (!fs6.existsSync(this.recoveryDir))
|
|
10583
|
-
return success(null, null, []);
|
|
10584
|
-
try {
|
|
10585
|
-
const candidates = [];
|
|
10586
|
-
for (const name of fs6.readdirSync(this.recoveryDir)) {
|
|
10587
|
-
const match = RECOVERY_NAME.exec(name);
|
|
10588
|
-
if (match)
|
|
10589
|
-
candidates.push({
|
|
10590
|
-
target: match[1],
|
|
10591
|
-
path: path7.join(this.recoveryDir, name),
|
|
10592
|
-
digest: match[3]
|
|
10593
|
-
});
|
|
10594
|
-
}
|
|
10595
|
-
return success(null, null, candidates);
|
|
10596
|
-
} catch (error) {
|
|
10597
|
-
return failure2("storage_error", `unable to list recovery: ${String(error)}`, {
|
|
10598
|
-
path: this.recoveryDir
|
|
10599
|
-
});
|
|
10600
|
-
}
|
|
10601
|
-
}
|
|
10602
|
-
recoverTask(taskId, input) {
|
|
10603
|
-
return this.recover("task", taskId, input);
|
|
10604
|
-
}
|
|
10605
|
-
recoverWorkspace(input) {
|
|
10606
|
-
return this.recover("workspace", null, input);
|
|
10607
|
-
}
|
|
10608
|
-
recover(target, taskId, input) {
|
|
10609
|
-
if (taskId !== null && !validId(taskId))
|
|
10610
|
-
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
10611
|
-
if (!input.reason || !Array.isArray(input.authorityRefs) || typeof input.processEvidence !== "function" || !validDigest(input.expectedBytes) || !validDigest(input.snapshotDigest))
|
|
10612
|
-
return failure2("invalid_input", "recovery authority is invalid");
|
|
10613
|
-
if (input.authorityRefs.some((ref) => !refSchema.safeParse(ref).success))
|
|
10614
|
-
return failure2("invalid_input", "recovery references are invalid");
|
|
10615
|
-
try {
|
|
10616
|
-
const workspace = this.readWorkspace();
|
|
10617
|
-
if (!workspace.ok && target === "task")
|
|
10618
|
-
return workspace;
|
|
10619
|
-
const workspaceValue = workspace.ok ? workspace.data : null;
|
|
10620
|
-
if (workspaceValue && workspaceValue.revision !== input.expectedWorkspaceRevision)
|
|
10621
|
-
return this.conflict(input.expectedWorkspaceRevision, workspaceValue.revision);
|
|
10622
|
-
const file = target === "workspace" ? this.workspacePath : this.taskPath(taskId);
|
|
10623
|
-
const currentBytes = this.snapshotBytes(file);
|
|
10624
|
-
if (!currentBytes)
|
|
10625
|
-
return failure2("not_found", "snapshot not found");
|
|
10626
|
-
if (digestBytes(currentBytes) !== input.expectedBytes)
|
|
10627
|
-
return failure2("revision_conflict", "snapshot bytes do not match expected bytes");
|
|
10628
|
-
let selectedBytes = currentBytes;
|
|
10629
|
-
if (digestBytes(currentBytes) !== input.snapshotDigest) {
|
|
10630
|
-
const candidate = this.findRecovery(target, taskId, input.snapshotDigest, workspaceValue);
|
|
10631
|
-
if (!candidate)
|
|
10632
|
-
return failure2("recovery_required", "validated recovery snapshot not found");
|
|
10633
|
-
selectedBytes = candidate;
|
|
10634
|
-
}
|
|
10635
|
-
const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
10636
|
-
if (!selectedRecord.ok)
|
|
10637
|
-
return selectedRecord;
|
|
10638
|
-
if (target === "workspace") {
|
|
10639
|
-
const parsed = selectedRecord;
|
|
10640
|
-
if (!parsed.ok || parsed.data.root !== this.root)
|
|
10641
|
-
return failure2("recovery_required", "workspace recovery binding is invalid");
|
|
10642
|
-
} else {
|
|
10643
|
-
const parsed = selectedRecord;
|
|
10644
|
-
if (!parsed.ok || parsed.data.id !== taskId || !workspaceValue || parsed.data.workspaceId !== workspaceValue.id)
|
|
10645
|
-
return failure2("recovery_required", "task recovery binding is invalid");
|
|
10646
|
-
}
|
|
10647
|
-
const lock = this.readLockSnapshot();
|
|
10648
|
-
if (!lock.ok)
|
|
10649
|
-
return lock;
|
|
10650
|
-
const writer = target === "workspace" ? selectedRecord.data.writer : workspaceValue?.writer ?? null;
|
|
10651
|
-
const evidence = this.processEvidence(input, lock.data?.data ?? null, writer);
|
|
10652
|
-
if (!evidence.ok)
|
|
10653
|
-
return evidence;
|
|
10654
|
-
if (lock.data && (evidence.data.pid !== lock.data.data.pid || evidence.data.processStart !== lock.data.data.processStart))
|
|
10655
|
-
return failure2("recovery_required", "process evidence does not match metadata lock");
|
|
10656
|
-
if (writer && (evidence.data.state !== "accounted_for" || evidence.data.ownerDigest !== sha256(canonicalJson(writer))))
|
|
10657
|
-
return failure2("recovery_required", "workspace writer is not accounted for");
|
|
10658
|
-
const recoveryGate = { reclaimed: false };
|
|
10659
|
-
const result = this.withLock((handle) => {
|
|
10660
|
-
if (lock.data && !recoveryGate.reclaimed)
|
|
10661
|
-
return failure2("recovery_required", "metadata lock changed during recovery", {
|
|
10662
|
-
path: this.lockPath
|
|
10663
|
-
});
|
|
10664
|
-
if (!handle.verifyStillHeld())
|
|
10665
|
-
return failure2("recovery_required", "metadata lock was compromised", {
|
|
10666
|
-
path: this.lockPath
|
|
10667
|
-
});
|
|
10668
|
-
const reacquiredBytes = this.snapshotBytes(file);
|
|
10669
|
-
if (!reacquiredBytes)
|
|
10670
|
-
return failure2("recovery_required", "snapshot disappeared during recovery");
|
|
10671
|
-
if (digestBytes(reacquiredBytes) !== input.expectedBytes)
|
|
10672
|
-
return failure2("revision_conflict", "snapshot changed during recovery");
|
|
10673
|
-
if (target === "task") {
|
|
10674
|
-
const currentWorkspace = this.readWorkspace();
|
|
10675
|
-
if (!currentWorkspace.ok || !currentWorkspace.data)
|
|
10676
|
-
return failure2("recovery_required", "workspace changed during recovery");
|
|
10677
|
-
if (currentWorkspace.data.revision !== input.expectedWorkspaceRevision)
|
|
10678
|
-
return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
|
|
10679
|
-
}
|
|
10680
|
-
if (target === "workspace") {
|
|
10681
|
-
const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
|
|
10682
|
-
if (!parsed.ok)
|
|
10683
|
-
return parsed;
|
|
10684
|
-
const value = { ...parsed.data, revision: newRevision(), writer: null };
|
|
10685
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
10686
|
-
return replaced.ok ? success(value.revision, value.revision, value) : replaced;
|
|
10687
|
-
}
|
|
10688
|
-
const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
10689
|
-
if (!parsed.ok)
|
|
10690
|
-
return parsed;
|
|
10691
|
-
const value = {
|
|
10692
|
-
...parsed.data,
|
|
10693
|
-
revision: newRevision(),
|
|
10694
|
-
updatedAt: now(),
|
|
10695
|
-
status: parsed.data.status === "active" ? "paused" : parsed.data.status
|
|
10696
|
-
};
|
|
10697
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
10698
|
-
return replaced.ok ? success(value.revision, null, value) : replaced;
|
|
10699
|
-
}, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
|
|
10700
|
-
return result;
|
|
10701
|
-
} catch (error) {
|
|
10702
|
-
return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
|
|
10703
|
-
}
|
|
10704
|
-
}
|
|
10705
|
-
processEvidence(input, lock, writer) {
|
|
10706
|
-
const lockCopy = lock ? this.deepFreeze(structuredClone(lock)) : null;
|
|
10707
|
-
const writerCopy = writer ? this.deepFreeze(structuredClone(writer)) : null;
|
|
10708
|
-
const lockDigest = lockCopy ? sha256(canonicalJson(lockCopy)) : null;
|
|
10709
|
-
const writerDigest = writerCopy ? sha256(canonicalJson(writerCopy)) : null;
|
|
10710
|
-
try {
|
|
10711
|
-
const result = input.processEvidence(lockCopy, writerCopy);
|
|
10712
|
-
if (lockCopy && sha256(canonicalJson(lockCopy)) !== lockDigest)
|
|
10713
|
-
return failure2("recovery_required", "process evidence mutated lock identity");
|
|
10714
|
-
if (writerCopy && sha256(canonicalJson(writerCopy)) !== writerDigest)
|
|
10715
|
-
return failure2("recovery_required", "process evidence mutated writer identity");
|
|
10716
|
-
if (!result.ok)
|
|
10717
|
-
return result;
|
|
10718
|
-
const parsed = processEvidenceSchema.safeParse(result.data);
|
|
10719
|
-
return parsed.success ? success(null, null, parsed.data) : failure2("recovery_required", "process evidence is invalid");
|
|
10720
|
-
} catch (error) {
|
|
10721
|
-
return failure2("recovery_required", `process evidence failed: ${String(error)}`);
|
|
10722
|
-
}
|
|
10723
|
-
}
|
|
10724
|
-
deepFreeze(value) {
|
|
10725
|
-
if (value && typeof value === "object") {
|
|
10726
|
-
for (const child of Object.values(value))
|
|
10727
|
-
this.deepFreeze(child);
|
|
10728
|
-
Object.freeze(value);
|
|
10729
|
-
}
|
|
10730
|
-
return value;
|
|
10731
|
-
}
|
|
10732
|
-
readLockSnapshot() {
|
|
10733
|
-
try {
|
|
10734
|
-
const stat = fs6.lstatSync(this.lockPath);
|
|
10735
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
10736
|
-
return failure2("recovery_required", "metadata lock is not a regular file", {
|
|
10737
|
-
path: this.lockPath
|
|
10738
|
-
});
|
|
10739
|
-
const raw = fs6.readFileSync(this.lockPath, "utf8");
|
|
10740
|
-
return success(null, null, { raw, data: parseMetadataLock(raw) });
|
|
10741
|
-
} catch (error) {
|
|
10742
|
-
if (error?.code === "ENOENT")
|
|
10743
|
-
return success(null, null, null);
|
|
10744
|
-
return failure2("recovery_required", `metadata lock is invalid: ${String(error)}`, {
|
|
10745
|
-
path: this.lockPath
|
|
10746
|
-
});
|
|
10747
|
-
}
|
|
10748
|
-
}
|
|
10749
|
-
metadataLockOptions() {
|
|
10750
|
-
const inProcess = heldInProcess.has(this.root);
|
|
10751
|
-
return {
|
|
10752
|
-
lockPath: this.lockPath,
|
|
10753
|
-
staleMs: Number.MAX_SAFE_INTEGER,
|
|
10754
|
-
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
10755
|
-
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
10756
|
-
staleRecovery: "remove-if-unchanged",
|
|
10757
|
-
shouldReclaim: ({ payload, nowMs }) => {
|
|
10758
|
-
let ageMs = null;
|
|
10759
|
-
try {
|
|
10760
|
-
ageMs = nowMs - fs6.lstatSync(this.lockPath).mtimeMs;
|
|
10761
|
-
} catch {}
|
|
10762
|
-
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
10763
|
-
},
|
|
10764
|
-
shouldRemoveStaleLock: () => true,
|
|
10765
|
-
parsePayload: parseMetadataLockOrNull,
|
|
10766
|
-
payload: () => ({
|
|
10767
|
-
pid: process.pid,
|
|
10768
|
-
processStart: processStartOf(process.pid),
|
|
10769
|
-
host: localLockHost(),
|
|
10770
|
-
nonce: randomUUID2()
|
|
10771
|
-
})
|
|
10772
|
-
};
|
|
10773
|
-
}
|
|
10774
|
-
acquireMetadataLock(options) {
|
|
10775
|
-
clearAbandonedReclaimGuard(this.lockPath);
|
|
10776
|
-
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
10777
|
-
while (true) {
|
|
10778
|
-
try {
|
|
10779
|
-
return acquireFileLockSync(this.workspacePath, {
|
|
10780
|
-
...options,
|
|
10781
|
-
timeoutMs: Math.max(0, deadline - Date.now())
|
|
10782
|
-
});
|
|
10783
|
-
} catch (error) {
|
|
10784
|
-
const code = error?.code;
|
|
10785
|
-
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
10786
|
-
throw error;
|
|
10787
|
-
if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
|
|
10788
|
-
throw error;
|
|
10789
|
-
if (code !== "file_lock_stale")
|
|
10790
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
10791
|
-
}
|
|
10792
|
-
}
|
|
10793
|
-
}
|
|
10794
|
-
externalActionLockOptions() {
|
|
10795
|
-
const options = this.metadataLockOptions();
|
|
10796
|
-
return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
|
|
11233
|
+
}));
|
|
11234
|
+
this.compactPending();
|
|
11235
|
+
return result;
|
|
10797
11236
|
}
|
|
10798
11237
|
async withExternalActionLock(operation, reentrant = false) {
|
|
10799
11238
|
const activeRoots = externalActionLockRoots.getStore();
|
|
10800
11239
|
if (activeRoots && [...activeRoots].some((root) => root === this.root || sameDirectoryIdentity(root, this.root)))
|
|
10801
11240
|
return operation();
|
|
11241
|
+
const ready = this.ready(true);
|
|
11242
|
+
if (!ready.ok)
|
|
11243
|
+
return ready;
|
|
11244
|
+
const lockPath = this.paths().checkoutLock;
|
|
10802
11245
|
try {
|
|
10803
|
-
this.
|
|
11246
|
+
this.initializeStorage();
|
|
10804
11247
|
} catch (error) {
|
|
10805
11248
|
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
10806
|
-
path: this.
|
|
11249
|
+
path: this.paths().dir
|
|
10807
11250
|
});
|
|
10808
11251
|
}
|
|
10809
11252
|
let handle;
|
|
10810
11253
|
try {
|
|
10811
|
-
|
|
11254
|
+
const options = this.lockOptions(lockPath);
|
|
11255
|
+
handle = this.acquire(lockPath, {
|
|
11256
|
+
...options,
|
|
11257
|
+
payload: () => ({ ...options.payload(), externalAction: true })
|
|
11258
|
+
});
|
|
10812
11259
|
} catch (error) {
|
|
10813
|
-
return this.lockFailure(error);
|
|
11260
|
+
return this.lockFailure(lockPath, error);
|
|
10814
11261
|
}
|
|
10815
|
-
|
|
11262
|
+
hold(lockPath);
|
|
10816
11263
|
let result;
|
|
10817
11264
|
try {
|
|
10818
11265
|
if (!handle.verifyStillHeld())
|
|
10819
|
-
result = failure2("recovery_required", "metadata lock was compromised", {
|
|
10820
|
-
path: this.lockPath
|
|
10821
|
-
});
|
|
11266
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
10822
11267
|
else {
|
|
10823
11268
|
try {
|
|
10824
11269
|
const run = () => operation();
|
|
@@ -10832,529 +11277,1147 @@ class TaskStore {
|
|
|
10832
11277
|
if (!handle.verifyStillHeld())
|
|
10833
11278
|
result = failure2("external_outcome_unknown", "metadata lock was compromised", {
|
|
10834
11279
|
outcome: "unknown",
|
|
10835
|
-
path:
|
|
11280
|
+
path: lockPath
|
|
10836
11281
|
});
|
|
10837
11282
|
} catch {
|
|
10838
11283
|
result = failure2("external_outcome_unknown", "metadata lock could not be verified", {
|
|
10839
11284
|
outcome: "unknown",
|
|
10840
|
-
path:
|
|
11285
|
+
path: lockPath
|
|
10841
11286
|
});
|
|
10842
11287
|
}
|
|
10843
11288
|
}
|
|
10844
11289
|
} catch {
|
|
10845
11290
|
result = failure2("external_outcome_unknown", "external action lock operation failed", {
|
|
10846
11291
|
outcome: "unknown",
|
|
10847
|
-
path:
|
|
11292
|
+
path: lockPath
|
|
10848
11293
|
});
|
|
10849
11294
|
}
|
|
10850
|
-
|
|
11295
|
+
drop(lockPath);
|
|
10851
11296
|
try {
|
|
10852
11297
|
retryTransient(() => handle.release());
|
|
10853
11298
|
} catch (error) {
|
|
10854
|
-
const released = this.lockFailure(error);
|
|
11299
|
+
const released = this.lockFailure(lockPath, error);
|
|
10855
11300
|
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
10856
11301
|
return result.ok ? failure2("external_outcome_unknown", "external action metadata lock release is uncertain", {
|
|
10857
11302
|
outcome: "unknown",
|
|
10858
|
-
path:
|
|
11303
|
+
path: lockPath
|
|
10859
11304
|
}) : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
10860
11305
|
}
|
|
10861
11306
|
return result;
|
|
10862
11307
|
}
|
|
10863
|
-
|
|
10864
|
-
const
|
|
10865
|
-
options.
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
11308
|
+
collectGarbage(options = {}) {
|
|
11309
|
+
const dryRun = options.dryRun === true;
|
|
11310
|
+
const compactAbove = options.compactAbove ?? COMPACT_ABOVE;
|
|
11311
|
+
const report = {
|
|
11312
|
+
dryRun,
|
|
11313
|
+
compacted: { tasks: [], eventsFolded: 0, bytesBefore: 0, bytesAfter: 0 },
|
|
11314
|
+
blobs: { removed: 0, removedBytes: 0, kept: 0 },
|
|
11315
|
+
temporary: { removed: 0 },
|
|
11316
|
+
legacyRecovery: null,
|
|
11317
|
+
retried: [],
|
|
11318
|
+
failed: []
|
|
11319
|
+
};
|
|
11320
|
+
const ready = this.ready(!dryRun);
|
|
11321
|
+
if (!ready.ok)
|
|
11322
|
+
return ready;
|
|
11323
|
+
const dirs = this.paths();
|
|
11324
|
+
const recovery = path9.join(this.root, ".workit", "recovery");
|
|
11325
|
+
if (fs8.existsSync(recovery)) {
|
|
11326
|
+
let files = 0;
|
|
11327
|
+
let bytes = 0;
|
|
11328
|
+
try {
|
|
11329
|
+
for (const name of fs8.readdirSync(recovery)) {
|
|
11330
|
+
const stat = fs8.lstatSync(path9.join(recovery, name));
|
|
11331
|
+
if (!stat.isFile())
|
|
11332
|
+
continue;
|
|
11333
|
+
files += 1;
|
|
11334
|
+
bytes += stat.size;
|
|
11335
|
+
}
|
|
11336
|
+
} catch {}
|
|
11337
|
+
const remove = options.pruneRecovery === true && !dryRun;
|
|
11338
|
+
if (remove)
|
|
11339
|
+
fs8.rmSync(recovery, { recursive: true, force: true });
|
|
11340
|
+
report.legacyRecovery = { path: recovery, files, bytes, removed: remove };
|
|
11341
|
+
}
|
|
11342
|
+
if (!fs8.existsSync(dirs.tasks))
|
|
11343
|
+
return success(null, null, report);
|
|
11344
|
+
const names = fs8.readdirSync(dirs.tasks).filter((name) => validId(name));
|
|
11345
|
+
for (const name of names) {
|
|
11346
|
+
const file = this.eventsPath(name);
|
|
11347
|
+
let size = 0;
|
|
11348
|
+
try {
|
|
11349
|
+
size = fs8.statSync(file).size;
|
|
11350
|
+
} catch {
|
|
11351
|
+
continue;
|
|
11352
|
+
}
|
|
11353
|
+
const lines = this.countEvents(file);
|
|
11354
|
+
if (lines > compactAbove) {
|
|
11355
|
+
if (dryRun) {
|
|
11356
|
+
report.compacted.tasks.push(name);
|
|
11357
|
+
report.compacted.eventsFolded += lines - COMPACT_KEEP;
|
|
11358
|
+
report.compacted.bytesBefore += size;
|
|
11359
|
+
} else {
|
|
11360
|
+
const compacted = this.compact(name);
|
|
11361
|
+
if (!compacted.ok)
|
|
11362
|
+
(compacted.code === "busy" ? report.retried : report.failed).push(name);
|
|
11363
|
+
else {
|
|
11364
|
+
report.compacted.tasks.push(name);
|
|
11365
|
+
report.compacted.eventsFolded += compacted.data.folded;
|
|
11366
|
+
report.compacted.bytesBefore += size;
|
|
11367
|
+
report.compacted.bytesAfter += compacted.data.bytes;
|
|
11368
|
+
}
|
|
11369
|
+
}
|
|
11370
|
+
}
|
|
11371
|
+
}
|
|
11372
|
+
const nowMs = Date.now();
|
|
11373
|
+
const sweep = () => {
|
|
11374
|
+
const references = new Set;
|
|
11375
|
+
for (const name of names)
|
|
11376
|
+
try {
|
|
11377
|
+
for (const match of fs8.readFileSync(this.eventsPath(name), "utf8").matchAll(/"\$blob":"([0-9a-f]{64})"/g))
|
|
11378
|
+
references.add(match[1]);
|
|
11379
|
+
} catch {}
|
|
11380
|
+
if (fs8.existsSync(dirs.blobs))
|
|
11381
|
+
for (const name of fs8.readdirSync(dirs.blobs)) {
|
|
11382
|
+
const digest = name.replace(/\.json$/, "");
|
|
11383
|
+
const file = path9.join(dirs.blobs, name);
|
|
11384
|
+
let stat;
|
|
11385
|
+
try {
|
|
11386
|
+
stat = fs8.lstatSync(file);
|
|
11387
|
+
} catch {
|
|
11388
|
+
continue;
|
|
11389
|
+
}
|
|
11390
|
+
if (references.has(digest) || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS) {
|
|
11391
|
+
report.blobs.kept += 1;
|
|
11392
|
+
continue;
|
|
11393
|
+
}
|
|
11394
|
+
report.blobs.removed += 1;
|
|
11395
|
+
report.blobs.removedBytes += stat.size;
|
|
11396
|
+
if (!dryRun)
|
|
11397
|
+
fs8.rmSync(file, { force: true });
|
|
11398
|
+
}
|
|
11399
|
+
return success(null, null, null);
|
|
10875
11400
|
};
|
|
10876
|
-
|
|
11401
|
+
const swept = dryRun ? sweep() : this.withLock(path9.join(dirs.dir, "gc.lock"), sweep);
|
|
11402
|
+
if (!swept.ok)
|
|
11403
|
+
return swept;
|
|
11404
|
+
const temporaryDirs = [
|
|
11405
|
+
dirs.dir,
|
|
11406
|
+
dirs.tasks,
|
|
11407
|
+
dirs.blobs,
|
|
11408
|
+
dirs.checkout,
|
|
11409
|
+
...names.map((name) => path9.join(dirs.tasks, name))
|
|
11410
|
+
];
|
|
11411
|
+
for (const directory of temporaryDirs) {
|
|
11412
|
+
if (!fs8.existsSync(directory))
|
|
11413
|
+
continue;
|
|
11414
|
+
for (const name of fs8.readdirSync(directory)) {
|
|
11415
|
+
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
11416
|
+
continue;
|
|
11417
|
+
const file = path9.join(directory, name);
|
|
11418
|
+
try {
|
|
11419
|
+
const stat = fs8.lstatSync(file);
|
|
11420
|
+
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
11421
|
+
continue;
|
|
11422
|
+
} catch {
|
|
11423
|
+
continue;
|
|
11424
|
+
}
|
|
11425
|
+
report.temporary.removed += 1;
|
|
11426
|
+
if (!dryRun)
|
|
11427
|
+
fs8.rmSync(file, { force: true });
|
|
11428
|
+
}
|
|
11429
|
+
}
|
|
11430
|
+
return success(null, null, report);
|
|
10877
11431
|
}
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
const
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
11432
|
+
paths() {
|
|
11433
|
+
const location = this.location();
|
|
11434
|
+
const dir = location.ok ? location.data.dir : path9.join(this.root, ".workit");
|
|
11435
|
+
const checkout = path9.join(dir, "checkouts", checkoutSlug(this.root));
|
|
11436
|
+
return {
|
|
11437
|
+
dir,
|
|
11438
|
+
shared: location.ok ? location.data.shared : false,
|
|
11439
|
+
marker: path9.join(dir, "store.json"),
|
|
11440
|
+
checkout,
|
|
11441
|
+
workspace: path9.join(checkout, "workspace.json"),
|
|
11442
|
+
checkoutLock: path9.join(checkout, "metadata.lock"),
|
|
11443
|
+
tasks: path9.join(dir, "tasks"),
|
|
11444
|
+
index: path9.join(dir, "task-index.json"),
|
|
11445
|
+
blobs: path9.join(dir, "blobs", "candidates"),
|
|
11446
|
+
legacy: path9.join(dir, "legacy", checkoutSlug(this.root), "v2"),
|
|
11447
|
+
v2: path9.join(this.root, ".workit")
|
|
10886
11448
|
};
|
|
10887
|
-
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
10888
11449
|
}
|
|
10889
|
-
|
|
11450
|
+
eventsPath(taskId) {
|
|
11451
|
+
return path9.join(this.paths().tasks, taskId, "events.jsonl");
|
|
11452
|
+
}
|
|
11453
|
+
snapshotPath(taskId) {
|
|
11454
|
+
return path9.join(this.paths().tasks, taskId, "snapshot.json");
|
|
11455
|
+
}
|
|
11456
|
+
taskLockPath(taskId) {
|
|
11457
|
+
return path9.join(this.paths().tasks, taskId, "lock");
|
|
11458
|
+
}
|
|
11459
|
+
ready(write = false) {
|
|
11460
|
+
const location = this.location();
|
|
11461
|
+
if (!location.ok)
|
|
11462
|
+
return location;
|
|
11463
|
+
if (this.migrationChecked)
|
|
11464
|
+
return success(null, null, null);
|
|
11465
|
+
const dirs = this.paths();
|
|
10890
11466
|
try {
|
|
10891
|
-
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
11467
|
+
const marker = JSON.parse(fs8.readFileSync(dirs.marker, "utf8"));
|
|
11468
|
+
if (typeof marker.version === "number" && marker.version > STORE_FORMAT)
|
|
11469
|
+
return failure2("recovery_required", `the workit store at ${dirs.dir} has format ${marker.version}; upgrade Workit to use it`, { path: dirs.marker });
|
|
11470
|
+
} catch {}
|
|
11471
|
+
this.migrationChecked = true;
|
|
11472
|
+
const settled = this.settleMigrations(write);
|
|
11473
|
+
if (!settled.ok)
|
|
11474
|
+
this.migrationChecked = false;
|
|
11475
|
+
return settled;
|
|
11476
|
+
}
|
|
11477
|
+
settleMigrations(write) {
|
|
11478
|
+
const moved = this.adoptLocalStore();
|
|
11479
|
+
if (!moved.ok)
|
|
11480
|
+
return moved;
|
|
11481
|
+
const pending = this.pendingMigrations();
|
|
11482
|
+
if (pending.length > 0 && !write && !(this.migrateOnRead ?? migrationReporterInstalled()))
|
|
11483
|
+
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" });
|
|
11484
|
+
for (const v2 of this.v2Dirs()) {
|
|
11485
|
+
const migrated = this.migrateV2(v2);
|
|
11486
|
+
if (!migrated.ok)
|
|
11487
|
+
return migrated;
|
|
10896
11488
|
}
|
|
10897
|
-
|
|
10898
|
-
|
|
11489
|
+
return success(null, null, null);
|
|
11490
|
+
}
|
|
11491
|
+
blobStore() {
|
|
11492
|
+
const dir = this.paths().blobs;
|
|
11493
|
+
return {
|
|
11494
|
+
read: (digest) => {
|
|
11495
|
+
try {
|
|
11496
|
+
return JSON.parse(fs8.readFileSync(path9.join(dir, `${digest}.json`), "utf8"));
|
|
11497
|
+
} catch (error) {
|
|
11498
|
+
throw new LogDamage(`stored candidate ${digest} is missing or unreadable: ${String(error)}`);
|
|
11499
|
+
}
|
|
11500
|
+
},
|
|
11501
|
+
write: (digest, value) => {
|
|
11502
|
+
const file = path9.join(dir, `${digest}.json`);
|
|
11503
|
+
if (fs8.existsSync(file)) {
|
|
11504
|
+
const stamp = new Date;
|
|
11505
|
+
try {
|
|
11506
|
+
fs8.utimesSync(file, stamp, stamp);
|
|
11507
|
+
return;
|
|
11508
|
+
} catch {}
|
|
11509
|
+
}
|
|
11510
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
11511
|
+
replaceFile(file, JSON.stringify(value), true);
|
|
11512
|
+
}
|
|
11513
|
+
};
|
|
11514
|
+
}
|
|
11515
|
+
load(taskId) {
|
|
10899
11516
|
try {
|
|
10900
|
-
|
|
11517
|
+
const loaded = this.loadOrThrow(taskId);
|
|
11518
|
+
if (!loaded)
|
|
11519
|
+
return success(null, null, null);
|
|
11520
|
+
const parsed = this.parseTask(loaded.state.record, taskId);
|
|
11521
|
+
if (!parsed.ok)
|
|
11522
|
+
return parsed;
|
|
11523
|
+
return success(null, null, {
|
|
11524
|
+
record: parsed.data,
|
|
11525
|
+
key: loaded.state.key,
|
|
11526
|
+
legacy: loaded.state.legacy,
|
|
11527
|
+
seq: loaded.seq,
|
|
11528
|
+
lastId: loaded.lastId,
|
|
11529
|
+
lastStart: loaded.lastStart,
|
|
11530
|
+
end: loaded.end,
|
|
11531
|
+
size: loaded.size,
|
|
11532
|
+
snapshotSeq: loaded.snapshotSeq
|
|
11533
|
+
});
|
|
10901
11534
|
} catch (error) {
|
|
10902
|
-
|
|
11535
|
+
if (error instanceof LogDamage)
|
|
11536
|
+
return failure2("recovery_required", error.upgrade ? error.message : `task ${taskId} log is damaged: ${error.message}`, { taskId, path: this.eventsPath(taskId) });
|
|
11537
|
+
return failure2("recovery_required", `task ${taskId} cannot be read: ${String(error)}`, {
|
|
11538
|
+
taskId
|
|
11539
|
+
});
|
|
10903
11540
|
}
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
11541
|
+
}
|
|
11542
|
+
loadOrThrow(taskId) {
|
|
11543
|
+
const blobs = this.blobStore();
|
|
11544
|
+
const file = this.eventsPath(taskId);
|
|
11545
|
+
const snapshot = this.readSnapshot(taskId);
|
|
11546
|
+
if (snapshot) {
|
|
10907
11547
|
try {
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
}
|
|
10916
|
-
|
|
10917
|
-
|
|
11548
|
+
const tail = readLog(file, snapshot.lastStart, snapshot.seq);
|
|
11549
|
+
const first = tail?.events[0];
|
|
11550
|
+
if (tail && first && first.id === snapshot.lastId) {
|
|
11551
|
+
let state = {
|
|
11552
|
+
record: snapshot.record,
|
|
11553
|
+
key: snapshot.key,
|
|
11554
|
+
legacy: snapshot.legacy
|
|
11555
|
+
};
|
|
11556
|
+
for (const event of tail.events.slice(1))
|
|
11557
|
+
state = reduce(state, event, blobs);
|
|
11558
|
+
return this.loadedFrom(state, tail, snapshot.seq);
|
|
10918
11559
|
}
|
|
10919
11560
|
} catch (error) {
|
|
10920
|
-
|
|
11561
|
+
if (error instanceof LogDamage && error.upgrade)
|
|
11562
|
+
throw error;
|
|
10921
11563
|
}
|
|
10922
11564
|
}
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
const releaseFailure = this.lockFailure(error);
|
|
10929
|
-
const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
|
|
10930
|
-
result = result.ok ? releaseFailure : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
10931
|
-
}
|
|
11565
|
+
const log = readLog(file, 0, null);
|
|
11566
|
+
if (!log || log.events.length === 0) {
|
|
11567
|
+
if (log && log.size > 0)
|
|
11568
|
+
throw new LogDamage("the log holds no complete event");
|
|
11569
|
+
return null;
|
|
10932
11570
|
}
|
|
10933
|
-
|
|
11571
|
+
if (!OPENING_TYPES.has(log.events[0].type))
|
|
11572
|
+
throw new LogDamage(`the log starts with ${log.events[0].type}, not an opening event`);
|
|
11573
|
+
let state = null;
|
|
11574
|
+
for (const event of log.events)
|
|
11575
|
+
state = reduce(state, event, blobs);
|
|
11576
|
+
return this.loadedFrom(state, log, 0);
|
|
11577
|
+
}
|
|
11578
|
+
loadedFrom(state, log, snapshotSeq) {
|
|
11579
|
+
const last = log.events.at(-1);
|
|
11580
|
+
return {
|
|
11581
|
+
state,
|
|
11582
|
+
seq: last.seq,
|
|
11583
|
+
lastId: last.id,
|
|
11584
|
+
lastStart: log.lastStart,
|
|
11585
|
+
end: log.end,
|
|
11586
|
+
size: log.size,
|
|
11587
|
+
snapshotSeq
|
|
11588
|
+
};
|
|
10934
11589
|
}
|
|
10935
|
-
|
|
10936
|
-
const probe = `${this.lockPath}.${process.pid}.${randomUUID2()}.probe`;
|
|
11590
|
+
readSnapshot(taskId) {
|
|
10937
11591
|
try {
|
|
10938
|
-
|
|
11592
|
+
const value = JSON.parse(fs8.readFileSync(this.snapshotPath(taskId), "utf8"));
|
|
11593
|
+
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))
|
|
11594
|
+
return null;
|
|
11595
|
+
return value;
|
|
10939
11596
|
} catch {
|
|
10940
|
-
return
|
|
11597
|
+
return null;
|
|
10941
11598
|
}
|
|
11599
|
+
}
|
|
11600
|
+
writeSnapshot(taskId, loaded) {
|
|
11601
|
+
const snapshot = {
|
|
11602
|
+
v: 1,
|
|
11603
|
+
task: taskId,
|
|
11604
|
+
seq: loaded.seq,
|
|
11605
|
+
lastId: loaded.lastId,
|
|
11606
|
+
lastStart: loaded.lastStart,
|
|
11607
|
+
end: loaded.end,
|
|
11608
|
+
key: loaded.key,
|
|
11609
|
+
legacy: loaded.legacy,
|
|
11610
|
+
record: loaded.record
|
|
11611
|
+
};
|
|
10942
11612
|
try {
|
|
10943
|
-
|
|
11613
|
+
replaceFile(this.snapshotPath(taskId), JSON.stringify(snapshot), false);
|
|
10944
11614
|
} catch {}
|
|
10945
|
-
return true;
|
|
10946
11615
|
}
|
|
10947
|
-
|
|
11616
|
+
loadBound(taskId) {
|
|
11617
|
+
const loaded = this.load(taskId);
|
|
11618
|
+
if (!loaded.ok)
|
|
11619
|
+
return loaded;
|
|
11620
|
+
if (!loaded.data)
|
|
11621
|
+
return failure2("not_found", "task not found", { taskId });
|
|
11622
|
+
const workspace = this.readWorkspace();
|
|
11623
|
+
if (!workspace.ok)
|
|
11624
|
+
return workspace;
|
|
11625
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
11626
|
+
return bound ?? success(null, null, loaded.data);
|
|
11627
|
+
}
|
|
11628
|
+
bindingError(task, workspace) {
|
|
11629
|
+
if (workspace && task.workspaceId === workspace.id)
|
|
11630
|
+
return null;
|
|
11631
|
+
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 });
|
|
11632
|
+
}
|
|
11633
|
+
nextTask(current, changed, context) {
|
|
11634
|
+
const valid = taskRecordSchema.safeParse({
|
|
11635
|
+
...changed,
|
|
11636
|
+
id: current.id,
|
|
11637
|
+
workspaceId: current.workspaceId,
|
|
11638
|
+
createdAt: current.createdAt,
|
|
11639
|
+
revision: context.revision,
|
|
11640
|
+
updatedAt: context.now,
|
|
11641
|
+
runtime: stampNew(current)
|
|
11642
|
+
});
|
|
11643
|
+
return valid.success ? valid.data : null;
|
|
11644
|
+
}
|
|
11645
|
+
event(taskId, seq, type, data) {
|
|
11646
|
+
return {
|
|
11647
|
+
v: EVENT_VERSION,
|
|
11648
|
+
seq,
|
|
11649
|
+
at: new Date().toISOString(),
|
|
11650
|
+
id: newId(),
|
|
11651
|
+
task: taskId,
|
|
11652
|
+
actor: this.actor,
|
|
11653
|
+
type,
|
|
11654
|
+
data
|
|
11655
|
+
};
|
|
11656
|
+
}
|
|
11657
|
+
appendPatch(loaded, next, extra = {}) {
|
|
11658
|
+
const taskId = next.id;
|
|
10948
11659
|
try {
|
|
10949
|
-
|
|
10950
|
-
|
|
11660
|
+
const blobs = this.blobStore();
|
|
11661
|
+
const before = loaded.record;
|
|
11662
|
+
const after = next;
|
|
11663
|
+
const ops = encodeOps(diff(before, after), blobs);
|
|
11664
|
+
const { type, ...data } = extra;
|
|
11665
|
+
if (data.key)
|
|
11666
|
+
data.key = { ...data.key, boundAt: data.key.boundAt ?? new Date().toISOString() };
|
|
11667
|
+
const event = this.event(taskId, loaded.seq + 1, type ?? ("key" in extra ? "task.bound" : eventType(before, after)), { ops, ...data });
|
|
11668
|
+
const start = appendEvent(this.eventsPath(taskId), event, loaded.end);
|
|
11669
|
+
const end = start + Buffer.byteLength(`${JSON.stringify(event)}
|
|
11670
|
+
`);
|
|
11671
|
+
const key = "key" in data ? data.key ?? null : loaded.key;
|
|
11672
|
+
const state = {
|
|
11673
|
+
record: next,
|
|
11674
|
+
key,
|
|
11675
|
+
legacy: extra.legacy ?? loaded.legacy,
|
|
11676
|
+
seq: event.seq,
|
|
11677
|
+
lastId: event.id,
|
|
11678
|
+
lastStart: start,
|
|
11679
|
+
end
|
|
11680
|
+
};
|
|
11681
|
+
if (event.seq - loaded.snapshotSeq >= SNAPSHOT_EVERY || next.status !== loaded.record.status || loaded.snapshotSeq === 0)
|
|
11682
|
+
this.writeSnapshot(taskId, state);
|
|
11683
|
+
if (end > AUTO_COMPACT_BYTES)
|
|
11684
|
+
this.compactDue.add(taskId);
|
|
11685
|
+
this.indexTask(next, key, (extra.legacy ?? loaded.legacy) !== null);
|
|
11686
|
+
return success(null, null, null);
|
|
10951
11687
|
} catch (error) {
|
|
10952
|
-
return error
|
|
11688
|
+
return failure2("storage_error", `task append failed: ${String(error)}`, {
|
|
11689
|
+
path: this.eventsPath(taskId)
|
|
11690
|
+
});
|
|
10953
11691
|
}
|
|
10954
11692
|
}
|
|
10955
|
-
|
|
10956
|
-
const
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
11693
|
+
openTask(record, type, key, legacy = null) {
|
|
11694
|
+
const dir = path9.join(this.paths().tasks, record.id);
|
|
11695
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
11696
|
+
const blobs = this.blobStore();
|
|
11697
|
+
const bound = key ? { ...key, boundAt: key.boundAt ?? new Date().toISOString() } : null;
|
|
11698
|
+
const event = this.event(record.id, 1, type, {
|
|
11699
|
+
record: encodeRecord(record, blobs),
|
|
11700
|
+
key: bound,
|
|
11701
|
+
...legacy ? { legacy } : {}
|
|
11702
|
+
});
|
|
11703
|
+
const file = this.eventsPath(record.id);
|
|
11704
|
+
const existing = readLog(file, 0, null);
|
|
11705
|
+
if (existing && existing.events.length > 0)
|
|
11706
|
+
throw new Error(`task ${record.id} already has a log`);
|
|
11707
|
+
appendEvent(file, event, 0);
|
|
11708
|
+
this.indexTask(record, bound, legacy !== null);
|
|
11709
|
+
}
|
|
11710
|
+
compactPending() {
|
|
11711
|
+
for (const taskId of this.compactDue) {
|
|
11712
|
+
this.compactDue.delete(taskId);
|
|
11713
|
+
this.compact(taskId);
|
|
11714
|
+
}
|
|
11715
|
+
}
|
|
11716
|
+
compact(taskId) {
|
|
11717
|
+
try {
|
|
11718
|
+
const file = this.eventsPath(taskId);
|
|
11719
|
+
const log = readLog(file, 0, null);
|
|
11720
|
+
if (!log || log.events.length <= COMPACT_KEEP + 1)
|
|
11721
|
+
return success(null, null, { folded: 0, bytes: log?.size ?? 0 });
|
|
11722
|
+
const blobs = this.blobStore();
|
|
11723
|
+
const cut = log.events.length - COMPACT_KEEP;
|
|
11724
|
+
let state = null;
|
|
11725
|
+
for (const event of log.events.slice(0, cut))
|
|
11726
|
+
state = reduce(state, event, blobs);
|
|
11727
|
+
const folded = log.events[cut - 1];
|
|
11728
|
+
const checkpoint = {
|
|
11729
|
+
...this.event(taskId, folded.seq, "task.checkpoint", {
|
|
11730
|
+
record: encodeRecord(state.record, blobs),
|
|
11731
|
+
key: state.key,
|
|
11732
|
+
...state.legacy ? { legacy: state.legacy } : {},
|
|
11733
|
+
folded: { from: log.events[0].seq, to: folded.seq }
|
|
11734
|
+
})
|
|
11735
|
+
};
|
|
11736
|
+
const prefix = `${[checkpoint, ...log.events.slice(cut)].map((event) => JSON.stringify(event)).join(`
|
|
11737
|
+
`)}
|
|
11738
|
+
`;
|
|
11739
|
+
const last = log.events.at(-1);
|
|
11740
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
11741
|
+
let latest = null;
|
|
11742
|
+
try {
|
|
11743
|
+
latest = readLog(file, log.lastStart, last.seq);
|
|
11744
|
+
} catch {}
|
|
11745
|
+
if (!latest || latest.events[0]?.id !== last.id)
|
|
11746
|
+
return failure2("busy", "the task log changed during compaction; retry", { taskId });
|
|
11747
|
+
const appended = fs8.readFileSync(file).subarray(log.end, latest.end);
|
|
11748
|
+
const bytes = Buffer.concat([Buffer.from(prefix, "utf8"), appended]);
|
|
11749
|
+
replaceFile(file, bytes.toString("utf8"), true);
|
|
11750
|
+
try {
|
|
11751
|
+
fs8.rmSync(this.snapshotPath(taskId), { force: true });
|
|
11752
|
+
} catch {}
|
|
11753
|
+
const reloaded = this.load(taskId);
|
|
11754
|
+
if (reloaded.ok && reloaded.data)
|
|
11755
|
+
this.writeSnapshot(taskId, reloaded.data);
|
|
11756
|
+
return success(null, null, { folded: cut, bytes: bytes.length });
|
|
10962
11757
|
});
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
try {
|
|
10966
|
-
lock = parseMetadataLockOrNull(fs6.readFileSync(this.lockPath, "utf8"));
|
|
10967
|
-
} catch {}
|
|
10968
|
-
if (lock?.externalAction)
|
|
10969
|
-
return failure2("writer_conflict", "workspace is reserved by a managed external action", {
|
|
10970
|
-
outcome: "not_started",
|
|
10971
|
-
path: this.lockPath
|
|
10972
|
-
});
|
|
10973
|
-
if (contention === "busy")
|
|
10974
|
-
return failure2("busy", `workspace metadata lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
|
|
10975
|
-
outcome: "not_started",
|
|
10976
|
-
path: this.lockPath,
|
|
10977
|
-
guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
|
|
10978
|
-
});
|
|
11758
|
+
} catch (error) {
|
|
11759
|
+
return failure2("storage_error", `compaction failed: ${String(error)}`, { taskId });
|
|
10979
11760
|
}
|
|
10980
|
-
const recovery = code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
|
|
10981
|
-
return failure2(recovery ? "recovery_required" : "storage_error", `metadata lock operation failed: ${String(error)}`, { path: this.lockPath });
|
|
10982
11761
|
}
|
|
10983
|
-
|
|
10984
|
-
retryTransient(() => fs6.mkdirSync(this.tasksDir, { recursive: true }));
|
|
10985
|
-
retryTransient(() => fs6.mkdirSync(this.recoveryDir, { recursive: true }));
|
|
10986
|
-
let current = null;
|
|
11762
|
+
countEvents(file) {
|
|
10987
11763
|
try {
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
|
|
11764
|
+
const bytes = fs8.readFileSync(file);
|
|
11765
|
+
let count = 0;
|
|
11766
|
+
for (let index = bytes.indexOf(10);index >= 0; index = bytes.indexOf(10, index + 1))
|
|
11767
|
+
count += 1;
|
|
11768
|
+
return count;
|
|
11769
|
+
} catch {
|
|
11770
|
+
return 0;
|
|
11771
|
+
}
|
|
11772
|
+
}
|
|
11773
|
+
openTasksForKey(key) {
|
|
11774
|
+
const all = this.listStoreIndex();
|
|
11775
|
+
if (!all.ok)
|
|
11776
|
+
return all;
|
|
11777
|
+
const location = this.location();
|
|
11778
|
+
const renames = location.ok ? branchRenames(location.data) : [];
|
|
11779
|
+
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));
|
|
11780
|
+
}
|
|
11781
|
+
openTaskForKey(key) {
|
|
11782
|
+
const bound = this.openTasksForKey(key);
|
|
11783
|
+
return bound.ok ? success(null, null, bound.data[0] ?? null) : bound;
|
|
11784
|
+
}
|
|
11785
|
+
withKeyLock(key, operation) {
|
|
11786
|
+
const digest = createHash6("sha256").update(key).digest("hex").slice(0, 32);
|
|
11787
|
+
return this.withLock(path9.join(this.paths().dir, "keys", `${digest}.lock`), operation);
|
|
11788
|
+
}
|
|
11789
|
+
bindLocked(taskId, key, timestamp) {
|
|
11790
|
+
const current = this.readWorkspace();
|
|
11791
|
+
if (!current.ok)
|
|
11792
|
+
return current;
|
|
11793
|
+
let workspace = current.data;
|
|
11794
|
+
if (!workspace) {
|
|
11795
|
+
workspace = this.nextWorkspace(null);
|
|
11796
|
+
const written = this.writeWorkspace(workspace);
|
|
11797
|
+
if (!written.ok)
|
|
11798
|
+
return written;
|
|
11799
|
+
}
|
|
11800
|
+
const target = workspace;
|
|
11801
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
11802
|
+
const loaded = this.load(taskId);
|
|
11803
|
+
if (!loaded.ok)
|
|
11804
|
+
return loaded;
|
|
11805
|
+
if (!loaded.data)
|
|
11806
|
+
return failure2("not_found", "task not found", { taskId });
|
|
11807
|
+
if (loaded.data.record.status === "closed")
|
|
11808
|
+
return failure2("invalid_transition", "a closed task cannot be adopted", { taskId });
|
|
11809
|
+
if (loaded.data.record.workspaceId === target.id && loaded.data.key?.key === key.key)
|
|
11810
|
+
return success(loaded.data.record.revision, null, loaded.data.record);
|
|
11811
|
+
const next = taskRecordSchema.safeParse({
|
|
11812
|
+
...loaded.data.record,
|
|
11813
|
+
workspaceId: target.id,
|
|
11814
|
+
revision: newRevision(),
|
|
11815
|
+
updatedAt: timestamp ?? now(),
|
|
11816
|
+
runtime: stampNew(loaded.data.record)
|
|
11817
|
+
});
|
|
11818
|
+
if (!next.success)
|
|
11819
|
+
return failure2("invalid_input", "rebound task is invalid");
|
|
11820
|
+
const written = this.appendPatch(loaded.data, next.data, { key });
|
|
11821
|
+
return written.ok ? success(next.data.revision, null, next.data) : written;
|
|
11822
|
+
});
|
|
11823
|
+
}
|
|
11824
|
+
unbindLocked(taskId) {
|
|
11825
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
11826
|
+
const loaded = this.load(taskId);
|
|
11827
|
+
if (!loaded.ok)
|
|
11828
|
+
return loaded;
|
|
11829
|
+
if (!loaded.data || loaded.data.key === null)
|
|
11830
|
+
return success(null, null, null);
|
|
11831
|
+
return this.appendPatch(loaded.data, loaded.data.record, { key: null });
|
|
11832
|
+
});
|
|
11833
|
+
}
|
|
11834
|
+
createLocked(value, bindCurrent, key, checkRevision = true) {
|
|
11835
|
+
const current = this.readWorkspace();
|
|
11836
|
+
if (!current.ok)
|
|
11837
|
+
return current;
|
|
11838
|
+
if (checkRevision && (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null))
|
|
11839
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
11840
|
+
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
11841
|
+
actualWorkspaceRevision: current.data?.revision ?? null
|
|
11842
|
+
});
|
|
11843
|
+
if (!value.provenance)
|
|
11844
|
+
return failure2("invalid_input", "provenance is required");
|
|
11845
|
+
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
11846
|
+
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
11847
|
+
if (bindCurrent) {
|
|
11848
|
+
const currentKey = this.currentKey();
|
|
11849
|
+
if (currentKey.ok)
|
|
11850
|
+
return this.withKeyLock(currentKey.data.key, () => {
|
|
11851
|
+
const holder = this.openTaskForKey(currentKey.data.key);
|
|
11852
|
+
if (!holder.ok)
|
|
11853
|
+
return holder;
|
|
11854
|
+
if (holder.data) {
|
|
11855
|
+
const released = this.unbindLocked(holder.data.id);
|
|
11856
|
+
if (!released.ok)
|
|
11857
|
+
return released;
|
|
11858
|
+
}
|
|
11859
|
+
return this.newTask(value, current.data, currentKey.data);
|
|
11860
|
+
});
|
|
11861
|
+
}
|
|
11862
|
+
return this.newTask(value, current.data, key);
|
|
11863
|
+
}
|
|
11864
|
+
newTask(value, previous, binding) {
|
|
11865
|
+
const current = { data: previous };
|
|
11866
|
+
const workspace = this.nextWorkspace(current.data);
|
|
11867
|
+
const timestamp = value.now ?? now();
|
|
11868
|
+
const task = {
|
|
11869
|
+
schemaVersion: SCHEMA_VERSION,
|
|
11870
|
+
id: newId(),
|
|
11871
|
+
workspaceId: workspace.id,
|
|
11872
|
+
revision: newRevision(),
|
|
11873
|
+
createdAt: timestamp,
|
|
11874
|
+
updatedAt: timestamp,
|
|
11875
|
+
origin: null,
|
|
11876
|
+
intent: {
|
|
11877
|
+
id: newId(),
|
|
11878
|
+
recordedAt: timestamp,
|
|
11879
|
+
provenance: value.provenance,
|
|
11880
|
+
data: value.intent
|
|
11881
|
+
},
|
|
11882
|
+
constraints: [],
|
|
11883
|
+
status: "active",
|
|
11884
|
+
closure: null,
|
|
11885
|
+
progress: { summary: "", nextAction: null, blockers: [] },
|
|
11886
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
11887
|
+
assessments: [],
|
|
11888
|
+
policy: null,
|
|
11889
|
+
policyChanges: [],
|
|
11890
|
+
candidates: [],
|
|
11891
|
+
evidence: [],
|
|
11892
|
+
decisions: [],
|
|
11893
|
+
findings: [],
|
|
11894
|
+
workers: []
|
|
11895
|
+
};
|
|
11896
|
+
return this.writeNewTask(current.data, workspace, task, "task.opened", binding, {
|
|
11897
|
+
operation: "create"
|
|
11898
|
+
});
|
|
10994
11899
|
}
|
|
10995
|
-
|
|
10996
|
-
|
|
11900
|
+
nextWorkspace(current, id) {
|
|
11901
|
+
return current ? { ...current, revision: newRevision(), root: this.root, runtime: stampNew(current) } : {
|
|
11902
|
+
schemaVersion: SCHEMA_VERSION,
|
|
11903
|
+
id: id ?? newId(),
|
|
11904
|
+
revision: newRevision(),
|
|
11905
|
+
root: this.root,
|
|
11906
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
11907
|
+
writer: null
|
|
11908
|
+
};
|
|
11909
|
+
}
|
|
11910
|
+
writeNewTask(previous, workspace, task, type, key, details) {
|
|
11911
|
+
const written = this.writeWorkspace(workspace);
|
|
11912
|
+
if (!written.ok)
|
|
11913
|
+
return written;
|
|
10997
11914
|
try {
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
this.
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
11005
|
-
const fd = fs6.openSync(temporary, "wx", 384);
|
|
11006
|
-
try {
|
|
11007
|
-
fs6.writeSync(fd, jsonBytes(value));
|
|
11008
|
-
fs6.fsyncSync(fd);
|
|
11009
|
-
} finally {
|
|
11010
|
-
fs6.closeSync(fd);
|
|
11915
|
+
this.openTask(task, type, key);
|
|
11916
|
+
} catch (error) {
|
|
11917
|
+
if (previous) {
|
|
11918
|
+
const restored = this.writeWorkspace(previous);
|
|
11919
|
+
if (!restored.ok)
|
|
11920
|
+
return failure2("external_outcome_unknown", `task ${details.operation} failed and workspace restoration is uncertain`, { ...details, outcome: "unknown" });
|
|
11011
11921
|
}
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11922
|
+
return failure2("external_outcome_unknown", `workspace written but the task log is uncertain: ${String(error)}`, { ...details, outcome: "unknown" });
|
|
11923
|
+
}
|
|
11924
|
+
return success(task.revision, workspace.revision, task);
|
|
11925
|
+
}
|
|
11926
|
+
writeWorkspace(record) {
|
|
11927
|
+
const file = this.paths().workspace;
|
|
11928
|
+
try {
|
|
11929
|
+
fs8.mkdirSync(path9.dirname(file), { recursive: true });
|
|
11930
|
+
replaceFile(file, `${canonicalJson(record)}
|
|
11931
|
+
`, true);
|
|
11932
|
+
return success(null, null, null);
|
|
11018
11933
|
} catch (error) {
|
|
11019
|
-
return failure2("storage_error", `
|
|
11020
|
-
path: file
|
|
11021
|
-
});
|
|
11022
|
-
} finally {
|
|
11023
|
-
if (temporary)
|
|
11024
|
-
try {
|
|
11025
|
-
fs6.unlinkSync(temporary);
|
|
11026
|
-
} catch {}
|
|
11934
|
+
return failure2("storage_error", `workspace write failed: ${String(error)}`, { path: file });
|
|
11027
11935
|
}
|
|
11028
11936
|
}
|
|
11029
|
-
|
|
11937
|
+
markUncertain(workspace) {
|
|
11938
|
+
if (!workspace.writer || workspace.writer.state === "uncertain")
|
|
11939
|
+
return;
|
|
11940
|
+
this.writeWorkspace({
|
|
11941
|
+
...workspace,
|
|
11942
|
+
writer: { ...workspace.writer, state: "uncertain" },
|
|
11943
|
+
revision: newRevision(),
|
|
11944
|
+
runtime: stampNew(workspace)
|
|
11945
|
+
});
|
|
11946
|
+
}
|
|
11947
|
+
readIndex() {
|
|
11030
11948
|
try {
|
|
11031
|
-
const value = JSON.parse(
|
|
11032
|
-
if (!
|
|
11949
|
+
const value = JSON.parse(fs8.readFileSync(this.paths().index, "utf8"));
|
|
11950
|
+
if (!isObject4(value) || value.version !== INDEX_VERSION || !isObject4(value.tasks))
|
|
11033
11951
|
return {};
|
|
11034
11952
|
const tasks = {};
|
|
11035
11953
|
for (const [id, entry] of Object.entries(value.tasks))
|
|
11036
|
-
if (
|
|
11954
|
+
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))
|
|
11037
11955
|
tasks[id] = entry;
|
|
11038
11956
|
return tasks;
|
|
11039
11957
|
} catch {
|
|
11040
11958
|
return {};
|
|
11041
11959
|
}
|
|
11042
11960
|
}
|
|
11043
|
-
writeIndex(
|
|
11044
|
-
const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
11961
|
+
writeIndex(entries) {
|
|
11045
11962
|
try {
|
|
11046
|
-
|
|
11963
|
+
fs8.mkdirSync(this.paths().tasks, { recursive: true });
|
|
11964
|
+
replaceFile(this.paths().index, JSON.stringify({
|
|
11047
11965
|
version: INDEX_VERSION,
|
|
11048
|
-
workspaceId,
|
|
11049
11966
|
tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
|
|
11050
|
-
}),
|
|
11051
|
-
|
|
11967
|
+
}), false);
|
|
11968
|
+
} catch {}
|
|
11969
|
+
}
|
|
11970
|
+
indexTask(task, key, legacy) {
|
|
11971
|
+
try {
|
|
11972
|
+
const signature = fileSignature(this.eventsPath(task.id));
|
|
11973
|
+
if (signature === null)
|
|
11974
|
+
return;
|
|
11975
|
+
const tasks = this.readIndex();
|
|
11976
|
+
tasks[task.id] = indexEntry(task, key, legacy, signature);
|
|
11977
|
+
this.writeIndex(Object.values(tasks));
|
|
11978
|
+
} catch {}
|
|
11979
|
+
}
|
|
11980
|
+
parseTask(record, taskId) {
|
|
11981
|
+
const parsed = this.parseValue(record, taskRecordSchema, "task");
|
|
11982
|
+
if (!parsed.ok)
|
|
11983
|
+
return { ...parsed, details: { ...parsed.details, taskId } };
|
|
11984
|
+
if (parsed.data.id !== taskId)
|
|
11985
|
+
return failure2("recovery_required", "task log and record ID differ", { taskId });
|
|
11986
|
+
return parsed;
|
|
11987
|
+
}
|
|
11988
|
+
parseRecord(bytes, schema, label) {
|
|
11989
|
+
let value;
|
|
11990
|
+
try {
|
|
11991
|
+
value = JSON.parse(bytes);
|
|
11052
11992
|
} catch {
|
|
11993
|
+
return failure2("recovery_required", `${label} record JSON is corrupt`);
|
|
11994
|
+
}
|
|
11995
|
+
return this.parseValue(value, schema, label);
|
|
11996
|
+
}
|
|
11997
|
+
parseValue(value, schema, label) {
|
|
11998
|
+
if (isObject4(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
11999
|
+
return failure2("unsupported_version", `unsupported ${label} schema version`);
|
|
12000
|
+
const parsed = parseStoredRecord(schema, value);
|
|
12001
|
+
if (parsed.success)
|
|
12002
|
+
return success(null, null, parsed.data);
|
|
12003
|
+
const writerVersion = isObject4(value) && isObject4(value.runtime) ? value.runtime.updatedWith : null;
|
|
12004
|
+
if (parsed.critical.length > 0)
|
|
12005
|
+
return failure2("recovery_required", `${label} record requires fields this Workit cannot read (${parsed.critical.join(", ")}); upgrade Workit before mutating this checkout`);
|
|
12006
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
12007
|
+
return failure2("recovery_required", `${label} record was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
12008
|
+
return failure2("recovery_required", `${label} record does not satisfy its schema`);
|
|
12009
|
+
}
|
|
12010
|
+
conflict(expected, actual) {
|
|
12011
|
+
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", { expectedRevision: expected, actualRevision: actual });
|
|
12012
|
+
}
|
|
12013
|
+
workspaceConflict(expected, actual) {
|
|
12014
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
12015
|
+
}
|
|
12016
|
+
v2Dirs() {
|
|
12017
|
+
return [...new Set([this.root, this.origin])].map((dir) => path9.join(dir, ".workit"));
|
|
12018
|
+
}
|
|
12019
|
+
pendingMigrations() {
|
|
12020
|
+
return this.v2Dirs().filter((dir) => {
|
|
12021
|
+
const raw = this.readV2Workspace(path9.join(dir, "workspace.json"));
|
|
12022
|
+
return raw !== null && raw !== TOMBSTONE;
|
|
12023
|
+
});
|
|
12024
|
+
}
|
|
12025
|
+
migrateV2(v2) {
|
|
12026
|
+
const dirs = this.paths();
|
|
12027
|
+
const v2Workspace = path9.join(v2, "workspace.json");
|
|
12028
|
+
const v2Tasks = path9.join(v2, "tasks");
|
|
12029
|
+
const legacy = path9.join(dirs.dir, "legacy", checkoutSlug(path9.dirname(v2)), "v2");
|
|
12030
|
+
const raw = this.readV2Workspace(v2Workspace);
|
|
12031
|
+
if (raw === null || raw === TOMBSTONE)
|
|
12032
|
+
return success(null, null, null);
|
|
12033
|
+
return this.withLock(dirs.checkoutLock, () => this.withLock(path9.join(v2, "metadata.lock"), () => {
|
|
12034
|
+
const workspace = this.readV2Workspace(v2Workspace);
|
|
12035
|
+
if (workspace === null || workspace === TOMBSTONE)
|
|
12036
|
+
return success(null, null, null);
|
|
12037
|
+
const parsedWorkspace = this.parseRecord(workspace.bytes, workspaceRecordSchema, "2.x workspace");
|
|
12038
|
+
if (!parsedWorkspace.ok)
|
|
12039
|
+
return failure2("recovery_required", `cannot migrate the 2.x store at ${v2}: ${parsedWorkspace.error}; move it aside to start fresh`, { path: v2Workspace });
|
|
12040
|
+
let files = [];
|
|
11053
12041
|
try {
|
|
11054
|
-
|
|
12042
|
+
files = fs8.readdirSync(v2Tasks).filter((name) => name.endsWith(".json") && validId(name.slice(0, -5))).filter((name) => !this.isStub(path9.join(v2Tasks, name)));
|
|
11055
12043
|
} catch {}
|
|
11056
|
-
|
|
12044
|
+
const current = this.readWorkspace();
|
|
12045
|
+
if (!current.ok)
|
|
12046
|
+
return current;
|
|
12047
|
+
let workspaceMoved = false;
|
|
12048
|
+
let target = current.data;
|
|
12049
|
+
if (!target) {
|
|
12050
|
+
target = { ...parsedWorkspace.data, root: this.root };
|
|
12051
|
+
const written = this.writeWorkspace(target);
|
|
12052
|
+
if (!written.ok)
|
|
12053
|
+
return written;
|
|
12054
|
+
workspaceMoved = true;
|
|
12055
|
+
}
|
|
12056
|
+
let migrated = 0;
|
|
12057
|
+
let skipped = 0;
|
|
12058
|
+
const done = new Map;
|
|
12059
|
+
try {
|
|
12060
|
+
fs8.mkdirSync(path9.join(legacy, "tasks"), { recursive: true });
|
|
12061
|
+
for (const name of ["workspace.json", "index.json"])
|
|
12062
|
+
if (fs8.existsSync(path9.join(v2, name)))
|
|
12063
|
+
fs8.copyFileSync(path9.join(v2, name), path9.join(legacy, name));
|
|
12064
|
+
for (const name of files) {
|
|
12065
|
+
const id = name.slice(0, -5);
|
|
12066
|
+
const file = path9.join(v2Tasks, name);
|
|
12067
|
+
const bytes = fs8.readFileSync(file, "utf8");
|
|
12068
|
+
const digest = sha256(bytes);
|
|
12069
|
+
fs8.writeFileSync(path9.join(legacy, "tasks", name), bytes);
|
|
12070
|
+
const parsed = this.parseRecord(bytes, taskRecordSchema, "2.x task");
|
|
12071
|
+
if (!parsed.ok || parsed.data.id !== id)
|
|
12072
|
+
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 });
|
|
12073
|
+
const record = { ...parsed.data, workspaceId: target.id };
|
|
12074
|
+
const origin = { path: path9.join(legacy, "tasks", name), digest };
|
|
12075
|
+
if (!fs8.existsSync(this.eventsPath(id))) {
|
|
12076
|
+
this.openTask(record, "migrated.from_v2", null, origin);
|
|
12077
|
+
migrated += 1;
|
|
12078
|
+
} else {
|
|
12079
|
+
const resynced = this.withLock(this.taskLockPath(id), () => {
|
|
12080
|
+
const loaded = this.load(id);
|
|
12081
|
+
if (!loaded.ok)
|
|
12082
|
+
return loaded;
|
|
12083
|
+
if (!loaded.data)
|
|
12084
|
+
return failure2("not_found", "task not found", { taskId: id });
|
|
12085
|
+
if (loaded.data.legacy?.digest === digest)
|
|
12086
|
+
return success(null, null, false);
|
|
12087
|
+
const appended = this.appendPatch(loaded.data, record, {
|
|
12088
|
+
type: "migrated.from_v2",
|
|
12089
|
+
legacy: origin
|
|
12090
|
+
});
|
|
12091
|
+
return appended.ok ? success(null, null, true) : appended;
|
|
12092
|
+
});
|
|
12093
|
+
if (!resynced.ok)
|
|
12094
|
+
return resynced;
|
|
12095
|
+
if (resynced.data)
|
|
12096
|
+
migrated += 1;
|
|
12097
|
+
else
|
|
12098
|
+
skipped += 1;
|
|
12099
|
+
}
|
|
12100
|
+
done.set(name, digest);
|
|
12101
|
+
}
|
|
12102
|
+
} catch (error) {
|
|
12103
|
+
return failure2("storage_error", `cannot migrate the 2.x store at ${v2}: ${String(error)}`, {
|
|
12104
|
+
path: v2
|
|
12105
|
+
});
|
|
12106
|
+
}
|
|
12107
|
+
try {
|
|
12108
|
+
let complete = true;
|
|
12109
|
+
for (const [name, digest] of done) {
|
|
12110
|
+
const file = path9.join(v2Tasks, name);
|
|
12111
|
+
if (sha256(fs8.readFileSync(file, "utf8")) !== digest) {
|
|
12112
|
+
complete = false;
|
|
12113
|
+
continue;
|
|
12114
|
+
}
|
|
12115
|
+
replaceFile(file, this.stubFor(name.slice(0, -5)), false);
|
|
12116
|
+
}
|
|
12117
|
+
if (!complete)
|
|
12118
|
+
return failure2("busy", `the 2.x store at ${v2} changed during migration; retry`, {
|
|
12119
|
+
path: v2
|
|
12120
|
+
});
|
|
12121
|
+
fs8.rmSync(path9.join(v2, "index.json"), { force: true });
|
|
12122
|
+
this.writeTombstone(v2, target.id);
|
|
12123
|
+
} catch (error) {
|
|
12124
|
+
return failure2("storage_error", `cannot finish the 2.x migration: ${String(error)}`, {
|
|
12125
|
+
path: v2
|
|
12126
|
+
});
|
|
12127
|
+
}
|
|
12128
|
+
reportMigration({
|
|
12129
|
+
from: v2,
|
|
12130
|
+
to: dirs.dir,
|
|
12131
|
+
backup: legacy,
|
|
12132
|
+
tasks: migrated,
|
|
12133
|
+
skipped,
|
|
12134
|
+
workspace: workspaceMoved
|
|
12135
|
+
});
|
|
12136
|
+
return success(null, null, null);
|
|
12137
|
+
}));
|
|
11057
12138
|
}
|
|
11058
|
-
|
|
12139
|
+
adoptLocalStore() {
|
|
12140
|
+
const dirs = this.paths();
|
|
12141
|
+
const marker = path9.join(dirs.v2, "store.json");
|
|
12142
|
+
if (!dirs.shared || !fs8.existsSync(marker))
|
|
12143
|
+
return success(null, null, null);
|
|
12144
|
+
return this.withLock(dirs.checkoutLock, () => {
|
|
12145
|
+
if (!fs8.existsSync(marker))
|
|
12146
|
+
return success(null, null, null);
|
|
12147
|
+
let tasks = 0;
|
|
12148
|
+
try {
|
|
12149
|
+
const merge = (from, to) => {
|
|
12150
|
+
if (!fs8.existsSync(to)) {
|
|
12151
|
+
fs8.mkdirSync(path9.dirname(to), { recursive: true });
|
|
12152
|
+
try {
|
|
12153
|
+
fs8.renameSync(from, to);
|
|
12154
|
+
} catch {
|
|
12155
|
+
fs8.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
|
|
12156
|
+
fs8.rmSync(from, { recursive: true, force: true });
|
|
12157
|
+
}
|
|
12158
|
+
return;
|
|
12159
|
+
}
|
|
12160
|
+
if (fs8.lstatSync(from).isDirectory() && fs8.lstatSync(to).isDirectory())
|
|
12161
|
+
for (const name of fs8.readdirSync(from))
|
|
12162
|
+
merge(path9.join(from, name), path9.join(to, name));
|
|
12163
|
+
};
|
|
12164
|
+
for (const sub of ["tasks", "checkouts", "blobs", "legacy"]) {
|
|
12165
|
+
const from = path9.join(dirs.v2, sub);
|
|
12166
|
+
if (!fs8.existsSync(from))
|
|
12167
|
+
continue;
|
|
12168
|
+
if (sub === "tasks") {
|
|
12169
|
+
for (const name of fs8.readdirSync(from))
|
|
12170
|
+
if (validId(name) && !fs8.existsSync(path9.join(dirs.dir, sub, name)))
|
|
12171
|
+
tasks += 1;
|
|
12172
|
+
}
|
|
12173
|
+
merge(from, path9.join(dirs.dir, sub));
|
|
12174
|
+
}
|
|
12175
|
+
const ledger = path9.join(dirs.v2, "ledger");
|
|
12176
|
+
if (fs8.existsSync(ledger) && !fs8.existsSync(path9.join(dirs.dir, "ledger")))
|
|
12177
|
+
merge(ledger, path9.join(dirs.dir, "ledger"));
|
|
12178
|
+
fs8.rmSync(path9.join(dirs.v2, "task-index.json"), { force: true });
|
|
12179
|
+
fs8.renameSync(marker, path9.join(dirs.v2, "store.moved.json"));
|
|
12180
|
+
} catch (error) {
|
|
12181
|
+
return failure2("storage_error", `cannot move ${dirs.v2} into ${dirs.dir}: ${String(error)}`, {
|
|
12182
|
+
path: dirs.v2
|
|
12183
|
+
});
|
|
12184
|
+
}
|
|
12185
|
+
reportMigration({
|
|
12186
|
+
from: dirs.v2,
|
|
12187
|
+
to: dirs.dir,
|
|
12188
|
+
backup: dirs.v2,
|
|
12189
|
+
tasks,
|
|
12190
|
+
skipped: 0,
|
|
12191
|
+
workspace: false
|
|
12192
|
+
});
|
|
12193
|
+
return success(null, null, null);
|
|
12194
|
+
});
|
|
12195
|
+
}
|
|
12196
|
+
readV2Workspace(file) {
|
|
12197
|
+
let bytes;
|
|
11059
12198
|
try {
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
12199
|
+
bytes = fs8.readFileSync(file, "utf8");
|
|
12200
|
+
} catch {
|
|
12201
|
+
return null;
|
|
12202
|
+
}
|
|
12203
|
+
try {
|
|
12204
|
+
const value = JSON.parse(bytes);
|
|
12205
|
+
if (isObject4(value) && isObject4(value.store) && value.store.format === "workit-store")
|
|
12206
|
+
return TOMBSTONE;
|
|
11066
12207
|
} catch {}
|
|
12208
|
+
return { bytes };
|
|
12209
|
+
}
|
|
12210
|
+
writeTombstone(v2, workspaceId) {
|
|
12211
|
+
const dirs = this.paths();
|
|
12212
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
12213
|
+
const marker = {
|
|
12214
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12215
|
+
id: workspaceId ?? newId(),
|
|
12216
|
+
revision: newRevision(),
|
|
12217
|
+
root: path9.dirname(v2),
|
|
12218
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
12219
|
+
writer: null,
|
|
12220
|
+
store: {
|
|
12221
|
+
format: "workit-store",
|
|
12222
|
+
version: STORE_FORMAT,
|
|
12223
|
+
path: dirs.dir,
|
|
12224
|
+
note: `workit ${STORE_MIN_RUNTIME}+ keeps task state in ${dirs.dir}; upgrade Workit`
|
|
12225
|
+
},
|
|
12226
|
+
critical: ["store"]
|
|
12227
|
+
};
|
|
12228
|
+
fs8.mkdirSync(v2, { recursive: true });
|
|
12229
|
+
const ignore = path9.join(v2, ".gitignore");
|
|
12230
|
+
if (!fs8.existsSync(ignore))
|
|
12231
|
+
fs8.writeFileSync(ignore, `*
|
|
12232
|
+
`);
|
|
12233
|
+
replaceFile(path9.join(v2, "workspace.json"), `${JSON.stringify(marker, null, 2)}
|
|
12234
|
+
`, true);
|
|
12235
|
+
}
|
|
12236
|
+
stubFor(taskId) {
|
|
12237
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
12238
|
+
return `${JSON.stringify({
|
|
12239
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12240
|
+
id: taskId,
|
|
12241
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
12242
|
+
store: { format: "workit-store", version: STORE_FORMAT, path: this.paths().dir },
|
|
12243
|
+
critical: ["store"]
|
|
12244
|
+
})}
|
|
12245
|
+
`;
|
|
11067
12246
|
}
|
|
11068
|
-
|
|
11069
|
-
const target = path7.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
11070
|
-
const id = target === "task" ? path7.basename(file, ".json") : "workspace";
|
|
11071
|
-
const destination = path7.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
11072
|
-
if (fs6.existsSync(destination)) {
|
|
11073
|
-
if (digestBytes(retryTransient(() => fs6.readFileSync(destination))) !== digestBytes(bytes))
|
|
11074
|
-
throw new Error("recovery copy already exists with different bytes");
|
|
11075
|
-
const stamp = new Date;
|
|
11076
|
-
retryTransient(() => fs6.utimesSync(destination, stamp, stamp));
|
|
11077
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
11078
|
-
return;
|
|
11079
|
-
}
|
|
11080
|
-
let temporary;
|
|
12247
|
+
isStub(file) {
|
|
11081
12248
|
try {
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
fs6.fsyncSync(fd);
|
|
11087
|
-
} finally {
|
|
11088
|
-
fs6.closeSync(fd);
|
|
11089
|
-
}
|
|
11090
|
-
retryTransient(() => fs6.renameSync(temporary, destination));
|
|
11091
|
-
temporary = undefined;
|
|
11092
|
-
this.fsyncDirectory(this.recoveryDir);
|
|
11093
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
11094
|
-
} finally {
|
|
11095
|
-
if (temporary)
|
|
11096
|
-
try {
|
|
11097
|
-
fs6.unlinkSync(temporary);
|
|
11098
|
-
} catch {}
|
|
12249
|
+
const value = JSON.parse(fs8.readFileSync(file, "utf8"));
|
|
12250
|
+
return isObject4(value) && isObject4(value.store) && value.store.format === "workit-store";
|
|
12251
|
+
} catch {
|
|
12252
|
+
return false;
|
|
11099
12253
|
}
|
|
11100
12254
|
}
|
|
11101
|
-
|
|
12255
|
+
writeTombstoneIfMissing() {
|
|
11102
12256
|
try {
|
|
11103
|
-
const
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
12257
|
+
const v2 = path9.join(this.root, ".workit");
|
|
12258
|
+
if (fs8.existsSync(path9.join(v2, "workspace.json")))
|
|
12259
|
+
return;
|
|
12260
|
+
let id = null;
|
|
12261
|
+
try {
|
|
12262
|
+
id = JSON.parse(fs8.readFileSync(this.paths().workspace, "utf8")).id ?? null;
|
|
12263
|
+
} catch {}
|
|
12264
|
+
this.writeTombstone(v2, id);
|
|
11107
12265
|
} catch {}
|
|
11108
12266
|
}
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
const file = path7.join(this.recoveryDir, name);
|
|
12267
|
+
initializeStorage() {
|
|
12268
|
+
const dirs = this.paths();
|
|
12269
|
+
retryTransient(() => fs8.mkdirSync(dirs.checkout, { recursive: true }));
|
|
12270
|
+
retryTransient(() => fs8.mkdirSync(dirs.tasks, { recursive: true }));
|
|
12271
|
+
if (!fs8.existsSync(dirs.marker))
|
|
12272
|
+
replaceFile(dirs.marker, `${JSON.stringify({ format: "workit-store", version: STORE_FORMAT, createdWith: runtimeVersion() })}
|
|
12273
|
+
`, false);
|
|
12274
|
+
if (!dirs.shared) {
|
|
12275
|
+
const ignore = path9.join(dirs.dir, ".gitignore");
|
|
12276
|
+
let current = null;
|
|
11120
12277
|
try {
|
|
11121
|
-
|
|
11122
|
-
if (!stat.isFile())
|
|
11123
|
-
continue;
|
|
11124
|
-
copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
|
|
12278
|
+
current = fs8.readFileSync(ignore, "utf8");
|
|
11125
12279
|
} catch {}
|
|
12280
|
+
if (current !== `*
|
|
12281
|
+
`)
|
|
12282
|
+
retryTransient(() => fs8.writeFileSync(ignore, `*
|
|
12283
|
+
`));
|
|
11126
12284
|
}
|
|
11127
|
-
|
|
11128
|
-
}
|
|
11129
|
-
surplusCopies(copies, keep) {
|
|
11130
|
-
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);
|
|
11131
|
-
return ordered.slice(RECOVERY_COPIES_PER_RECORD);
|
|
12285
|
+
this.writeTombstoneIfMissing();
|
|
11132
12286
|
}
|
|
11133
|
-
|
|
11134
|
-
const
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
12287
|
+
lockOptions(lockPath) {
|
|
12288
|
+
const inProcess = heldInProcess.has(lockPath);
|
|
12289
|
+
return {
|
|
12290
|
+
lockPath,
|
|
12291
|
+
staleMs: Number.MAX_SAFE_INTEGER,
|
|
12292
|
+
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
12293
|
+
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
12294
|
+
staleRecovery: "remove-if-unchanged",
|
|
12295
|
+
shouldReclaim: ({ payload, nowMs }) => {
|
|
12296
|
+
let ageMs = null;
|
|
12297
|
+
try {
|
|
12298
|
+
ageMs = nowMs - fs8.lstatSync(lockPath).mtimeMs;
|
|
12299
|
+
} catch {}
|
|
12300
|
+
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
12301
|
+
},
|
|
12302
|
+
shouldRemoveStaleLock: () => true,
|
|
12303
|
+
parsePayload: parseMetadataLockOrNull,
|
|
12304
|
+
payload: () => ({
|
|
12305
|
+
pid: process.pid,
|
|
12306
|
+
processStart: processStartOf(process.pid),
|
|
12307
|
+
host: localLockHost(),
|
|
12308
|
+
nonce: randomUUID3()
|
|
12309
|
+
})
|
|
11140
12310
|
};
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
12311
|
+
}
|
|
12312
|
+
acquire(lockPath, options) {
|
|
12313
|
+
clearAbandonedReclaimGuard(lockPath);
|
|
12314
|
+
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
12315
|
+
while (true) {
|
|
11144
12316
|
try {
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
for (const copies of groups.values()) {
|
|
11149
|
-
const surplus = this.surplusCopies(copies);
|
|
11150
|
-
report.recovery.kept += copies.length - surplus.length;
|
|
11151
|
-
for (const copy of surplus) {
|
|
11152
|
-
report.recovery.removed += 1;
|
|
11153
|
-
try {
|
|
11154
|
-
report.recovery.removedBytes += fs6.statSync(copy.path).size;
|
|
11155
|
-
} catch {}
|
|
11156
|
-
if (!dryRun)
|
|
11157
|
-
fs6.rmSync(copy.path, { force: true });
|
|
11158
|
-
}
|
|
11159
|
-
}
|
|
11160
|
-
const nowMs = Date.now();
|
|
11161
|
-
for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
|
|
11162
|
-
if (!fs6.existsSync(directory))
|
|
11163
|
-
continue;
|
|
11164
|
-
for (const name of fs6.readdirSync(directory)) {
|
|
11165
|
-
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
11166
|
-
continue;
|
|
11167
|
-
const file = path7.join(directory, name);
|
|
11168
|
-
const stat = fs6.lstatSync(file);
|
|
11169
|
-
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
11170
|
-
continue;
|
|
11171
|
-
report.temporary.removed += 1;
|
|
11172
|
-
if (!dryRun)
|
|
11173
|
-
fs6.rmSync(file, { force: true });
|
|
11174
|
-
}
|
|
11175
|
-
}
|
|
11176
|
-
return success(null, null, null);
|
|
11177
|
-
} catch (error) {
|
|
11178
|
-
return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
|
|
11179
|
-
path: this.recoveryDir
|
|
12317
|
+
return acquireFileLockSync(lockPath, {
|
|
12318
|
+
...options,
|
|
12319
|
+
timeoutMs: Math.max(0, deadline - Date.now())
|
|
11180
12320
|
});
|
|
12321
|
+
} catch (error) {
|
|
12322
|
+
const code = error?.code;
|
|
12323
|
+
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
12324
|
+
throw error;
|
|
12325
|
+
if (code !== "file_lock_stale" && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
12326
|
+
throw error;
|
|
12327
|
+
if (code !== "file_lock_stale")
|
|
12328
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
11181
12329
|
}
|
|
11182
|
-
};
|
|
11183
|
-
const pruned = dryRun ? sweep() : this.withLock(sweep);
|
|
11184
|
-
if (!pruned.ok)
|
|
11185
|
-
return pruned;
|
|
11186
|
-
const tasks = this.listTasks();
|
|
11187
|
-
if (!tasks.ok)
|
|
11188
|
-
return tasks;
|
|
11189
|
-
for (const task of tasks.data) {
|
|
11190
|
-
const deduped = dedupeCandidates(task.candidates);
|
|
11191
|
-
const removed = task.candidates.length - deduped.length;
|
|
11192
|
-
if (removed === 0)
|
|
11193
|
-
continue;
|
|
11194
|
-
if (task.status === "active") {
|
|
11195
|
-
report.candidates.skippedActive.push(task.id);
|
|
11196
|
-
continue;
|
|
11197
|
-
}
|
|
11198
|
-
if (task.status === "closed") {
|
|
11199
|
-
report.candidates.skippedClosed.push(task.id);
|
|
11200
|
-
continue;
|
|
11201
|
-
}
|
|
11202
|
-
if (!dryRun) {
|
|
11203
|
-
const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
|
|
11204
|
-
...current,
|
|
11205
|
-
candidates: dedupeCandidates(current.candidates)
|
|
11206
|
-
}));
|
|
11207
|
-
if (!written.ok) {
|
|
11208
|
-
report.candidates.failed.push(task.id);
|
|
11209
|
-
continue;
|
|
11210
|
-
}
|
|
11211
|
-
}
|
|
11212
|
-
report.candidates.removed += removed;
|
|
11213
|
-
report.candidates.tasks.push(task.id);
|
|
11214
12330
|
}
|
|
11215
|
-
return success(null, null, report);
|
|
11216
12331
|
}
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
return
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
return null;
|
|
11226
|
-
for (const candidate of candidates.data) {
|
|
11227
|
-
if (candidate.target !== target || candidate.digest !== digest)
|
|
11228
|
-
continue;
|
|
11229
|
-
try {
|
|
11230
|
-
if (!fs6.lstatSync(candidate.path).isFile())
|
|
11231
|
-
continue;
|
|
11232
|
-
const bytes = fs6.readFileSync(candidate.path);
|
|
11233
|
-
if (digestBytes(bytes) !== digest)
|
|
11234
|
-
continue;
|
|
11235
|
-
if (target === "workspace") {
|
|
11236
|
-
const parsed = this.parseBytes(bytes, workspaceRecordSchema);
|
|
11237
|
-
if (parsed.ok && parsed.data.id === workspaceId && parsed.data.root === this.root)
|
|
11238
|
-
return bytes;
|
|
11239
|
-
} else {
|
|
11240
|
-
const parsed = this.parseBytes(bytes, taskRecordSchema);
|
|
11241
|
-
if (parsed.ok && parsed.data.id === taskId && parsed.data.workspaceId === workspace?.id)
|
|
11242
|
-
return bytes;
|
|
11243
|
-
}
|
|
11244
|
-
} catch {}
|
|
12332
|
+
withLock(lockPath, operation) {
|
|
12333
|
+
try {
|
|
12334
|
+
this.initializeStorage();
|
|
12335
|
+
fs8.mkdirSync(path9.dirname(lockPath), { recursive: true });
|
|
12336
|
+
} catch (error) {
|
|
12337
|
+
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
12338
|
+
path: this.paths().dir
|
|
12339
|
+
});
|
|
11245
12340
|
}
|
|
11246
|
-
|
|
11247
|
-
}
|
|
11248
|
-
fsyncDirectory(directory) {
|
|
12341
|
+
let handle;
|
|
11249
12342
|
try {
|
|
11250
|
-
|
|
11251
|
-
try {
|
|
11252
|
-
fs6.fsyncSync(fd);
|
|
11253
|
-
} finally {
|
|
11254
|
-
fs6.closeSync(fd);
|
|
11255
|
-
}
|
|
12343
|
+
handle = this.acquire(lockPath, this.lockOptions(lockPath));
|
|
11256
12344
|
} catch (error) {
|
|
11257
|
-
|
|
11258
|
-
throw error;
|
|
12345
|
+
return this.lockFailure(lockPath, error);
|
|
11259
12346
|
}
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
if (!fs6.existsSync(this.tasksDir))
|
|
11263
|
-
return null;
|
|
11264
|
-
const ids = new Set;
|
|
12347
|
+
hold(lockPath);
|
|
12348
|
+
let result;
|
|
11265
12349
|
try {
|
|
11266
|
-
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
12350
|
+
if (!handle.verifyStillHeld())
|
|
12351
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
12352
|
+
else {
|
|
12353
|
+
try {
|
|
12354
|
+
result = operation();
|
|
12355
|
+
} catch (error) {
|
|
12356
|
+
result = failure2("storage_error", `mutation failed: ${String(error)}`);
|
|
12357
|
+
}
|
|
11273
12358
|
}
|
|
11274
|
-
} catch {
|
|
11275
|
-
|
|
12359
|
+
} catch (error) {
|
|
12360
|
+
result = this.lockFailure(lockPath, error);
|
|
12361
|
+
}
|
|
12362
|
+
drop(lockPath);
|
|
12363
|
+
try {
|
|
12364
|
+
retryTransient(() => handle.release());
|
|
12365
|
+
} catch (error) {
|
|
12366
|
+
const released = this.lockFailure(lockPath, error);
|
|
12367
|
+
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
12368
|
+
result = result.ok ? released : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
11276
12369
|
}
|
|
11277
|
-
return
|
|
12370
|
+
return result;
|
|
11278
12371
|
}
|
|
11279
|
-
|
|
12372
|
+
lockDirWritable(lockPath) {
|
|
12373
|
+
const probe = `${lockPath}.${process.pid}.${randomUUID3()}.probe`;
|
|
11280
12374
|
try {
|
|
11281
|
-
|
|
12375
|
+
fs8.closeSync(fs8.openSync(probe, "wx", 384));
|
|
11282
12376
|
} catch {
|
|
11283
|
-
return
|
|
12377
|
+
return false;
|
|
11284
12378
|
}
|
|
12379
|
+
try {
|
|
12380
|
+
fs8.rmSync(probe, { force: true });
|
|
12381
|
+
} catch {}
|
|
12382
|
+
return true;
|
|
11285
12383
|
}
|
|
11286
|
-
|
|
12384
|
+
lockHolderPresent(lockPath) {
|
|
11287
12385
|
try {
|
|
11288
|
-
|
|
11289
|
-
return
|
|
12386
|
+
fs8.lstatSync(lockPath);
|
|
12387
|
+
return true;
|
|
11290
12388
|
} catch (error) {
|
|
11291
|
-
|
|
11292
|
-
return { exists: false, result: success(null, null, null) };
|
|
11293
|
-
return {
|
|
11294
|
-
exists: true,
|
|
11295
|
-
result: failure2("recovery_required", `snapshot cannot be read: ${String(error)}`, {
|
|
11296
|
-
path: file
|
|
11297
|
-
})
|
|
11298
|
-
};
|
|
12389
|
+
return error?.code !== "ENOENT";
|
|
11299
12390
|
}
|
|
11300
12391
|
}
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
|
|
12392
|
+
lockFailure(lockPath, error) {
|
|
12393
|
+
const value = error;
|
|
12394
|
+
const code = typeof value?.code === "string" ? value.code : "";
|
|
12395
|
+
if (isTransientWindowsError(error) && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
12396
|
+
return failure2("storage_error", `cannot create the workit lock (${code}); no other Workit call holds it`, {
|
|
12397
|
+
path: lockPath,
|
|
12398
|
+
guidance: `Check permissions and the read-only attribute on ${path9.dirname(lockPath)}.`
|
|
12399
|
+
});
|
|
12400
|
+
if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
|
|
12401
|
+
let lock = null;
|
|
12402
|
+
try {
|
|
12403
|
+
lock = parseMetadataLockOrNull(fs8.readFileSync(lockPath, "utf8"));
|
|
12404
|
+
} catch {}
|
|
12405
|
+
if (lock?.externalAction)
|
|
12406
|
+
return failure2("writer_conflict", "workspace is reserved by a managed external action", {
|
|
12407
|
+
outcome: "not_started",
|
|
12408
|
+
path: lockPath
|
|
12409
|
+
});
|
|
12410
|
+
return failure2("busy", `workit lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
|
|
12411
|
+
outcome: "not_started",
|
|
12412
|
+
path: lockPath,
|
|
12413
|
+
guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
|
|
12414
|
+
});
|
|
11307
12415
|
}
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
const parsed = parseStoredRecord(schema, value);
|
|
11311
|
-
if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
|
|
11312
|
-
return success(null, null, parsed.data);
|
|
11313
|
-
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
11314
|
-
const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
|
|
11315
|
-
if (parsed.success)
|
|
11316
|
-
return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
|
|
11317
|
-
if (parsed.critical.length > 0)
|
|
11318
|
-
return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
|
|
11319
|
-
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
11320
|
-
return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
11321
|
-
return failure2("recovery_required", "snapshot does not satisfy its schema");
|
|
11322
|
-
}
|
|
11323
|
-
conflict(expected, actual) {
|
|
11324
|
-
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
11325
|
-
expectedRevision: expected,
|
|
11326
|
-
actualRevision: actual
|
|
11327
|
-
});
|
|
11328
|
-
}
|
|
11329
|
-
workspaceConflict(expected, actual) {
|
|
11330
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
11331
|
-
}
|
|
11332
|
-
taskPath(taskId) {
|
|
11333
|
-
return path7.join(this.tasksDir, `${taskId}.json`);
|
|
11334
|
-
}
|
|
11335
|
-
get workitDir() {
|
|
11336
|
-
return path7.join(this.root, ".workit");
|
|
11337
|
-
}
|
|
11338
|
-
get tasksDir() {
|
|
11339
|
-
return path7.join(this.workitDir, "tasks");
|
|
11340
|
-
}
|
|
11341
|
-
get recoveryDir() {
|
|
11342
|
-
return path7.join(this.workitDir, "recovery");
|
|
11343
|
-
}
|
|
11344
|
-
get workspacePath() {
|
|
11345
|
-
return path7.join(this.workitDir, "workspace.json");
|
|
11346
|
-
}
|
|
11347
|
-
get indexPath() {
|
|
11348
|
-
return path7.join(this.workitDir, "index.json");
|
|
11349
|
-
}
|
|
11350
|
-
get lockPath() {
|
|
11351
|
-
return path7.join(this.workitDir, "metadata.lock");
|
|
11352
|
-
}
|
|
11353
|
-
get gitignorePath() {
|
|
11354
|
-
return path7.join(this.workitDir, ".gitignore");
|
|
12416
|
+
const recovery = code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
|
|
12417
|
+
return failure2(recovery ? "recovery_required" : "storage_error", `lock operation failed: ${String(error)}`, { path: lockPath });
|
|
11355
12418
|
}
|
|
11356
12419
|
}
|
|
11357
|
-
var
|
|
12420
|
+
var MIGRATION_PENDING = "workit migration pending", STORE_FORMAT = 3, STORE_MIN_RUNTIME = "3.0.0", SNAPSHOT_EVERY = 32, COMPACT_ABOVE = 200, COMPACT_KEEP = 50, AUTO_COMPACT_BYTES, TOMBSTONE, STALE_TEMPORARY_MS, INDEX_VERSION = 3, isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), hostSession = (value) => isObject4(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null, indexEntry = (task, key, legacy, file) => {
|
|
11358
12421
|
const sessions = [];
|
|
11359
12422
|
const intent = hostSession(task.intent.provenance.session);
|
|
11360
12423
|
if (intent)
|
|
@@ -11374,11 +12437,16 @@ var RECOVERY_COPIES_PER_RECORD = 3, STALE_TEMPORARY_MS, RECOVERY_NAME, processEv
|
|
|
11374
12437
|
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
11375
12438
|
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
11376
12439
|
sessions,
|
|
12440
|
+
workspaceId: task.workspaceId,
|
|
12441
|
+
key: key?.key ?? null,
|
|
12442
|
+
branch: key?.branch ?? null,
|
|
12443
|
+
boundAt: key?.boundAt ?? null,
|
|
12444
|
+
legacy,
|
|
11377
12445
|
file
|
|
11378
12446
|
};
|
|
11379
12447
|
}, fileSignature = (file) => {
|
|
11380
12448
|
try {
|
|
11381
|
-
const stat =
|
|
12449
|
+
const stat = fs8.statSync(file, { bigint: true });
|
|
11382
12450
|
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
11383
12451
|
} catch {
|
|
11384
12452
|
return null;
|
|
@@ -11387,10 +12455,10 @@ var RECOVERY_COPIES_PER_RECORD = 3, STALE_TEMPORARY_MS, RECOVERY_NAME, processEv
|
|
|
11387
12455
|
const [mtime = 0n, ctime = 0n] = signature.split(":").slice(3, 5).map(BigInt);
|
|
11388
12456
|
const changed = mtime > ctime ? mtime : ctime;
|
|
11389
12457
|
return BigInt(Date.now()) * 1000000n - changed < RACY_WINDOW_NS;
|
|
11390
|
-
}, now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"), cachedRuntimeVersion = null, runtimeVersion = () => {
|
|
12458
|
+
}, now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"), oldestFirst = (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id), cachedRuntimeVersion = null, runtimeVersion = () => {
|
|
11391
12459
|
if (cachedRuntimeVersion === null) {
|
|
11392
12460
|
try {
|
|
11393
|
-
const pkg = JSON.parse(
|
|
12461
|
+
const pkg = JSON.parse(fs8.readFileSync(path9.join(packageRoot(), "package.json"), "utf8"));
|
|
11394
12462
|
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
11395
12463
|
} catch {
|
|
11396
12464
|
cachedRuntimeVersion = "0.0.0";
|
|
@@ -11410,69 +12478,52 @@ var RECOVERY_COPIES_PER_RECORD = 3, STALE_TEMPORARY_MS, RECOVERY_NAME, processEv
|
|
|
11410
12478
|
return left > right;
|
|
11411
12479
|
}
|
|
11412
12480
|
return false;
|
|
11413
|
-
},
|
|
11414
|
-
|
|
11415
|
-
if (!path7.isAbsolute(left) || !path7.isAbsolute(right))
|
|
12481
|
+
}, 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), sameDirectoryIdentity = (left, right) => {
|
|
12482
|
+
if (!path9.isAbsolute(left) || !path9.isAbsolute(right))
|
|
11416
12483
|
return false;
|
|
11417
|
-
const normalizedLeft =
|
|
11418
|
-
const normalizedRight =
|
|
12484
|
+
const normalizedLeft = path9.resolve(left);
|
|
12485
|
+
const normalizedRight = path9.resolve(right);
|
|
11419
12486
|
try {
|
|
11420
|
-
const a =
|
|
11421
|
-
const b =
|
|
12487
|
+
const a = fs8.statSync(normalizedLeft, { bigint: true });
|
|
12488
|
+
const b = fs8.statSync(normalizedRight, { bigint: true });
|
|
11422
12489
|
if (!a.isDirectory() || !b.isDirectory())
|
|
11423
12490
|
return false;
|
|
11424
12491
|
if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
|
|
11425
12492
|
return true;
|
|
11426
|
-
const canonicalLeft =
|
|
11427
|
-
const canonicalRight =
|
|
12493
|
+
const canonicalLeft = fs8.realpathSync(normalizedLeft);
|
|
12494
|
+
const canonicalRight = fs8.realpathSync(normalizedRight);
|
|
11428
12495
|
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
11429
12496
|
return true;
|
|
11430
12497
|
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
11431
12498
|
} catch {
|
|
11432
12499
|
return false;
|
|
11433
12500
|
}
|
|
11434
|
-
},
|
|
11435
|
-
|
|
11436
|
-
return false;
|
|
11437
|
-
const code = error?.code;
|
|
11438
|
-
return typeof code === "string" && TRANSIENT_WINDOWS_CODES.has(code);
|
|
11439
|
-
}, retryTransient = (run) => {
|
|
11440
|
-
if (process.platform !== "win32")
|
|
11441
|
-
return run();
|
|
11442
|
-
for (let attempt = 0;; attempt += 1) {
|
|
11443
|
-
try {
|
|
11444
|
-
return run();
|
|
11445
|
-
} catch (error) {
|
|
11446
|
-
if (attempt >= 20 || !isTransientWindowsError(error))
|
|
11447
|
-
throw error;
|
|
11448
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 * (attempt + 1));
|
|
11449
|
-
}
|
|
11450
|
-
}
|
|
11451
|
-
}, externalActionLockRoots, heldInProcess, holdRoot = (root) => heldInProcess.set(root, (heldInProcess.get(root) ?? 0) + 1), dropRoot = (root) => {
|
|
11452
|
-
const count = (heldInProcess.get(root) ?? 1) - 1;
|
|
12501
|
+
}, externalActionLockRoots, heldInProcess, hold = (lock) => heldInProcess.set(lock, (heldInProcess.get(lock) ?? 0) + 1), drop = (lock) => {
|
|
12502
|
+
const count = (heldInProcess.get(lock) ?? 1) - 1;
|
|
11453
12503
|
if (count > 0)
|
|
11454
|
-
heldInProcess.set(
|
|
12504
|
+
heldInProcess.set(lock, count);
|
|
11455
12505
|
else
|
|
11456
|
-
heldInProcess.delete(
|
|
11457
|
-
},
|
|
11458
|
-
const
|
|
11459
|
-
return
|
|
12506
|
+
heldInProcess.delete(lock);
|
|
12507
|
+
}, envActor = () => {
|
|
12508
|
+
const value = (key) => process.env[key]?.trim() || null;
|
|
12509
|
+
return {
|
|
12510
|
+
host: value("WORKIT_HOST") ?? "runtime",
|
|
12511
|
+
session: value("WORKIT_SESSION_ID"),
|
|
12512
|
+
agentId: value("WORKIT_AGENT_ID")
|
|
12513
|
+
};
|
|
11460
12514
|
};
|
|
11461
12515
|
var init_task_store = __esm(() => {
|
|
11462
|
-
init_zod();
|
|
11463
|
-
init_package_root();
|
|
11464
12516
|
init_file_lock();
|
|
12517
|
+
init_package_root();
|
|
11465
12518
|
init_task_contract();
|
|
11466
12519
|
init_store_lock();
|
|
12520
|
+
init_event_log();
|
|
12521
|
+
init_paths();
|
|
12522
|
+
init_reduce();
|
|
12523
|
+
init_notes();
|
|
12524
|
+
AUTO_COMPACT_BYTES = 2 * 1024 * 1024;
|
|
12525
|
+
TOMBSTONE = Symbol("tombstone");
|
|
11467
12526
|
STALE_TEMPORARY_MS = 60 * 60000;
|
|
11468
|
-
RECOVERY_NAME = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/;
|
|
11469
|
-
processEvidenceSchema = object({
|
|
11470
|
-
state: _enum(["stopped", "accounted_for"]),
|
|
11471
|
-
pid: number2().int().nonnegative(),
|
|
11472
|
-
processStart: string2().nullable(),
|
|
11473
|
-
ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
|
|
11474
|
-
}).strict();
|
|
11475
|
-
TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
11476
12527
|
externalActionLockRoots = new AsyncLocalStorage;
|
|
11477
12528
|
heldInProcess = new Map;
|
|
11478
12529
|
});
|
|
@@ -11534,13 +12585,13 @@ var init_workers = __esm(() => {
|
|
|
11534
12585
|
});
|
|
11535
12586
|
|
|
11536
12587
|
// packages/workit-core/src/core/auto-approval.ts
|
|
11537
|
-
import { createHash as
|
|
12588
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
11538
12589
|
import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
11539
|
-
import
|
|
12590
|
+
import path10 from "node:path";
|
|
11540
12591
|
var AUTO_CLASSES, OPERATION_CLASS, operationAutoClass = (operation) => typeof operation === "string" ? OPERATION_CLASS[operation] ?? null : null, digestOf = (configDirPath) => {
|
|
11541
12592
|
try {
|
|
11542
12593
|
const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
|
|
11543
|
-
return
|
|
12594
|
+
return createHash7("sha256").update(raw).digest("hex");
|
|
11544
12595
|
} catch {
|
|
11545
12596
|
return null;
|
|
11546
12597
|
}
|
|
@@ -11580,7 +12631,7 @@ var AUTO_CLASSES, OPERATION_CLASS, operationAutoClass = (operation) => typeof op
|
|
|
11580
12631
|
const cwd = parsed?.payload?.cwd;
|
|
11581
12632
|
if (cwd === undefined)
|
|
11582
12633
|
return root;
|
|
11583
|
-
if (typeof cwd !== "string" || !
|
|
12634
|
+
if (typeof cwd !== "string" || !path10.isAbsolute(cwd))
|
|
11584
12635
|
return null;
|
|
11585
12636
|
return realpathSync3(cwd);
|
|
11586
12637
|
} catch {
|
|
@@ -11672,9 +12723,9 @@ var init_auto_approval = __esm(() => {
|
|
|
11672
12723
|
});
|
|
11673
12724
|
|
|
11674
12725
|
// packages/workit-core/src/core/authority.ts
|
|
11675
|
-
import { createHash as
|
|
11676
|
-
import * as
|
|
11677
|
-
import
|
|
12726
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
12727
|
+
import * as fs9 from "node:fs";
|
|
12728
|
+
import path11 from "node:path";
|
|
11678
12729
|
function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
11679
12730
|
if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
|
|
11680
12731
|
return [];
|
|
@@ -12076,10 +13127,10 @@ var verifiedAuthorities, trustedAuthority = (kind, provenance, expected, binding
|
|
|
12076
13127
|
const record = verifiedAuthorities.get(authority);
|
|
12077
13128
|
verifiedAuthorities.delete(authority);
|
|
12078
13129
|
return record?.provenance ?? null;
|
|
12079
|
-
}, authorityMatches = (authority, expected) => authority.kind === "action" && authority.taskId === expected.taskId && authority.workspaceId === expected.workspaceId && authority.decisionId === expected.decisionId && sameRef(authority.actionRef, expected.actionRef) && authority.taskRevision === expected.taskRevision && authority.workspaceRevision === expected.workspaceRevision && authority.outcome === expected.outcome, authorityDecisionMatches = (authority, decision) => authority.purpose === decision.purpose && authority.response === decision.response && authority.digest === decision.digest && canonicalJson(authority.binding) === canonicalJson(decision.binding) && canonicalJson(authority.requirementIds) === canonicalJson(decision.requirementIds), provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session),
|
|
13130
|
+
}, authorityMatches = (authority, expected) => authority.kind === "action" && authority.taskId === expected.taskId && authority.workspaceId === expected.workspaceId && authority.decisionId === expected.decisionId && sameRef(authority.actionRef, expected.actionRef) && authority.taskRevision === expected.taskRevision && authority.workspaceRevision === expected.workspaceRevision && authority.outcome === expected.outcome, authorityDecisionMatches = (authority, decision) => authority.purpose === decision.purpose && authority.response === decision.response && authority.digest === decision.digest && canonicalJson(authority.binding) === canonicalJson(decision.binding) && canonicalJson(authority.requirementIds) === canonicalJson(decision.requirementIds), provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session), digestBytes = (bytes) => createHash8("sha256").update(bytes).digest("hex"), verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
12080
13131
|
let root;
|
|
12081
13132
|
try {
|
|
12082
|
-
root =
|
|
13133
|
+
root = fs9.realpathSync(checkoutRoot);
|
|
12083
13134
|
} catch {
|
|
12084
13135
|
return failure2("permission_denied", "checkout root is unavailable");
|
|
12085
13136
|
}
|
|
@@ -12088,23 +13139,23 @@ var verifiedAuthorities, trustedAuthority = (kind, provenance, expected, binding
|
|
|
12088
13139
|
continue;
|
|
12089
13140
|
if (!reference.digest)
|
|
12090
13141
|
return failure2("invalid_input", "document references require a byte digest");
|
|
12091
|
-
const target =
|
|
12092
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
13142
|
+
const target = path11.resolve(root, reference.path);
|
|
13143
|
+
if (target !== root && !target.startsWith(`${root}${path11.sep}`))
|
|
12093
13144
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
12094
13145
|
fields: [
|
|
12095
13146
|
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
12096
13147
|
]
|
|
12097
13148
|
});
|
|
12098
13149
|
try {
|
|
12099
|
-
const relative =
|
|
13150
|
+
const relative = path11.relative(root, target);
|
|
12100
13151
|
let current = root;
|
|
12101
|
-
for (const segment of relative.split(
|
|
13152
|
+
for (const segment of relative.split(path11.sep)) {
|
|
12102
13153
|
if (!segment)
|
|
12103
13154
|
continue;
|
|
12104
|
-
current =
|
|
12105
|
-
const stat =
|
|
12106
|
-
const real =
|
|
12107
|
-
if (real !== root && !real.startsWith(`${root}${
|
|
13155
|
+
current = path11.join(current, segment);
|
|
13156
|
+
const stat = fs9.lstatSync(current);
|
|
13157
|
+
const real = fs9.realpathSync(current);
|
|
13158
|
+
if (real !== root && !real.startsWith(`${root}${path11.sep}`))
|
|
12108
13159
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
12109
13160
|
fields: [
|
|
12110
13161
|
{
|
|
@@ -12120,7 +13171,7 @@ var verifiedAuthorities, trustedAuthority = (kind, provenance, expected, binding
|
|
|
12120
13171
|
if (current !== target && !stat.isDirectory())
|
|
12121
13172
|
return failure2("invalid_input", "document reference ancestor is not a directory");
|
|
12122
13173
|
}
|
|
12123
|
-
if (
|
|
13174
|
+
if (digestBytes(fs9.readFileSync(target)) !== reference.digest)
|
|
12124
13175
|
return failure2("permission_denied", "approved document bytes have changed");
|
|
12125
13176
|
} catch {
|
|
12126
13177
|
return failure2("invalid_input", "approved document is unavailable");
|
|
@@ -12532,9 +13583,9 @@ var init_policy_resolver = __esm(() => {
|
|
|
12532
13583
|
});
|
|
12533
13584
|
|
|
12534
13585
|
// packages/workit-core/src/check-config.ts
|
|
12535
|
-
import { spawnSync as
|
|
12536
|
-
import
|
|
12537
|
-
import
|
|
13586
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
13587
|
+
import fs10 from "node:fs";
|
|
13588
|
+
import path12 from "node:path";
|
|
12538
13589
|
function splitCommand(command) {
|
|
12539
13590
|
const argv = [];
|
|
12540
13591
|
let current = "";
|
|
@@ -12572,12 +13623,12 @@ function splitCommand(command) {
|
|
|
12572
13623
|
return argv.length ? argv : new Error("empty command");
|
|
12573
13624
|
}
|
|
12574
13625
|
function checkRoot(cwd) {
|
|
12575
|
-
return
|
|
13626
|
+
return path12.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
|
|
12576
13627
|
}
|
|
12577
13628
|
function detectedChecks(root) {
|
|
12578
13629
|
const read = (file) => {
|
|
12579
13630
|
try {
|
|
12580
|
-
return
|
|
13631
|
+
return fs10.readFileSync(path12.join(root, file), "utf8");
|
|
12581
13632
|
} catch {
|
|
12582
13633
|
return null;
|
|
12583
13634
|
}
|
|
@@ -12615,7 +13666,7 @@ function detectedChecks(root) {
|
|
|
12615
13666
|
}
|
|
12616
13667
|
function loadCheckConfig(cwd) {
|
|
12617
13668
|
const root = checkRoot(cwd);
|
|
12618
|
-
const file =
|
|
13669
|
+
const file = path12.join(root, CHECKS_FILE);
|
|
12619
13670
|
const none = (error = null) => ({
|
|
12620
13671
|
source: error ? CHECKS_FILE : "none",
|
|
12621
13672
|
root,
|
|
@@ -12625,7 +13676,7 @@ function loadCheckConfig(cwd) {
|
|
|
12625
13676
|
});
|
|
12626
13677
|
let text = null;
|
|
12627
13678
|
try {
|
|
12628
|
-
text =
|
|
13679
|
+
text = fs10.readFileSync(file, "utf8");
|
|
12629
13680
|
} catch (error) {
|
|
12630
13681
|
if (error.code !== "ENOENT")
|
|
12631
13682
|
return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
|
|
@@ -12686,10 +13737,10 @@ function workitAvailable(env = process.env) {
|
|
|
12686
13737
|
return workitOnPath;
|
|
12687
13738
|
const windows = process.platform === "win32";
|
|
12688
13739
|
const exts = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
12689
|
-
const dirs = (env.PATH ?? env.Path ?? "").split(
|
|
13740
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path12.delimiter).filter(Boolean);
|
|
12690
13741
|
workitOnPath = dirs.some((dir) => exts.some((ext) => {
|
|
12691
13742
|
try {
|
|
12692
|
-
return
|
|
13743
|
+
return fs10.statSync(path12.join(dir, `workit${ext}`)).isFile();
|
|
12693
13744
|
} catch {
|
|
12694
13745
|
return false;
|
|
12695
13746
|
}
|
|
@@ -12700,7 +13751,7 @@ function checkCommand(args, version, onPath = workitAvailable()) {
|
|
|
12700
13751
|
return onPath ? `workit check ${args}` : `npx -y @brainervirus/workit-cli@${version} check ${args}`;
|
|
12701
13752
|
}
|
|
12702
13753
|
var SCRIPT_CHECKS, CHECKS_FILE = "workit.checks.json", isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), SHELL_META, git = (cwd, args) => {
|
|
12703
|
-
const run =
|
|
13754
|
+
const run = spawnSync4("git", args, {
|
|
12704
13755
|
cwd,
|
|
12705
13756
|
encoding: "utf8",
|
|
12706
13757
|
timeout: 20000,
|
|
@@ -12710,7 +13761,7 @@ var SCRIPT_CHECKS, CHECKS_FILE = "workit.checks.json", isRecord2 = (value) => ty
|
|
|
12710
13761
|
const value = run.status === 0 ? (run.stdout ?? "").trim() : "";
|
|
12711
13762
|
return value || null;
|
|
12712
13763
|
}, packageManager = (root) => {
|
|
12713
|
-
const has = (file) =>
|
|
13764
|
+
const has = (file) => fs10.existsSync(path12.join(root, file));
|
|
12714
13765
|
if (has("bun.lock") || has("bun.lockb"))
|
|
12715
13766
|
return "bun";
|
|
12716
13767
|
if (has("pnpm-lock.yaml"))
|
|
@@ -12733,11 +13784,11 @@ var init_check_config = __esm(() => {
|
|
|
12733
13784
|
});
|
|
12734
13785
|
|
|
12735
13786
|
// packages/workit-core/src/git/rev.ts
|
|
12736
|
-
import { spawnSync as
|
|
12737
|
-
import { createHash as
|
|
12738
|
-
import
|
|
13787
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
13788
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
13789
|
+
import fs11 from "node:fs";
|
|
12739
13790
|
import os2 from "node:os";
|
|
12740
|
-
import
|
|
13791
|
+
import path13 from "node:path";
|
|
12741
13792
|
function headSha(cwd) {
|
|
12742
13793
|
return line(git2(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
|
|
12743
13794
|
}
|
|
@@ -12748,10 +13799,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12748
13799
|
return null;
|
|
12749
13800
|
const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
|
|
12750
13801
|
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
|
|
12751
|
-
const scratch =
|
|
13802
|
+
const scratch = fs11.mkdtempSync(path13.join(os2.tmpdir(), "workit-tree-"));
|
|
12752
13803
|
try {
|
|
12753
|
-
const tempIndex =
|
|
12754
|
-
if (
|
|
13804
|
+
const tempIndex = path13.join(scratch, "index");
|
|
13805
|
+
if (fs11.existsSync(indexPath))
|
|
12755
13806
|
seedIndex(indexPath, tempIndex);
|
|
12756
13807
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
12757
13808
|
const untracked = git2(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
@@ -12763,7 +13814,7 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12763
13814
|
const skipped = [];
|
|
12764
13815
|
for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
|
|
12765
13816
|
try {
|
|
12766
|
-
const stat =
|
|
13817
|
+
const stat = fs11.statSync(path13.join(top, file));
|
|
12767
13818
|
if (stat.isFile() && stat.size > cap)
|
|
12768
13819
|
skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
|
|
12769
13820
|
} catch {}
|
|
@@ -12780,10 +13831,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12780
13831
|
if (!tree)
|
|
12781
13832
|
return null;
|
|
12782
13833
|
const base = line(git2(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
|
|
12783
|
-
const key = skipped.length ? `sha256:${
|
|
13834
|
+
const key = skipped.length ? `sha256:${createHash9("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
|
|
12784
13835
|
return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
|
|
12785
13836
|
} finally {
|
|
12786
|
-
|
|
13837
|
+
fs11.rmSync(scratch, { recursive: true, force: true });
|
|
12787
13838
|
}
|
|
12788
13839
|
}
|
|
12789
13840
|
function worktreeSignal(cwd, options = {}) {
|
|
@@ -12803,16 +13854,16 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
12803
13854
|
});
|
|
12804
13855
|
if (!status.ok)
|
|
12805
13856
|
return null;
|
|
12806
|
-
const hash =
|
|
13857
|
+
const hash = createHash9("sha256");
|
|
12807
13858
|
hash.update(`${headSha(top) ?? "unborn"}\x00`);
|
|
12808
13859
|
for (const entry of status.stdout.split("\x00")) {
|
|
12809
13860
|
if (entry.length < 4)
|
|
12810
13861
|
continue;
|
|
12811
13862
|
const file = entry.slice(3);
|
|
12812
|
-
const absolute =
|
|
13863
|
+
const absolute = path13.join(top, file);
|
|
12813
13864
|
let stat = "-";
|
|
12814
13865
|
try {
|
|
12815
|
-
const info =
|
|
13866
|
+
const info = fs11.lstatSync(absolute, { bigint: true });
|
|
12816
13867
|
stat = `${info.mode.toString(8)}:${info.size}:${info.mtimeNs}`;
|
|
12817
13868
|
if (info.isDirectory())
|
|
12818
13869
|
stat += `:${headSha(absolute) ?? "-"}`;
|
|
@@ -12822,7 +13873,7 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
12822
13873
|
return `sig:${hash.digest("hex")}`;
|
|
12823
13874
|
}
|
|
12824
13875
|
var MAX_BUFFER, GIT_TIMEOUTS, git2 = (cwd, args, options = {}) => {
|
|
12825
|
-
const result =
|
|
13876
|
+
const result = spawnSync5("git", args, {
|
|
12826
13877
|
cwd,
|
|
12827
13878
|
encoding: "utf8",
|
|
12828
13879
|
env: options.env ?? process.env,
|
|
@@ -12845,10 +13896,10 @@ var MAX_BUFFER, GIT_TIMEOUTS, git2 = (cwd, args, options = {}) => {
|
|
|
12845
13896
|
const value = run.stdout.trim();
|
|
12846
13897
|
return value ? value : null;
|
|
12847
13898
|
}, emptyTree = (cwd) => line(git2(cwd, ["mktree"], { input: "" })), MAX_UNTRACKED_BYTES, seedIndex = (from, to) => {
|
|
12848
|
-
|
|
12849
|
-
const { atimeNs, mtimeNs } =
|
|
13899
|
+
fs11.copyFileSync(from, to);
|
|
13900
|
+
const { atimeNs, mtimeNs } = fs11.statSync(from, { bigint: true });
|
|
12850
13901
|
const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
|
|
12851
|
-
|
|
13902
|
+
fs11.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
|
|
12852
13903
|
};
|
|
12853
13904
|
var init_rev = __esm(() => {
|
|
12854
13905
|
MAX_BUFFER = 256 * 1024 * 1024;
|
|
@@ -12857,14 +13908,14 @@ var init_rev = __esm(() => {
|
|
|
12857
13908
|
});
|
|
12858
13909
|
|
|
12859
13910
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
12860
|
-
import { createHash as
|
|
12861
|
-
import * as
|
|
12862
|
-
import { spawnSync as
|
|
12863
|
-
import
|
|
13911
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
13912
|
+
import * as fs12 from "node:fs";
|
|
13913
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
13914
|
+
import path14 from "node:path";
|
|
12864
13915
|
function captureCandidate(root, scope, environment = []) {
|
|
12865
13916
|
let checkout;
|
|
12866
13917
|
try {
|
|
12867
|
-
checkout =
|
|
13918
|
+
checkout = fs12.realpathSync(root);
|
|
12868
13919
|
} catch {
|
|
12869
13920
|
return failure2("invalid_input", "candidate root does not exist");
|
|
12870
13921
|
}
|
|
@@ -12882,11 +13933,11 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12882
13933
|
if (item === ".git" || item === ".workit")
|
|
12883
13934
|
continue;
|
|
12884
13935
|
names.add(item);
|
|
12885
|
-
if (!
|
|
13936
|
+
if (!fs12.existsSync(target))
|
|
12886
13937
|
continue;
|
|
12887
13938
|
let stat;
|
|
12888
13939
|
try {
|
|
12889
|
-
stat =
|
|
13940
|
+
stat = fs12.lstatSync(target);
|
|
12890
13941
|
} catch {
|
|
12891
13942
|
uncertain = true;
|
|
12892
13943
|
continue;
|
|
@@ -12902,17 +13953,17 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12902
13953
|
for (const item of [...names].toSorted()) {
|
|
12903
13954
|
if (!scopeMatches(item, normalizedScope))
|
|
12904
13955
|
continue;
|
|
12905
|
-
const target =
|
|
13956
|
+
const target = path14.join(checkout, item);
|
|
12906
13957
|
try {
|
|
12907
|
-
const stat =
|
|
13958
|
+
const stat = fs12.lstatSync(target);
|
|
12908
13959
|
if (stat.isSymbolicLink()) {
|
|
12909
|
-
const link =
|
|
12910
|
-
files.push({ path: item, kind: "symlink", digest:
|
|
13960
|
+
const link = fs12.readlinkSync(target);
|
|
13961
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes2(link), executable: null });
|
|
12911
13962
|
} else if (stat.isFile()) {
|
|
12912
13963
|
files.push({
|
|
12913
13964
|
path: item,
|
|
12914
13965
|
kind: "file",
|
|
12915
|
-
digest:
|
|
13966
|
+
digest: digestBytes2(fs12.readFileSync(target)),
|
|
12916
13967
|
executable: (stat.mode & 73) !== 0
|
|
12917
13968
|
});
|
|
12918
13969
|
} else if (!stat.isDirectory()) {
|
|
@@ -12937,7 +13988,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12937
13988
|
const values = supplied.toSorted((left, right) => compareCodeUnits3(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
12938
13989
|
if (values.some(({ value }) => value === null))
|
|
12939
13990
|
completeness = "uncertain";
|
|
12940
|
-
const headResult =
|
|
13991
|
+
const headResult = spawnSync6("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
12941
13992
|
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
12942
13993
|
completeness = "uncertain";
|
|
12943
13994
|
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
@@ -13247,12 +14298,12 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
13247
14298
|
requirementIds: evaluations.map((item) => item.requirementId)
|
|
13248
14299
|
});
|
|
13249
14300
|
}
|
|
13250
|
-
var
|
|
14301
|
+
var digestBytes2 = (value) => createHash10("sha256").update(value).digest("hex"), inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path14.sep}`), relative = (root, candidate) => path14.relative(root, candidate).split(path14.sep).join("/") || ".", canonicalPath = (value) => {
|
|
13251
14302
|
if (typeof value !== "string" || !value)
|
|
13252
14303
|
return null;
|
|
13253
14304
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
13254
14305
|
return null;
|
|
13255
|
-
const normalized =
|
|
14306
|
+
const normalized = path14.posix.normalize(value);
|
|
13256
14307
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
13257
14308
|
return null;
|
|
13258
14309
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -13282,7 +14333,7 @@ var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex"),
|
|
|
13282
14333
|
}
|
|
13283
14334
|
return left.length - right.length;
|
|
13284
14335
|
}, gitPaths = (root) => {
|
|
13285
|
-
const result =
|
|
14336
|
+
const result = spawnSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
13286
14337
|
cwd: root,
|
|
13287
14338
|
encoding: "buffer"
|
|
13288
14339
|
});
|
|
@@ -13294,20 +14345,20 @@ var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex"),
|
|
|
13294
14345
|
git: !/not a git repository/i.test(stderr)
|
|
13295
14346
|
};
|
|
13296
14347
|
}
|
|
13297
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
13298
|
-
const stagedDeleted =
|
|
14348
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")) : [];
|
|
14349
|
+
const stagedDeleted = spawnSync6("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
13299
14350
|
if (stagedDeleted.status !== 0) {
|
|
13300
14351
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
13301
14352
|
if (!/not a git repository/i.test(stderr))
|
|
13302
14353
|
return { paths, uncertain: true, git: true };
|
|
13303
14354
|
} else if (stagedDeleted.stdout) {
|
|
13304
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
14355
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")));
|
|
13305
14356
|
}
|
|
13306
14357
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
13307
14358
|
}, walk = (root, directory, output) => {
|
|
13308
14359
|
let names;
|
|
13309
14360
|
try {
|
|
13310
|
-
names =
|
|
14361
|
+
names = fs12.readdirSync(directory);
|
|
13311
14362
|
} catch {
|
|
13312
14363
|
return true;
|
|
13313
14364
|
}
|
|
@@ -13315,10 +14366,10 @@ var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex"),
|
|
|
13315
14366
|
for (const name of names) {
|
|
13316
14367
|
if (name === ".git" || name === ".workit")
|
|
13317
14368
|
continue;
|
|
13318
|
-
const target =
|
|
14369
|
+
const target = path14.join(directory, name);
|
|
13319
14370
|
let stat;
|
|
13320
14371
|
try {
|
|
13321
|
-
stat =
|
|
14372
|
+
stat = fs12.lstatSync(target);
|
|
13322
14373
|
} catch {
|
|
13323
14374
|
uncertain = true;
|
|
13324
14375
|
continue;
|
|
@@ -13332,14 +14383,14 @@ var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex"),
|
|
|
13332
14383
|
}, scopeRoots = (root, scope) => {
|
|
13333
14384
|
const roots = [];
|
|
13334
14385
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
13335
|
-
const target =
|
|
14386
|
+
const target = path14.resolve(root, item);
|
|
13336
14387
|
if (!inside(root, target))
|
|
13337
14388
|
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` }] });
|
|
13338
14389
|
let ancestor = target;
|
|
13339
|
-
while (!
|
|
13340
|
-
ancestor =
|
|
14390
|
+
while (!fs12.existsSync(ancestor) && ancestor !== root)
|
|
14391
|
+
ancestor = path14.dirname(ancestor);
|
|
13341
14392
|
try {
|
|
13342
|
-
if (!inside(root,
|
|
14393
|
+
if (!inside(root, fs12.realpathSync(ancestor)))
|
|
13343
14394
|
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` }] });
|
|
13344
14395
|
} catch {
|
|
13345
14396
|
return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
@@ -13459,104 +14510,76 @@ var METHODS, methodMatches = (definition, requirement) => definition.dimensions?
|
|
|
13459
14510
|
return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
|
|
13460
14511
|
return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
|
|
13461
14512
|
}, invariantBootstrap = () => `
|
|
13462
|
-
Workit is optional coordination and
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
decisions make continuity useful; similar titles alone never merge tasks.
|
|
14513
|
+
Workit is optional coordination and proof tooling around the host: you decide
|
|
14514
|
+
what and whether, the \`workit\` CLI does how and records what it observed.
|
|
14515
|
+
Native host allow/ask/deny, sandbox and org rules stay authoritative. Never
|
|
14516
|
+
switch paths to evade a denial, never claim enforcement a host cannot provide,
|
|
14517
|
+
and never fabricate a receipt, approval or verdict. A question answer is not
|
|
14518
|
+
host permission, and imported decisions or handoffs never grant authority.
|
|
13469
14519
|
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
14520
|
+
Autonomy contract:
|
|
14521
|
+
- Continue to the requested endpoint (commit, push, PR, green CI, verified,
|
|
14522
|
+
merged) without asking "continue?", running checks and repairs on the way.
|
|
14523
|
+
Stop only for a new consequential choice, a host denial, a conflicting edit
|
|
14524
|
+
or an unresolved blocker; report open gaps and uncertain workers.
|
|
14525
|
+
- No endpoint named: stop at a local commit on a policy-compliant branch and
|
|
14526
|
+
state the next command. Push and open a PR only when the request implies
|
|
14527
|
+
delivery (fix, implement, ship) and \`workit grant show\` reports
|
|
14528
|
+
\`defaultEndpoint\` \`pr\`; otherwise the endpoint is \`commit\`. PRs open,
|
|
14529
|
+
CI green and verified is the most autonomy allows, never the default target.
|
|
14530
|
+
Merge and release need a workspace grant; a blocked verb names its unblock.
|
|
14531
|
+
- Ask only for a product or preference choice, or for authority you lack, with
|
|
14532
|
+
your recommended answer. Facts are yours: read, run or prototype.
|
|
14533
|
+
- Label claims measured (you ran it this session and saw the result),
|
|
14534
|
+
inferred, or guess. Prose is a note: only a configured check the CLI ran
|
|
14535
|
+
(\`workit check <name>\`) satisfies a gate; an ad-hoc
|
|
14536
|
+
\`workit check -- <cmd>\` never does.
|
|
14537
|
+
- Author is not verifier: never record a passing verdict on work your session
|
|
14538
|
+
wrote. The lead starts each verifier with its own session
|
|
14539
|
+
(\`WORKIT_SESSION_ID=<lead>-v<n>\`; Claude Code subagents get one from the
|
|
14540
|
+
hook), and the verifier records \`workit ledger verdict\`.
|
|
14541
|
+
- A local-commit endpoint does not imply PR readiness, and a local commit alone
|
|
14542
|
+
is not evidence of a requested remote push. Before reporting delivery,
|
|
14543
|
+
reconcile every requested item and observe it (\`workit verify-delivery\`).
|
|
13478
14544
|
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
13487
|
-
|
|
13488
|
-
|
|
13489
|
-
|
|
13490
|
-
|
|
13491
|
-
before deciding whether to retry.
|
|
13492
|
-
A solo edit does not need writer acquisition; use it when concurrent checkout
|
|
13493
|
-
writers need coordination. Record only observed facts and checks. Close-time
|
|
13494
|
-
testing and verification gates accept only a configured check the CLI ran:
|
|
13495
|
-
\`workit check <name>\` (\`npx -y @brainervirus/workit-cli check <name>\` when
|
|
13496
|
-
\`workit\` is not on PATH); a recorded check result is a note and an ad-hoc
|
|
13497
|
-
\`workit check -- <cmd>\` never satisfies a gate. Evidence can become stale when its bound candidate or tree changes; reconcile findings against the
|
|
13498
|
-
current candidate before recording completion.
|
|
14545
|
+
CLI first: if a step has one right answer, use the \`workit\` verb instead of
|
|
14546
|
+
hand-running git, gh or glab: check, git, pr, ci, stack, verify-delivery,
|
|
14547
|
+
ledger, handoff, test-audit (\`workit help <verb>\`; without \`workit\` on
|
|
14548
|
+
PATH, \`npx -y @brainervirus/workit-cli\`). \`busy\` is retryable
|
|
14549
|
+
(\`workit doctor --fix-lock\` clears a dead lock); \`blocked\` names its unblock.
|
|
14550
|
+
The task families (task, policy, evidence, finding, decision, worker, writer,
|
|
14551
|
+
state) are optional continuity for tracked work; each branch has one implicit
|
|
14552
|
+
task that the first recording creates, and a solo edit needs no task or writer.
|
|
14553
|
+
Omit revisions; a revision_conflict means one you passed is stale, so re-read.
|
|
14554
|
+
Across repositories, bind each item to its checkout, branch and endpoint,
|
|
14555
|
+
resolve competing targets before a mutation, and reconcile an uncertain
|
|
14556
|
+
external effect before retrying it.
|
|
13499
14557
|
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
state. The task directory is coordination state, not a boundary on which
|
|
13503
|
-
repository may be changed. Use an action-time cwd target where a managed
|
|
13504
|
-
Git/hosting action supports it; a non-Git directory remains valid for OS work.
|
|
13505
|
-
GitHub/GitLab use the active gh/glab CLI identity; YouTrack uses its own token.
|
|
13506
|
-
Preserve uncertain external outcomes and reconcile repository/provider state
|
|
13507
|
-
before retrying. Internal reservations prevent duplicate or ambiguous effects;
|
|
13508
|
-
they are not permission tickets for every edit.
|
|
14558
|
+
Mid-task input: answer a quick question in place, fold an adjustment in, and
|
|
14559
|
+
never silently drop or resume an objective (workit-continue).
|
|
13509
14560
|
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
13521
|
-
|
|
13522
|
-
evidence and a recommendation, then continue toward the requested delivery
|
|
13523
|
-
endpoint: investigation, implementation, branch/commit/push, PR/MR-ready,
|
|
13524
|
-
merge, or release. Run
|
|
13525
|
-
applicable checks and safe repairs without repeated continuation questions;
|
|
13526
|
-
stop for missing host authority, a new consequential choice, a conflicting edit,
|
|
13527
|
-
or an unresolved blocker. A tracked record may close when actual evidence
|
|
13528
|
-
supports its outcome; no final human closure ceremony is required.
|
|
13529
|
-
|
|
13530
|
-
A completed native subagent run stops its bound worker by itself; if a worker
|
|
13531
|
-
strands in cancelling with its run verifiably over, repeat worker.cancel to
|
|
13532
|
-
confirm the stop. Preserve unresolved requirements, gaps, and uncertain workers.
|
|
13533
|
-
When context changes, distinguish a quick question, same-task adjustment, and
|
|
13534
|
-
separate request. Answer a quick question without pausing/resuming task state;
|
|
13535
|
-
park a concise checkpoint only when substantial work needs to continue later.
|
|
13536
|
-
For work spanning repositories, bind each unfinished item to its actual checkout,
|
|
13537
|
-
branch, requested deliverables, and delivery endpoint. Preserve held items with
|
|
13538
|
-
an explicit resume condition. Resolve competing plausible targets before a
|
|
13539
|
-
mutation; ask only when available context cannot settle that consequential choice.
|
|
13540
|
-
Before reporting delivery, reconcile every requested item and observe the actual
|
|
13541
|
-
target result; a local commit alone is not evidence of a requested remote push.
|
|
13542
|
-
|
|
13543
|
-
Skill routing: slash aliases /wk-* load on demand. Use workit-steer for a
|
|
13544
|
-
substantial interruption or change of direction, workit-deslop when relevant
|
|
13545
|
-
to a PR-ready endpoint, workit-green-run for failing CI, workit-blast-radius
|
|
13546
|
-
when impact is uncertain, workit-challenge for genuinely open consequential
|
|
13547
|
-
choices, workit-bdd to turn acceptance criteria into Given/When/Then tests, and
|
|
13548
|
-
workit-test-audit to check tests for tautologies. Load workit-plan when dependencies or handoff need durable next actions.
|
|
13549
|
-
Load the skill; never act from memory of it.
|
|
14561
|
+
Skills: load the skill, never act from memory of it (slash aliases /wk-<name>).
|
|
14562
|
+
- brainstorm, plan, spec, grill, should we: workit-shape
|
|
14563
|
+
- implement, build, add a feature: workit-implement
|
|
14564
|
+
- review, blast radius: workit-review
|
|
14565
|
+
- bug, broken, flaky, regression: workit-debug
|
|
14566
|
+
- ship, babysit, CI, merge: workit-ship
|
|
14567
|
+
- resume, pick up, handoff, interruption: workit-continue
|
|
14568
|
+
- BDD, TDD, acceptance criteria, Given/When/Then: workit-bdd
|
|
14569
|
+
- test audit, tautology, weak tests: workit-test-audit
|
|
14570
|
+
- deslop, slop, dead code: workit-deslop
|
|
14571
|
+
- fan out, parallelize, parallel agents, swarm: workit-fanout
|
|
14572
|
+
- verify the app, smoke test, prove it works: workit-verify-app
|
|
13550
14573
|
`.trim();
|
|
13551
14574
|
var init_methods = __esm(() => {
|
|
13552
14575
|
METHODS = {
|
|
13553
|
-
"workit-
|
|
13554
|
-
"workit-
|
|
14576
|
+
"workit-shape": { dimensions: ["challenge", "decisions", "artifacts", "continuity"] },
|
|
14577
|
+
"workit-bdd": { dimensions: ["testing"] },
|
|
13555
14578
|
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
|
|
13556
|
-
"workit-
|
|
13557
|
-
"workit-
|
|
14579
|
+
"workit-implement": { ruleIds: ["mechanical-existing-checks"] },
|
|
14580
|
+
"workit-fanout": { dimensions: ["delegation"] },
|
|
13558
14581
|
"workit-debug": {},
|
|
13559
|
-
"workit-
|
|
14582
|
+
"workit-continue": {},
|
|
13560
14583
|
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
13561
14584
|
};
|
|
13562
14585
|
});
|
|
@@ -13690,7 +14713,9 @@ class WorkitCore {
|
|
|
13690
14713
|
}
|
|
13691
14714
|
contextRootError() {
|
|
13692
14715
|
try {
|
|
13693
|
-
|
|
14716
|
+
realpathSync6(this.context.root);
|
|
14717
|
+
const root = checkoutRootOf(this.context.root);
|
|
14718
|
+
if (root !== this.store.root && !sameDirectoryIdentity(root, this.store.root))
|
|
13694
14719
|
return failure2("invalid_input", "operation context root does not match the task store root");
|
|
13695
14720
|
} catch {
|
|
13696
14721
|
return failure2("invalid_input", "operation context root cannot be resolved");
|
|
@@ -13712,6 +14737,30 @@ class WorkitCore {
|
|
|
13712
14737
|
retryOmittedRevisions(request, run) {
|
|
13713
14738
|
return retryFilledRevisions(filledRevisions(request), run);
|
|
13714
14739
|
}
|
|
14740
|
+
implicitTask(family, request) {
|
|
14741
|
+
if (typeof request !== "object" || request === null || Array.isArray(request))
|
|
14742
|
+
return success(null, null, request);
|
|
14743
|
+
const value = request;
|
|
14744
|
+
if (value.taskId !== undefined || typeof value.action !== "string")
|
|
14745
|
+
return success(null, null, request);
|
|
14746
|
+
const action = `${family}.${value.action}`;
|
|
14747
|
+
if (!TASK_SCOPED_ACTIONS.has(action))
|
|
14748
|
+
return success(null, null, request);
|
|
14749
|
+
const root = this.contextRootError();
|
|
14750
|
+
if (!root.ok)
|
|
14751
|
+
return root;
|
|
14752
|
+
const create = CREATING_ACTIONS.has(action) && (this.context.workerId ?? null) === null;
|
|
14753
|
+
const found = this.store.implicitTask({
|
|
14754
|
+
provenance: provenance(this.context),
|
|
14755
|
+
create,
|
|
14756
|
+
now: trustedNow(this.context)
|
|
14757
|
+
});
|
|
14758
|
+
if (!found.ok)
|
|
14759
|
+
return found;
|
|
14760
|
+
if (!found.data)
|
|
14761
|
+
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');
|
|
14762
|
+
return success(null, null, { ...request, taskId: found.data.task.id });
|
|
14763
|
+
}
|
|
13715
14764
|
helperEntry(task, requireSession = false, requireActive = false) {
|
|
13716
14765
|
const workerId = this.context.workerId ?? null;
|
|
13717
14766
|
if (workerId === null)
|
|
@@ -13743,7 +14792,11 @@ class WorkitCore {
|
|
|
13743
14792
|
return null;
|
|
13744
14793
|
}
|
|
13745
14794
|
state(request) {
|
|
13746
|
-
|
|
14795
|
+
const resolved = this.implicitTask("state", request);
|
|
14796
|
+
if (!resolved.ok)
|
|
14797
|
+
return resolved;
|
|
14798
|
+
const value = resolved.data;
|
|
14799
|
+
return value?.action === "import" ? this.retryOmittedRevisions(value, () => this.stateOnce(value)) : this.stateOnce(value);
|
|
13747
14800
|
}
|
|
13748
14801
|
stateOnce(request) {
|
|
13749
14802
|
const root = this.contextRootError();
|
|
@@ -13810,44 +14863,14 @@ class WorkitCore {
|
|
|
13810
14863
|
return imported;
|
|
13811
14864
|
return this.summary(imported.data);
|
|
13812
14865
|
}
|
|
13813
|
-
|
|
13814
|
-
return failure2("permission_denied", "native recovery authority is unavailable");
|
|
13815
|
-
const processEvidence = (lock, writer) => this.context.nativeRecovery({
|
|
13816
|
-
lock,
|
|
13817
|
-
writer,
|
|
13818
|
-
reason: input.reason,
|
|
13819
|
-
authorityRefs: input.authorityRefs
|
|
13820
|
-
});
|
|
13821
|
-
if (input.target === "workspace")
|
|
13822
|
-
return this.store.recoverWorkspace({
|
|
13823
|
-
expectedBytes: input.expectedBytes,
|
|
13824
|
-
snapshotDigest: input.snapshotDigest,
|
|
13825
|
-
reason: input.reason,
|
|
13826
|
-
authorityRefs: input.authorityRefs,
|
|
13827
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
13828
|
-
processEvidence
|
|
13829
|
-
});
|
|
13830
|
-
const workspace = this.store.readWorkspace();
|
|
13831
|
-
if (!workspace.ok)
|
|
13832
|
-
return workspace;
|
|
13833
|
-
if (!workspace.data)
|
|
13834
|
-
return failure2("not_found", "workspace not found");
|
|
13835
|
-
this.fillRevisions(input, undefined, workspace.data);
|
|
13836
|
-
if (input.target === "workspace" && input.taskId !== undefined)
|
|
13837
|
-
return failure2("invalid_input", "workspace recovery takes no task ID");
|
|
13838
|
-
if (input.target === "task" && input.taskId === undefined)
|
|
13839
|
-
return failure2("invalid_input", "task recovery requires a task ID");
|
|
13840
|
-
return this.store.recoverTask(input.taskId, {
|
|
13841
|
-
expectedBytes: input.expectedBytes,
|
|
13842
|
-
snapshotDigest: input.snapshotDigest,
|
|
13843
|
-
reason: input.reason,
|
|
13844
|
-
authorityRefs: input.authorityRefs,
|
|
13845
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
13846
|
-
processEvidence
|
|
13847
|
-
});
|
|
14866
|
+
return failure2("invalid_input", "unknown state action");
|
|
13848
14867
|
}
|
|
13849
14868
|
task(request) {
|
|
13850
|
-
|
|
14869
|
+
const resolved = this.implicitTask("task", request);
|
|
14870
|
+
if (!resolved.ok)
|
|
14871
|
+
return resolved;
|
|
14872
|
+
const value = resolved.data;
|
|
14873
|
+
return this.retryOmittedRevisions(value, () => this.taskOnce(value));
|
|
13851
14874
|
}
|
|
13852
14875
|
taskOnce(request) {
|
|
13853
14876
|
const root = this.contextRootError();
|
|
@@ -13949,7 +14972,11 @@ class WorkitCore {
|
|
|
13949
14972
|
}
|
|
13950
14973
|
}
|
|
13951
14974
|
policy(request) {
|
|
13952
|
-
|
|
14975
|
+
const resolved = this.implicitTask("policy", request);
|
|
14976
|
+
if (!resolved.ok)
|
|
14977
|
+
return resolved;
|
|
14978
|
+
const value = resolved.data;
|
|
14979
|
+
return this.retryOmittedRevisions(value, () => this.policyOnce(value));
|
|
13953
14980
|
}
|
|
13954
14981
|
policyOnce(request) {
|
|
13955
14982
|
const root = this.contextRootError();
|
|
@@ -13998,7 +15025,11 @@ class WorkitCore {
|
|
|
13998
15025
|
return success(changed.data.revision, null, changed.data.policy);
|
|
13999
15026
|
}
|
|
14000
15027
|
evidence(request) {
|
|
14001
|
-
|
|
15028
|
+
const resolved = this.implicitTask("evidence", request);
|
|
15029
|
+
if (!resolved.ok)
|
|
15030
|
+
return resolved;
|
|
15031
|
+
const value = resolved.data;
|
|
15032
|
+
return this.retryOmittedRevisions(value, () => this.evidenceOnce(value));
|
|
14002
15033
|
}
|
|
14003
15034
|
evidenceOnce(request) {
|
|
14004
15035
|
const root = this.contextRootError();
|
|
@@ -14139,13 +15170,16 @@ class WorkitCore {
|
|
|
14139
15170
|
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
14140
15171
|
}
|
|
14141
15172
|
decision(request) {
|
|
14142
|
-
|
|
15173
|
+
const resolved = this.implicitTask("decision", request);
|
|
15174
|
+
return resolved.ok ? this.recordDecision(resolved.data) : resolved;
|
|
14143
15175
|
}
|
|
14144
15176
|
observeDecision(request, observation) {
|
|
14145
|
-
|
|
15177
|
+
const resolved = this.implicitTask("decision", request);
|
|
15178
|
+
return resolved.ok ? this.recordDecision(resolved.data, observation, true) : resolved;
|
|
14146
15179
|
}
|
|
14147
15180
|
observeStandingDecision(request) {
|
|
14148
|
-
|
|
15181
|
+
const resolved = this.implicitTask("decision", request);
|
|
15182
|
+
return resolved.ok ? this.recordDecision(resolved.data, undefined, true, true) : resolved;
|
|
14149
15183
|
}
|
|
14150
15184
|
commitTask(taskId, expected, filled, recheck, update) {
|
|
14151
15185
|
let revision = expected;
|
|
@@ -14307,7 +15341,11 @@ class WorkitCore {
|
|
|
14307
15341
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
|
|
14308
15342
|
}
|
|
14309
15343
|
finding(request) {
|
|
14310
|
-
|
|
15344
|
+
const resolved = this.implicitTask("finding", request);
|
|
15345
|
+
if (!resolved.ok)
|
|
15346
|
+
return resolved;
|
|
15347
|
+
const value = resolved.data;
|
|
15348
|
+
return this.retryOmittedRevisions(value, () => this.findingOnce(value));
|
|
14311
15349
|
}
|
|
14312
15350
|
findingOnce(request) {
|
|
14313
15351
|
const root = this.contextRootError();
|
|
@@ -14433,7 +15471,11 @@ class WorkitCore {
|
|
|
14433
15471
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
|
|
14434
15472
|
}
|
|
14435
15473
|
worker(request) {
|
|
14436
|
-
|
|
15474
|
+
const resolved = this.implicitTask("worker", request);
|
|
15475
|
+
if (!resolved.ok)
|
|
15476
|
+
return resolved;
|
|
15477
|
+
const value = resolved.data;
|
|
15478
|
+
return this.retryOmittedRevisions(value, () => this.workerOnce(value));
|
|
14437
15479
|
}
|
|
14438
15480
|
workerOnce(request) {
|
|
14439
15481
|
const root = this.contextRootError();
|
|
@@ -14783,7 +15825,11 @@ class WorkitCore {
|
|
|
14783
15825
|
return applied;
|
|
14784
15826
|
}
|
|
14785
15827
|
writer(request) {
|
|
14786
|
-
|
|
15828
|
+
const resolved = this.implicitTask("writer", request);
|
|
15829
|
+
if (!resolved.ok)
|
|
15830
|
+
return resolved;
|
|
15831
|
+
const value = resolved.data;
|
|
15832
|
+
return this.retryOmittedRevisions(value, () => this.writerOnce(value));
|
|
14787
15833
|
}
|
|
14788
15834
|
writerOnce(request) {
|
|
14789
15835
|
const root = this.contextRootError();
|
|
@@ -15547,7 +16593,7 @@ var provenance = (context, kind = context.provenanceKind ?? "host_observed") =>
|
|
|
15547
16593
|
return changed;
|
|
15548
16594
|
const updated = changed.data.task.workers.find((candidate) => candidate.id === input.workerId);
|
|
15549
16595
|
return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
|
|
15550
|
-
}, refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] })), REVISION_RETRY_ATTEMPTS = 8, retryPause, filledRevisions = (request) => {
|
|
16596
|
+
}, refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] })), TASK_SCOPED_ACTIONS, CREATING_ACTIONS, REVISION_RETRY_ATTEMPTS = 8, retryPause, filledRevisions = (request) => {
|
|
15551
16597
|
const value = typeof request === "object" && request !== null ? request : {};
|
|
15552
16598
|
return {
|
|
15553
16599
|
task: value.expectedRevision === undefined,
|
|
@@ -15776,12 +16822,46 @@ var init_task_engine = __esm(() => {
|
|
|
15776
16822
|
init_authority();
|
|
15777
16823
|
init_policy_resolver();
|
|
15778
16824
|
init_auto_approval();
|
|
16825
|
+
init_task_store();
|
|
16826
|
+
init_paths();
|
|
15779
16827
|
init_store_lock();
|
|
15780
16828
|
init_task_context();
|
|
15781
16829
|
init_workers();
|
|
15782
16830
|
init_task_evaluation();
|
|
15783
16831
|
verifiedWorkers = new WeakMap;
|
|
15784
16832
|
dispatchReservations = new WeakMap;
|
|
16833
|
+
TASK_SCOPED_ACTIONS = new Set([
|
|
16834
|
+
"task.inspect",
|
|
16835
|
+
"task.revise",
|
|
16836
|
+
"task.progress",
|
|
16837
|
+
"task.pause",
|
|
16838
|
+
"task.resume",
|
|
16839
|
+
"task.close",
|
|
16840
|
+
"policy.assess",
|
|
16841
|
+
"policy.preview",
|
|
16842
|
+
"policy.explain",
|
|
16843
|
+
"evidence.record",
|
|
16844
|
+
"finding.record",
|
|
16845
|
+
"finding.resolve",
|
|
16846
|
+
"decision.record",
|
|
16847
|
+
"decision.revoke",
|
|
16848
|
+
"worker.assign",
|
|
16849
|
+
"worker.report",
|
|
16850
|
+
"worker.cancel",
|
|
16851
|
+
"writer.acquire",
|
|
16852
|
+
"writer.release",
|
|
16853
|
+
"state.export"
|
|
16854
|
+
]);
|
|
16855
|
+
CREATING_ACTIONS = new Set([
|
|
16856
|
+
"task.revise",
|
|
16857
|
+
"task.progress",
|
|
16858
|
+
"policy.assess",
|
|
16859
|
+
"evidence.record",
|
|
16860
|
+
"finding.record",
|
|
16861
|
+
"decision.record",
|
|
16862
|
+
"worker.assign",
|
|
16863
|
+
"writer.acquire"
|
|
16864
|
+
]);
|
|
15785
16865
|
retryPause = new Int32Array(new SharedArrayBuffer(4));
|
|
15786
16866
|
});
|
|
15787
16867
|
|
|
@@ -15812,12 +16892,12 @@ var support = (descriptor, axis) => {
|
|
|
15812
16892
|
});
|
|
15813
16893
|
|
|
15814
16894
|
// packages/workit-core/src/hooks/context.ts
|
|
15815
|
-
import
|
|
16895
|
+
import path15 from "node:path";
|
|
15816
16896
|
function sessionCompactContext(store, session, context, selection = "session-bound") {
|
|
15817
16897
|
const listed = store.listTaskIndex();
|
|
15818
16898
|
if (!listed.ok)
|
|
15819
|
-
return null;
|
|
15820
|
-
const entry = currentTaskEntry(listed.data, session, selection);
|
|
16899
|
+
return migrationPending(listed) ? MIGRATION_PENDING_NOTE : null;
|
|
16900
|
+
const entry = currentTaskEntry(listed.data, session, selection) ?? implicitTaskEntry(store, listed.data, session);
|
|
15821
16901
|
return entry ? entryCompactContext(store, entry, session, context) : null;
|
|
15822
16902
|
}
|
|
15823
16903
|
function entryCompactContext(store, entry, session, context) {
|
|
@@ -15842,7 +16922,7 @@ function entryCompactContext(store, entry, session, context) {
|
|
|
15842
16922
|
return null;
|
|
15843
16923
|
const freshness = { mode: "signal", current: () => recentSignal(store.root) };
|
|
15844
16924
|
const observed = task.data.evidence.some(isObservedCheck);
|
|
15845
|
-
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [
|
|
16925
|
+
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path15.resolve(store.root, ref.path)] : []));
|
|
15846
16926
|
const observedFiles = filesKey(files);
|
|
15847
16927
|
const compact = new WorkitCore(store, context).compactContext(entry.id, freshness);
|
|
15848
16928
|
if (!compact.ok)
|
|
@@ -15859,11 +16939,19 @@ function entryCompactContext(store, entry, session, context) {
|
|
|
15859
16939
|
cache.delete(cache.keys().next().value);
|
|
15860
16940
|
return compact.data;
|
|
15861
16941
|
}
|
|
15862
|
-
var boundTo = (entry, session) => entry.sessions.some((item) => item.host === session.host && item.handle === session.handle), newestFirst = (left, right) => right.updatedAt.localeCompare(left.updatedAt), sessionTaskEntry = (entries, session) => entries.filter((entry) => entry.status !== "closed" && boundTo(entry, session)).toSorted(newestFirst)[0] ?? null, unboundOpenTaskEntries = (entries, session, limit = 3) => entries.filter((entry) => entry.status !== "closed" && !boundTo(entry, session)).toSorted(newestFirst).slice(0, limit), currentTaskEntry = (entries, session, selection) => {
|
|
16942
|
+
var MIGRATION_PENDING_NOTE = "workit migration pending — run `workit task status`", migrationPending = (result) => !result.ok && result.code === "needs_input" && (result.error ?? "").startsWith(MIGRATION_PENDING), boundTo = (entry, session) => entry.sessions.some((item) => item.host === session.host && item.handle === session.handle), newestFirst = (left, right) => right.updatedAt.localeCompare(left.updatedAt), sessionTaskEntry = (entries, session) => entries.filter((entry) => entry.status !== "closed" && boundTo(entry, session)).toSorted(newestFirst)[0] ?? null, unboundOpenTaskEntries = (entries, session, limit = 3) => entries.filter((entry) => entry.status !== "closed" && !boundTo(entry, session)).toSorted(newestFirst).slice(0, limit), currentTaskEntry = (entries, session, selection) => {
|
|
15863
16943
|
if (selection === "session-bound")
|
|
15864
16944
|
return sessionTaskEntry(entries, session);
|
|
15865
16945
|
const active = entries.filter((entry) => entry.status === "active");
|
|
15866
16946
|
return active.length === 1 ? active[0] : null;
|
|
16947
|
+
}, implicitTaskEntry = (store, entries, session) => {
|
|
16948
|
+
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)));
|
|
16949
|
+
if (open.length === 0)
|
|
16950
|
+
return null;
|
|
16951
|
+
const key = store.currentKey();
|
|
16952
|
+
if (!key.ok)
|
|
16953
|
+
return null;
|
|
16954
|
+
return open.filter((entry) => entry.key === key.data.key).toSorted(newestFirst)[0] ?? null;
|
|
15867
16955
|
}, unfinishedTaskOffer = (entries, session, excludeTaskId = null) => {
|
|
15868
16956
|
const tasks = unboundOpenTaskEntries(entries.filter((entry) => entry.id !== excludeTaskId), session);
|
|
15869
16957
|
if (tasks.length === 0)
|
|
@@ -15899,9 +16987,15 @@ ${tasks.map((task) => `- ${task.id} [${task.status}; source ${task.source.host}/
|
|
|
15899
16987
|
try {
|
|
15900
16988
|
const store = new TaskStore(input.cwd);
|
|
15901
16989
|
const listed = store.listTaskIndex();
|
|
16990
|
+
if (migrationPending(listed))
|
|
16991
|
+
return `<workit-contract>
|
|
16992
|
+
${invariantBootstrap()}
|
|
16993
|
+
${MIGRATION_PENDING_NOTE}${options.addendum ? `
|
|
16994
|
+
${options.addendum}` : ""}
|
|
16995
|
+
</workit-contract>`;
|
|
15902
16996
|
if (!listed.ok)
|
|
15903
16997
|
throw new Error(listed.error);
|
|
15904
|
-
const entry = currentTaskEntry(listed.data, session, descriptor.context.task);
|
|
16998
|
+
const entry = currentTaskEntry(listed.data, session, descriptor.context.task) ?? implicitTaskEntry(store, listed.data, session);
|
|
15905
16999
|
const text = entry ? entryCompactContext(store, entry, session, hookOperationContext(input, descriptor)) : null;
|
|
15906
17000
|
if (text)
|
|
15907
17001
|
compact = `
|
|
@@ -15920,6 +17014,8 @@ ${options.addendum}` : ""}
|
|
|
15920
17014
|
}, turnContextText = (input, descriptor) => {
|
|
15921
17015
|
try {
|
|
15922
17016
|
const text = sessionCompactContext(new TaskStore(input.cwd), { host: input.host, handle: input.session.id }, hookOperationContext(input, descriptor), descriptor.context.task);
|
|
17017
|
+
if (text === MIGRATION_PENDING_NOTE)
|
|
17018
|
+
return text;
|
|
15923
17019
|
return text ? `<workit-task-context>${text}</workit-task-context>` : null;
|
|
15924
17020
|
} catch {
|
|
15925
17021
|
return null;
|
|
@@ -15989,7 +17085,10 @@ function handleHook(input, deps) {
|
|
|
15989
17085
|
case "shell.pre":
|
|
15990
17086
|
return usable2(descriptor.shellPolicy.deny) ? shellPolicy(input.cwd, event.command) : NONE2;
|
|
15991
17087
|
case "subagent.start":
|
|
15992
|
-
return {
|
|
17088
|
+
return {
|
|
17089
|
+
kind: "context",
|
|
17090
|
+
text: subagentStartText(input.host, input.session.id ?? null, descriptor, event)
|
|
17091
|
+
};
|
|
15993
17092
|
case "compact.pre":
|
|
15994
17093
|
return {
|
|
15995
17094
|
kind: "notice",
|
|
@@ -16003,7 +17102,14 @@ function handleHook(input, deps) {
|
|
|
16003
17102
|
return NONE2;
|
|
16004
17103
|
}
|
|
16005
17104
|
}
|
|
16006
|
-
var usable2 = (value) => value === "native" || value === "partial", NONE2, offered, CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer",
|
|
17105
|
+
var usable2 = (value) => value === "native" || value === "partial", NONE2, offered, CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer", CLAUDE_JUDGES, subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128), subagentStartText = (host, session, descriptor, event) => {
|
|
17106
|
+
if (host === "claude_code" && event.agentType === CLAUDE_WORKTREE_IMPLEMENTER)
|
|
17107
|
+
return `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) working in its own git worktree: it may edit and commit there, within its brief's scope. Before the first commit, switch to a policy-compliant branch with \`workit git branch <branch> --base <base>\` (e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
|
|
17108
|
+
const readOnly = `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided; writer delegation is unavailable.`;
|
|
17109
|
+
if (host === "claude_code" && event.agentType && CLAUDE_JUDGES.has(event.agentType) && event.agentId)
|
|
17110
|
+
return `${readOnly} Its own Workit session is ${subagentSession(session, event.agentId)}: record verdicts with \`workit ledger verdict <result> --session ${subagentSession(session, event.agentId)} ...\` so the ledger tells it apart from the author.`;
|
|
17111
|
+
return readOnly;
|
|
17112
|
+
}, failureDecision = (descriptor, event, error) => {
|
|
16007
17113
|
if (event === "shell.pre" || event === "tool.pre")
|
|
16008
17114
|
return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE2;
|
|
16009
17115
|
if (event === "session.start" || event === "subagent.start")
|
|
@@ -16036,13 +17142,14 @@ var init_handle = __esm(() => {
|
|
|
16036
17142
|
init_policy();
|
|
16037
17143
|
NONE2 = { kind: "none" };
|
|
16038
17144
|
offered = new Set;
|
|
17145
|
+
CLAUDE_JUDGES = new Set(["workit:verifier", "workit:reviewer"]);
|
|
16039
17146
|
});
|
|
16040
17147
|
|
|
16041
17148
|
// packages/workit-core/src/hooks/hosts/fields.ts
|
|
16042
17149
|
import { existsSync as existsSync5, realpathSync as realpathSync7, statSync as statSync2 } from "node:fs";
|
|
16043
|
-
import
|
|
17150
|
+
import path16 from "node:path";
|
|
16044
17151
|
var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value), nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "", optionalText = (value) => typeof value === "string" ? value : null, existingDirectory = (value) => {
|
|
16045
|
-
if (!nonEmpty2(value) || !
|
|
17152
|
+
if (!nonEmpty2(value) || !path16.isAbsolute(value) || !existsSync5(value))
|
|
16046
17153
|
return null;
|
|
16047
17154
|
try {
|
|
16048
17155
|
return statSync2(value).isDirectory() ? realpathSync7(value) : null;
|
|
@@ -16544,12 +17651,12 @@ var init_codex = __esm(() => {
|
|
|
16544
17651
|
};
|
|
16545
17652
|
},
|
|
16546
17653
|
render: render2,
|
|
16547
|
-
addendum: (input) => `<workit-codex-mutations>Codex MCP is read-only: unattested callers cannot mutate. Run the workit CLI
|
|
17654
|
+
addendum: (input) => `<workit-codex-mutations>Codex MCP is read-only: unattested callers cannot mutate. Run workit verbs with the workit CLI on the shell (node_modules/.bin/workit, or npx -y @brainervirus/workit-cli): check, git branch|commit|push, pr, ci, stack, ledger, handoff; task-family mutations take --json --confirm${input.session.id ? `; bind the writer to this session with node_modules/.bin/workit writer acquire --task <id> --revision <rev> --actor ${input.session.id} --confirm` : ""}. A verifier or reviewer records workit ledger verdict under a session the lead assigns (WORKIT_SESSION_ID=<lead>-v<n>), never the author's. Merge and release need a workspace grant; a question answer is not host permission.</workit-codex-mutations>`
|
|
16548
17655
|
};
|
|
16549
17656
|
});
|
|
16550
17657
|
|
|
16551
17658
|
// packages/workit-core/src/hooks/hosts/cursor.ts
|
|
16552
|
-
import
|
|
17659
|
+
import path17 from "node:path";
|
|
16553
17660
|
var undocumented2, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (value) => {
|
|
16554
17661
|
if (!isRecord3(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
|
|
16555
17662
|
return { ok: false, error: "hook_event_name is required" };
|
|
@@ -16613,7 +17720,7 @@ var undocumented2, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (val
|
|
|
16613
17720
|
case "preCompact":
|
|
16614
17721
|
return { kind: "compact.pre", trigger: "auto" };
|
|
16615
17722
|
}
|
|
16616
|
-
}, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(
|
|
17723
|
+
}, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path17.sep) ? root : `${root}${path17.sep}`), hookCwd = (input) => {
|
|
16617
17724
|
const cwd = existingDirectory(input.cwd);
|
|
16618
17725
|
if (cwd && input.hook_event_name === "beforeShellExecution")
|
|
16619
17726
|
return cwd;
|