@brainervirus/workit-codex 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/dist/launch-mcp.js +2377 -1254
- package/dist/workit-hook.js +2326 -1181
- package/package.json +3 -3
package/dist/workit-hook.js
CHANGED
|
@@ -26,7 +26,7 @@ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map
|
|
|
26
26
|
};
|
|
27
27
|
});
|
|
28
28
|
// packages/workit-core/src/hooks/context.ts
|
|
29
|
-
import
|
|
29
|
+
import path15 from "node:path";
|
|
30
30
|
|
|
31
31
|
// packages/workit-core/src/core/methods.ts
|
|
32
32
|
var METHODS = {
|
|
@@ -93,7 +93,10 @@ PR readiness, a fresh review, or post-commit assessment/closure paperwork.
|
|
|
93
93
|
|
|
94
94
|
For tracked work, inspect its current state and use the shared operations for
|
|
95
95
|
task, policy, evidence, finding, decision, worker, writer, and state changes.
|
|
96
|
-
|
|
96
|
+
Each branch has one implicit task: an operation without taskId applies to it,
|
|
97
|
+
and the first recording (a note, check, finding or evidence) creates it, so you
|
|
98
|
+
never manage task ids. Start a record only for an explicit tracked objective;
|
|
99
|
+
assess or reassess only
|
|
97
100
|
when policy selection or changed evidence/constraints requires it. Omitted
|
|
98
101
|
expectedRevision and expectedWorkspaceRevision use current values; explicit
|
|
99
102
|
values are still concurrency-checked, so never copy revisions between calls.
|
|
@@ -8507,16 +8510,6 @@ var stateOperations = {
|
|
|
8507
8510
|
expectedWorkspaceRevision: revision.nullable().optional(),
|
|
8508
8511
|
bundle: exportBundleSchema,
|
|
8509
8512
|
authorityRefs: array(refSchema)
|
|
8510
|
-
}),
|
|
8511
|
-
recover: operation({
|
|
8512
|
-
action: literal("recover"),
|
|
8513
|
-
taskId: id.optional(),
|
|
8514
|
-
expectedWorkspaceRevision: revision.optional(),
|
|
8515
|
-
target: _enum(["task", "workspace"]),
|
|
8516
|
-
expectedBytes: digest,
|
|
8517
|
-
snapshotDigest: digest,
|
|
8518
|
-
reason: text,
|
|
8519
|
-
authorityRefs: array(refSchema)
|
|
8520
8513
|
})
|
|
8521
8514
|
};
|
|
8522
8515
|
var operationSchemas = {
|
|
@@ -8529,10 +8522,17 @@ var operationSchemas = {
|
|
|
8529
8522
|
writer: discriminatedUnion("action", Object.values(writerOperations)),
|
|
8530
8523
|
state: discriminatedUnion("action", Object.values(stateOperations))
|
|
8531
8524
|
};
|
|
8532
|
-
var {
|
|
8525
|
+
var withImplicitTask = (options) => Object.values(options).map((option) => ("taskId" in option.shape) ? option.extend({ taskId: id.optional() }) : option);
|
|
8526
|
+
var advertised = (options) => discriminatedUnion("action", withImplicitTask(options));
|
|
8533
8527
|
var advertisedOperationSchemas = {
|
|
8534
|
-
|
|
8535
|
-
|
|
8528
|
+
task: advertised(taskOperations),
|
|
8529
|
+
policy: advertised(policyOperations),
|
|
8530
|
+
evidence: advertised(evidenceOperations),
|
|
8531
|
+
finding: advertised(findingOperations),
|
|
8532
|
+
decision: advertised(decisionOperations),
|
|
8533
|
+
worker: advertised(workerOperations),
|
|
8534
|
+
writer: advertised(writerOperations),
|
|
8535
|
+
state: advertised(stateOperations)
|
|
8536
8536
|
};
|
|
8537
8537
|
var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
|
|
8538
8538
|
var success = (revisionValue, workspaceRevisionValue, data) => ({
|
|
@@ -8651,20 +8651,20 @@ function requirementId(input) {
|
|
|
8651
8651
|
}
|
|
8652
8652
|
|
|
8653
8653
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
8654
|
-
import { createHash as
|
|
8655
|
-
import * as
|
|
8656
|
-
import { spawnSync as
|
|
8657
|
-
import
|
|
8654
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
8655
|
+
import * as fs12 from "node:fs";
|
|
8656
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
8657
|
+
import path14 from "node:path";
|
|
8658
8658
|
|
|
8659
8659
|
// packages/workit-core/src/core/authority.ts
|
|
8660
|
-
import { createHash as
|
|
8661
|
-
import * as
|
|
8662
|
-
import
|
|
8660
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
8661
|
+
import * as fs9 from "node:fs";
|
|
8662
|
+
import path11 from "node:path";
|
|
8663
8663
|
|
|
8664
8664
|
// packages/workit-core/src/core/auto-approval.ts
|
|
8665
|
-
import { createHash as
|
|
8665
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
8666
8666
|
import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
8667
|
-
import
|
|
8667
|
+
import path10 from "node:path";
|
|
8668
8668
|
|
|
8669
8669
|
// packages/workit-core/src/core/workspaces.ts
|
|
8670
8670
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -9478,24 +9478,10 @@ var verifyPushIdentity = (root, provider, account) => {
|
|
|
9478
9478
|
};
|
|
9479
9479
|
|
|
9480
9480
|
// packages/workit-core/src/core/task-store.ts
|
|
9481
|
-
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
9482
9481
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9483
|
-
import
|
|
9484
|
-
import
|
|
9485
|
-
|
|
9486
|
-
// packages/workit-core/src/core/package-root.ts
|
|
9487
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
9488
|
-
import path4 from "node:path";
|
|
9489
|
-
import { fileURLToPath } from "node:url";
|
|
9490
|
-
var packageRoot = () => {
|
|
9491
|
-
let dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
9492
|
-
while (true) {
|
|
9493
|
-
const parent = path4.dirname(dir);
|
|
9494
|
-
if (existsSync2(path4.join(dir, "package.json")) || parent === dir)
|
|
9495
|
-
return dir;
|
|
9496
|
-
dir = parent;
|
|
9497
|
-
}
|
|
9498
|
-
};
|
|
9482
|
+
import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
|
|
9483
|
+
import * as fs8 from "node:fs";
|
|
9484
|
+
import path9 from "node:path";
|
|
9499
9485
|
|
|
9500
9486
|
// node_modules/@openclaw/fs-safe/dist/errors.js
|
|
9501
9487
|
var OPERATIONAL_CODES = new Set([
|
|
@@ -9546,7 +9532,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
|
|
|
9546
9532
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
9547
9533
|
import { randomBytes } from "node:crypto";
|
|
9548
9534
|
import fsSync from "node:fs";
|
|
9549
|
-
import
|
|
9535
|
+
import path4 from "node:path";
|
|
9550
9536
|
|
|
9551
9537
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
9552
9538
|
import fs2 from "node:fs";
|
|
@@ -9636,13 +9622,13 @@ ${ownershipToken}
|
|
|
9636
9622
|
return { raw, ownershipToken };
|
|
9637
9623
|
}
|
|
9638
9624
|
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
9639
|
-
const resolved =
|
|
9640
|
-
const lexicalRelative =
|
|
9641
|
-
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${
|
|
9642
|
-
if (!relative || relative === ".." || relative.startsWith(`..${
|
|
9625
|
+
const resolved = path4.resolve(lockPath);
|
|
9626
|
+
const lexicalRelative = path4.relative(lockRoot.rootDir, resolved);
|
|
9627
|
+
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(lexicalRelative) ? lexicalRelative : path4.relative(lockRoot.rootReal, resolved);
|
|
9628
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path4.sep}`) || path4.isAbsolute(relative)) {
|
|
9643
9629
|
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
9644
9630
|
}
|
|
9645
|
-
return relative.split(
|
|
9631
|
+
return relative.split(path4.sep).join(path4.posix.sep);
|
|
9646
9632
|
}
|
|
9647
9633
|
function parseSidecarLockPayload(raw, parser) {
|
|
9648
9634
|
if (parser) {
|
|
@@ -9789,7 +9775,7 @@ function getFsSafeLockConfig() {
|
|
|
9789
9775
|
|
|
9790
9776
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
9791
9777
|
import fs4 from "node:fs";
|
|
9792
|
-
import
|
|
9778
|
+
import path5 from "node:path";
|
|
9793
9779
|
|
|
9794
9780
|
// node_modules/@openclaw/fs-safe/dist/timing.js
|
|
9795
9781
|
function sleepSync(ms) {
|
|
@@ -9880,26 +9866,26 @@ function canonicalLockParentSync(parent) {
|
|
|
9880
9866
|
return process.platform === "win32" ? fs4.realpathSync.native(parent) : fs4.realpathSync(parent);
|
|
9881
9867
|
}
|
|
9882
9868
|
function normalizeTargetPath(targetPath) {
|
|
9883
|
-
const resolved =
|
|
9884
|
-
fs4.mkdirSync(
|
|
9869
|
+
const resolved = path5.resolve(targetPath);
|
|
9870
|
+
fs4.mkdirSync(path5.dirname(resolved), { recursive: true });
|
|
9885
9871
|
try {
|
|
9886
|
-
return
|
|
9872
|
+
return path5.join(canonicalLockParentSync(path5.dirname(resolved)), path5.basename(resolved));
|
|
9887
9873
|
} catch {
|
|
9888
9874
|
return resolved;
|
|
9889
9875
|
}
|
|
9890
9876
|
}
|
|
9891
9877
|
function boundedLockPath(lockPath, lockRoot) {
|
|
9892
|
-
const resolved =
|
|
9878
|
+
const resolved = path5.resolve(lockPath);
|
|
9893
9879
|
if (!lockRoot)
|
|
9894
9880
|
return resolved;
|
|
9895
9881
|
relativeSidecarLockPath(lockRoot, resolved);
|
|
9896
|
-
const parent =
|
|
9882
|
+
const parent = path5.dirname(resolved);
|
|
9897
9883
|
const parentReal = canonicalLockParentSync(parent);
|
|
9898
|
-
const parentRelative =
|
|
9899
|
-
if (parentRelative === ".." || parentRelative.startsWith(`..${
|
|
9884
|
+
const parentRelative = path5.relative(lockRoot.rootReal, parentReal);
|
|
9885
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path5.sep}`) || path5.isAbsolute(parentRelative)) {
|
|
9900
9886
|
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
9901
9887
|
}
|
|
9902
|
-
return
|
|
9888
|
+
return path5.join(parentReal, path5.basename(resolved));
|
|
9903
9889
|
}
|
|
9904
9890
|
function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
9905
9891
|
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
@@ -10098,10 +10084,306 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
10098
10084
|
}
|
|
10099
10085
|
}
|
|
10100
10086
|
}
|
|
10087
|
+
// packages/workit-core/src/core/package-root.ts
|
|
10088
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
10089
|
+
import path6 from "node:path";
|
|
10090
|
+
import { fileURLToPath } from "node:url";
|
|
10091
|
+
var packageRoot = () => {
|
|
10092
|
+
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
10093
|
+
while (true) {
|
|
10094
|
+
const parent = path6.dirname(dir);
|
|
10095
|
+
if (existsSync2(path6.join(dir, "package.json")) || parent === dir)
|
|
10096
|
+
return dir;
|
|
10097
|
+
dir = parent;
|
|
10098
|
+
}
|
|
10099
|
+
};
|
|
10100
|
+
|
|
10101
10101
|
// packages/workit-core/src/core/store-lock.ts
|
|
10102
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
10103
|
+
import fs6 from "node:fs";
|
|
10104
|
+
import { hostname } from "node:os";
|
|
10105
|
+
|
|
10106
|
+
// packages/workit-core/src/store/paths.ts
|
|
10102
10107
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
10108
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
10103
10109
|
import fs5 from "node:fs";
|
|
10104
|
-
import
|
|
10110
|
+
import path7 from "node:path";
|
|
10111
|
+
var NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
|
|
10112
|
+
var gitRun = (cwd, args) => {
|
|
10113
|
+
const run = spawnSync2("git", args, {
|
|
10114
|
+
cwd,
|
|
10115
|
+
encoding: "utf8",
|
|
10116
|
+
timeout: 20000,
|
|
10117
|
+
killSignal: "SIGKILL",
|
|
10118
|
+
windowsHide: true,
|
|
10119
|
+
env: {
|
|
10120
|
+
...process.env,
|
|
10121
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
10122
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
10123
|
+
LC_ALL: "C",
|
|
10124
|
+
LANGUAGE: "C"
|
|
10125
|
+
}
|
|
10126
|
+
});
|
|
10127
|
+
const error = run.error;
|
|
10128
|
+
return {
|
|
10129
|
+
ok: run.status === 0,
|
|
10130
|
+
stdout: run.stdout ?? "",
|
|
10131
|
+
stderr: error ? String(error.message) : run.stderr ?? "",
|
|
10132
|
+
missing: error?.code === "ENOENT"
|
|
10133
|
+
};
|
|
10134
|
+
};
|
|
10135
|
+
var shortHash = (value) => createHash4("sha256").update(value).digest("hex").slice(0, 12);
|
|
10136
|
+
var real = (file) => {
|
|
10137
|
+
try {
|
|
10138
|
+
return fs5.realpathSync.native(file);
|
|
10139
|
+
} catch {
|
|
10140
|
+
return file;
|
|
10141
|
+
}
|
|
10142
|
+
};
|
|
10143
|
+
var DISCOVERY_ENV = [
|
|
10144
|
+
"GIT_DIR",
|
|
10145
|
+
"GIT_COMMON_DIR",
|
|
10146
|
+
"GIT_WORK_TREE",
|
|
10147
|
+
"GIT_CEILING_DIRECTORIES",
|
|
10148
|
+
"GIT_DISCOVERY_ACROSS_FILESYSTEM"
|
|
10149
|
+
];
|
|
10150
|
+
var isGitDir = (dir) => {
|
|
10151
|
+
try {
|
|
10152
|
+
return fs5.statSync(path7.join(dir, "HEAD")).isFile() && fs5.statSync(path7.join(dir, "objects")).isDirectory() && fs5.statSync(path7.join(dir, "refs")).isDirectory();
|
|
10153
|
+
} catch {
|
|
10154
|
+
return false;
|
|
10155
|
+
}
|
|
10156
|
+
};
|
|
10157
|
+
var reftableRefs = (common) => {
|
|
10158
|
+
try {
|
|
10159
|
+
return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs5.readFileSync(path7.join(common, "config"), "utf8"));
|
|
10160
|
+
} catch {
|
|
10161
|
+
return false;
|
|
10162
|
+
}
|
|
10163
|
+
};
|
|
10164
|
+
function fastGit(root) {
|
|
10165
|
+
if (DISCOVERY_ENV.some((name) => process.env[name]))
|
|
10166
|
+
return null;
|
|
10167
|
+
let dir = real(root);
|
|
10168
|
+
let device = null;
|
|
10169
|
+
for (;; ) {
|
|
10170
|
+
let stat;
|
|
10171
|
+
try {
|
|
10172
|
+
stat = fs5.statSync(dir);
|
|
10173
|
+
} catch {
|
|
10174
|
+
return null;
|
|
10175
|
+
}
|
|
10176
|
+
if (device !== null && stat.dev !== device)
|
|
10177
|
+
return { kind: "none" };
|
|
10178
|
+
device = stat.dev;
|
|
10179
|
+
const dotGit = path7.join(dir, ".git");
|
|
10180
|
+
let entry = null;
|
|
10181
|
+
try {
|
|
10182
|
+
entry = fs5.statSync(dotGit);
|
|
10183
|
+
} catch {}
|
|
10184
|
+
let gitDir = null;
|
|
10185
|
+
let top = null;
|
|
10186
|
+
if (entry) {
|
|
10187
|
+
top = dir;
|
|
10188
|
+
if (entry.isDirectory())
|
|
10189
|
+
gitDir = dotGit;
|
|
10190
|
+
else if (entry.isFile()) {
|
|
10191
|
+
let text = "";
|
|
10192
|
+
try {
|
|
10193
|
+
text = fs5.readFileSync(dotGit, "utf8");
|
|
10194
|
+
} catch {
|
|
10195
|
+
return null;
|
|
10196
|
+
}
|
|
10197
|
+
const match = /^gitdir: (.+)$/mu.exec(text);
|
|
10198
|
+
if (!match)
|
|
10199
|
+
return null;
|
|
10200
|
+
gitDir = path7.resolve(dir, match[1].trim());
|
|
10201
|
+
} else
|
|
10202
|
+
return null;
|
|
10203
|
+
if (!fs5.existsSync(path7.join(gitDir, "HEAD")))
|
|
10204
|
+
return null;
|
|
10205
|
+
} else if (isGitDir(dir))
|
|
10206
|
+
gitDir = dir;
|
|
10207
|
+
if (gitDir) {
|
|
10208
|
+
let common = gitDir;
|
|
10209
|
+
try {
|
|
10210
|
+
common = path7.resolve(gitDir, fs5.readFileSync(path7.join(gitDir, "commondir"), "utf8").trim());
|
|
10211
|
+
} catch {}
|
|
10212
|
+
if (!fs5.existsSync(path7.join(common, "objects")))
|
|
10213
|
+
return null;
|
|
10214
|
+
return {
|
|
10215
|
+
kind: "repo",
|
|
10216
|
+
common: real(common),
|
|
10217
|
+
gitDir: real(gitDir),
|
|
10218
|
+
top: top === null ? null : real(top),
|
|
10219
|
+
reftable: reftableRefs(common)
|
|
10220
|
+
};
|
|
10221
|
+
}
|
|
10222
|
+
const parent = path7.dirname(dir);
|
|
10223
|
+
if (parent === dir)
|
|
10224
|
+
return { kind: "none" };
|
|
10225
|
+
dir = parent;
|
|
10226
|
+
}
|
|
10227
|
+
}
|
|
10228
|
+
function resolveStore(root) {
|
|
10229
|
+
root = path7.resolve(root);
|
|
10230
|
+
let directory = false;
|
|
10231
|
+
try {
|
|
10232
|
+
directory = fs5.statSync(root).isDirectory();
|
|
10233
|
+
} catch {}
|
|
10234
|
+
if (!directory)
|
|
10235
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10236
|
+
const fast = fastGit(root);
|
|
10237
|
+
if (fast?.kind === "none")
|
|
10238
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10239
|
+
if (fast?.kind === "repo")
|
|
10240
|
+
return {
|
|
10241
|
+
dir: path7.join(fast.common, "workit"),
|
|
10242
|
+
shared: true,
|
|
10243
|
+
top: fast.top,
|
|
10244
|
+
git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
|
|
10245
|
+
};
|
|
10246
|
+
const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
10247
|
+
const common = run.stdout.trim();
|
|
10248
|
+
if (run.ok && common) {
|
|
10249
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
10250
|
+
return {
|
|
10251
|
+
dir: path7.join(real(path7.resolve(root, common)), "workit"),
|
|
10252
|
+
shared: true,
|
|
10253
|
+
top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
|
|
10254
|
+
};
|
|
10255
|
+
}
|
|
10256
|
+
if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
|
|
10257
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10258
|
+
return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
|
|
10259
|
+
`)[0] || "no output"}); fix the repository (git status should work here)`);
|
|
10260
|
+
}
|
|
10261
|
+
var rebasingBranch = (gitDir) => {
|
|
10262
|
+
for (const state of ["rebase-merge", "rebase-apply"]) {
|
|
10263
|
+
try {
|
|
10264
|
+
const ref = fs5.readFileSync(path7.join(gitDir, state, "head-name"), "utf8").trim();
|
|
10265
|
+
const match = /^refs\/heads\/(.+)$/u.exec(ref);
|
|
10266
|
+
if (match)
|
|
10267
|
+
return match[1];
|
|
10268
|
+
} catch {}
|
|
10269
|
+
}
|
|
10270
|
+
return null;
|
|
10271
|
+
};
|
|
10272
|
+
function resolveTaskKey(root, location) {
|
|
10273
|
+
if (!location.shared)
|
|
10274
|
+
return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
|
|
10275
|
+
const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
|
|
10276
|
+
if (location.git && !location.git.reftable) {
|
|
10277
|
+
let head = "";
|
|
10278
|
+
try {
|
|
10279
|
+
head = fs5.readFileSync(path7.join(location.git.gitDir, "HEAD"), "utf8").trim();
|
|
10280
|
+
} catch {}
|
|
10281
|
+
const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
|
|
10282
|
+
if (ref && ref[1] !== ".invalid")
|
|
10283
|
+
return branchKey(ref[1]);
|
|
10284
|
+
if (/^[0-9a-f]{40,64}$/u.test(head)) {
|
|
10285
|
+
const rebasing = rebasingBranch(location.git.gitDir);
|
|
10286
|
+
if (rebasing)
|
|
10287
|
+
return branchKey(rebasing);
|
|
10288
|
+
return {
|
|
10289
|
+
key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
|
|
10290
|
+
kind: "detached",
|
|
10291
|
+
branch: null
|
|
10292
|
+
};
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10295
|
+
const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
|
|
10296
|
+
const name = branch.ok ? branch.stdout.trim() : "";
|
|
10297
|
+
if (name && name !== ".invalid")
|
|
10298
|
+
return branchKey(name);
|
|
10299
|
+
const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
|
|
10300
|
+
if (gitDir.ok && gitDir.stdout.trim()) {
|
|
10301
|
+
const rebasing = rebasingBranch(gitDir.stdout.trim());
|
|
10302
|
+
if (rebasing)
|
|
10303
|
+
return branchKey(rebasing);
|
|
10304
|
+
}
|
|
10305
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
10306
|
+
const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
|
|
10307
|
+
return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
|
|
10308
|
+
}
|
|
10309
|
+
function checkoutRootOf(root, location) {
|
|
10310
|
+
const resolved = path7.resolve(root);
|
|
10311
|
+
let canonical = resolved;
|
|
10312
|
+
try {
|
|
10313
|
+
canonical = fs5.realpathSync(resolved);
|
|
10314
|
+
} catch {
|
|
10315
|
+
return resolved;
|
|
10316
|
+
}
|
|
10317
|
+
const found = location ?? resolveStore(canonical);
|
|
10318
|
+
if (found instanceof Error || !found.shared || !found.top)
|
|
10319
|
+
return canonical;
|
|
10320
|
+
if (real(canonical) === found.top)
|
|
10321
|
+
return canonical;
|
|
10322
|
+
try {
|
|
10323
|
+
return fs5.realpathSync(found.top);
|
|
10324
|
+
} catch {
|
|
10325
|
+
return canonical;
|
|
10326
|
+
}
|
|
10327
|
+
}
|
|
10328
|
+
function branchRenames(location) {
|
|
10329
|
+
if (!location.shared)
|
|
10330
|
+
return [];
|
|
10331
|
+
const files = [];
|
|
10332
|
+
const walk = (dir) => {
|
|
10333
|
+
let entries;
|
|
10334
|
+
try {
|
|
10335
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
10336
|
+
} catch {
|
|
10337
|
+
return;
|
|
10338
|
+
}
|
|
10339
|
+
for (const entry of entries)
|
|
10340
|
+
if (entry.isDirectory())
|
|
10341
|
+
walk(path7.join(dir, entry.name));
|
|
10342
|
+
else
|
|
10343
|
+
files.push(path7.join(dir, entry.name));
|
|
10344
|
+
};
|
|
10345
|
+
walk(path7.join(path7.dirname(location.dir), "logs", "refs", "heads"));
|
|
10346
|
+
if (location.git)
|
|
10347
|
+
files.push(path7.join(location.git.gitDir, "logs", "HEAD"));
|
|
10348
|
+
const seen = new Set;
|
|
10349
|
+
const renames = [];
|
|
10350
|
+
for (const file of files) {
|
|
10351
|
+
let text = "";
|
|
10352
|
+
try {
|
|
10353
|
+
text = fs5.readFileSync(file, "utf8");
|
|
10354
|
+
} catch {
|
|
10355
|
+
continue;
|
|
10356
|
+
}
|
|
10357
|
+
for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
|
|
10358
|
+
const rename = { from: match[2], to: match[3], at: Number(match[1]) };
|
|
10359
|
+
const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
|
|
10360
|
+
if (seen.has(id))
|
|
10361
|
+
continue;
|
|
10362
|
+
seen.add(id);
|
|
10363
|
+
renames.push(rename);
|
|
10364
|
+
}
|
|
10365
|
+
}
|
|
10366
|
+
return renames.toSorted((left, right) => left.at - right.at);
|
|
10367
|
+
}
|
|
10368
|
+
function followRenames(key, boundAt, renames) {
|
|
10369
|
+
let current = key;
|
|
10370
|
+
let since = Math.floor(Date.parse(boundAt) / 1000);
|
|
10371
|
+
for (const rename of renames)
|
|
10372
|
+
if (rename.from === current && rename.at >= since) {
|
|
10373
|
+
current = rename.to;
|
|
10374
|
+
since = rename.at;
|
|
10375
|
+
}
|
|
10376
|
+
return current;
|
|
10377
|
+
}
|
|
10378
|
+
var checkoutSlug = (root) => {
|
|
10379
|
+
let canonical = real(root);
|
|
10380
|
+
if (process.platform === "win32")
|
|
10381
|
+
canonical = canonical.toLowerCase();
|
|
10382
|
+
const base = path7.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
|
|
10383
|
+
return `${base}-${shortHash(canonical)}`;
|
|
10384
|
+
};
|
|
10385
|
+
|
|
10386
|
+
// packages/workit-core/src/core/store-lock.ts
|
|
10105
10387
|
var metadataLockSchema = object({
|
|
10106
10388
|
pid: number2().int().nonnegative().safe(),
|
|
10107
10389
|
processStart: string2().nullable(),
|
|
@@ -10133,10 +10415,6 @@ var parseMetadataLockOrNull = (raw) => {
|
|
|
10133
10415
|
return null;
|
|
10134
10416
|
}
|
|
10135
10417
|
};
|
|
10136
|
-
var sameMetadataLock = (left, right) => {
|
|
10137
|
-
const parsed = metadataLockSchema.safeParse(left);
|
|
10138
|
-
return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
|
|
10139
|
-
};
|
|
10140
10418
|
var parseProcStatStart = (stat) => {
|
|
10141
10419
|
const close = stat.lastIndexOf(")");
|
|
10142
10420
|
if (close < 0)
|
|
@@ -10146,14 +10424,14 @@ var parseProcStatStart = (stat) => {
|
|
|
10146
10424
|
var processStartOf = (pid) => {
|
|
10147
10425
|
if (process.platform === "linux") {
|
|
10148
10426
|
try {
|
|
10149
|
-
return parseProcStatStart(
|
|
10427
|
+
return parseProcStatStart(fs6.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
10150
10428
|
} catch {
|
|
10151
10429
|
return null;
|
|
10152
10430
|
}
|
|
10153
10431
|
}
|
|
10154
10432
|
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
10155
10433
|
try {
|
|
10156
|
-
const run =
|
|
10434
|
+
const run = spawnSync3("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
10157
10435
|
encoding: "utf8",
|
|
10158
10436
|
timeout: 1000
|
|
10159
10437
|
});
|
|
@@ -10167,7 +10445,7 @@ var processStartOf = (pid) => {
|
|
|
10167
10445
|
};
|
|
10168
10446
|
var readTrimmed = (file) => {
|
|
10169
10447
|
try {
|
|
10170
|
-
return
|
|
10448
|
+
return fs6.readFileSync(file, "utf8").trim() || null;
|
|
10171
10449
|
} catch {
|
|
10172
10450
|
return null;
|
|
10173
10451
|
}
|
|
@@ -10178,7 +10456,7 @@ var localLockHost = () => {
|
|
|
10178
10456
|
return cachedLockHost;
|
|
10179
10457
|
let pidns = null;
|
|
10180
10458
|
try {
|
|
10181
|
-
pidns = /\[(\d+)\]/.exec(
|
|
10459
|
+
pidns = /\[(\d+)\]/.exec(fs6.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
|
|
10182
10460
|
} catch {}
|
|
10183
10461
|
const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
10184
10462
|
cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
|
|
@@ -10218,7 +10496,7 @@ var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
|
|
|
10218
10496
|
};
|
|
10219
10497
|
var ageOf = (file, nowMs) => {
|
|
10220
10498
|
try {
|
|
10221
|
-
return nowMs -
|
|
10499
|
+
return nowMs - fs6.lstatSync(file).mtimeMs;
|
|
10222
10500
|
} catch {
|
|
10223
10501
|
return null;
|
|
10224
10502
|
}
|
|
@@ -10229,118 +10507,152 @@ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
|
|
|
10229
10507
|
if (age === null || age <= RECLAIM_GUARD_TTL_MS)
|
|
10230
10508
|
return false;
|
|
10231
10509
|
try {
|
|
10232
|
-
|
|
10510
|
+
fs6.rmdirSync(guard);
|
|
10233
10511
|
return true;
|
|
10234
10512
|
} catch {
|
|
10235
10513
|
return false;
|
|
10236
10514
|
}
|
|
10237
10515
|
};
|
|
10238
10516
|
|
|
10239
|
-
// packages/workit-core/src/
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
var
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
10251
|
-
var indexEntry = (task, file) => {
|
|
10252
|
-
const sessions = [];
|
|
10253
|
-
const intent = hostSession(task.intent.provenance.session);
|
|
10254
|
-
if (intent)
|
|
10255
|
-
sessions.push({ ...intent, workerId: null });
|
|
10256
|
-
for (const worker of task.workers) {
|
|
10257
|
-
const session = hostSession(worker.data.session);
|
|
10258
|
-
if (session)
|
|
10259
|
-
sessions.push({ ...session, workerId: worker.id });
|
|
10517
|
+
// packages/workit-core/src/store/event-log.ts
|
|
10518
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10519
|
+
import fs7 from "node:fs";
|
|
10520
|
+
import path8 from "node:path";
|
|
10521
|
+
var EVENT_VERSION = 1;
|
|
10522
|
+
|
|
10523
|
+
class LogDamage extends Error {
|
|
10524
|
+
upgrade;
|
|
10525
|
+
constructor(message, upgrade = false) {
|
|
10526
|
+
super(message);
|
|
10527
|
+
this.upgrade = upgrade;
|
|
10260
10528
|
}
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
createdAt: task.createdAt,
|
|
10266
|
-
updatedAt: task.updatedAt,
|
|
10267
|
-
objective: task.intent.data.objective,
|
|
10268
|
-
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
10269
|
-
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
10270
|
-
sessions,
|
|
10271
|
-
file
|
|
10272
|
-
};
|
|
10273
|
-
};
|
|
10274
|
-
var fileSignature = (file) => {
|
|
10529
|
+
}
|
|
10530
|
+
var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10531
|
+
var parseEvent = (line, file, offset) => {
|
|
10532
|
+
let value;
|
|
10275
10533
|
try {
|
|
10276
|
-
|
|
10277
|
-
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
10534
|
+
value = JSON.parse(line);
|
|
10278
10535
|
} catch {
|
|
10279
|
-
|
|
10280
|
-
}
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10536
|
+
throw new LogDamage(`${file}: unreadable event at byte ${offset}`);
|
|
10537
|
+
}
|
|
10538
|
+
if (!isObject2(value) || typeof value.v !== "number")
|
|
10539
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
10540
|
+
if (value.v > EVENT_VERSION)
|
|
10541
|
+
throw new LogDamage(`${file}: event format v${value.v} was written by a newer Workit; upgrade Workit to read this task`, true);
|
|
10542
|
+
if (typeof value.seq !== "number" || !Number.isInteger(value.seq) || typeof value.type !== "string" || typeof value.id !== "string" || typeof value.task !== "string" || !isObject2(value.data))
|
|
10543
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
10544
|
+
return value;
|
|
10287
10545
|
};
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10546
|
+
function readLog(file, from = 0, expectSeq = null) {
|
|
10547
|
+
let fd;
|
|
10548
|
+
try {
|
|
10549
|
+
fd = fs7.openSync(file, "r");
|
|
10550
|
+
} catch (error) {
|
|
10551
|
+
if (error.code === "ENOENT")
|
|
10552
|
+
return null;
|
|
10553
|
+
throw error;
|
|
10554
|
+
}
|
|
10555
|
+
let bytes;
|
|
10556
|
+
let size;
|
|
10557
|
+
try {
|
|
10558
|
+
size = fs7.fstatSync(fd).size;
|
|
10559
|
+
if (from > size)
|
|
10560
|
+
throw new LogDamage(`${file}: shorter than its snapshot`);
|
|
10561
|
+
bytes = Buffer.alloc(size - from);
|
|
10562
|
+
let read = 0;
|
|
10563
|
+
while (read < bytes.length) {
|
|
10564
|
+
const got = fs7.readSync(fd, bytes, read, bytes.length - read, from + read);
|
|
10565
|
+
if (got === 0)
|
|
10566
|
+
break;
|
|
10567
|
+
read += got;
|
|
10568
|
+
}
|
|
10569
|
+
bytes = bytes.subarray(0, read);
|
|
10570
|
+
size = from + read;
|
|
10571
|
+
} finally {
|
|
10572
|
+
fs7.closeSync(fd);
|
|
10573
|
+
}
|
|
10574
|
+
const events = [];
|
|
10575
|
+
let start = 0;
|
|
10576
|
+
let lastStart = from;
|
|
10577
|
+
let seq = expectSeq;
|
|
10578
|
+
for (;; ) {
|
|
10579
|
+
const newline = bytes.indexOf(10, start);
|
|
10580
|
+
if (newline < 0)
|
|
10581
|
+
break;
|
|
10582
|
+
const line = bytes.subarray(start, newline).toString("utf8");
|
|
10583
|
+
if (line.trim() !== "") {
|
|
10584
|
+
const event = parseEvent(line, file, from + start);
|
|
10585
|
+
if (seq !== null && event.seq !== seq)
|
|
10586
|
+
throw new LogDamage(`${file}: event seq ${event.seq} where ${seq} was expected`);
|
|
10587
|
+
seq = event.seq + 1;
|
|
10588
|
+
events.push(event);
|
|
10589
|
+
lastStart = from + start;
|
|
10590
|
+
}
|
|
10591
|
+
start = newline + 1;
|
|
10592
|
+
}
|
|
10593
|
+
return { events, end: from + start, lastStart, size };
|
|
10594
|
+
}
|
|
10595
|
+
var fsyncDirectory = (dir) => {
|
|
10596
|
+
try {
|
|
10597
|
+
const fd = fs7.openSync(dir, "r");
|
|
10292
10598
|
try {
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
cachedRuntimeVersion = "0.0.0";
|
|
10599
|
+
fs7.fsyncSync(fd);
|
|
10600
|
+
} finally {
|
|
10601
|
+
fs7.closeSync(fd);
|
|
10297
10602
|
}
|
|
10603
|
+
} catch (error) {
|
|
10604
|
+
if (process.platform !== "win32")
|
|
10605
|
+
throw error;
|
|
10298
10606
|
}
|
|
10299
|
-
return cachedRuntimeVersion;
|
|
10300
10607
|
};
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10311
|
-
|
|
10312
|
-
|
|
10608
|
+
function appendEvent(file, event, validEnd) {
|
|
10609
|
+
const line = Buffer.from(`${JSON.stringify(event)}
|
|
10610
|
+
`, "utf8");
|
|
10611
|
+
const created = !fs7.existsSync(file);
|
|
10612
|
+
if (!created && fs7.statSync(file).size !== validEnd) {
|
|
10613
|
+
const cut = fs7.openSync(file, "r+");
|
|
10614
|
+
try {
|
|
10615
|
+
fs7.ftruncateSync(cut, validEnd);
|
|
10616
|
+
fs7.fsyncSync(cut);
|
|
10617
|
+
} finally {
|
|
10618
|
+
fs7.closeSync(cut);
|
|
10619
|
+
}
|
|
10313
10620
|
}
|
|
10314
|
-
|
|
10315
|
-
};
|
|
10316
|
-
var jsonBytes = (value) => `${canonicalJson(value)}
|
|
10317
|
-
`;
|
|
10318
|
-
var digestBytes = (value) => createHash4("sha256").update(value).digest("hex");
|
|
10319
|
-
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
10320
|
-
var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
10321
|
-
var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
|
|
10322
|
-
var sameDirectoryIdentity = (left, right) => {
|
|
10323
|
-
if (!path7.isAbsolute(left) || !path7.isAbsolute(right))
|
|
10324
|
-
return false;
|
|
10325
|
-
const normalizedLeft = path7.resolve(left);
|
|
10326
|
-
const normalizedRight = path7.resolve(right);
|
|
10621
|
+
const fd = fs7.openSync(file, "a", 384);
|
|
10327
10622
|
try {
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
const canonicalLeft = fs6.realpathSync(normalizedLeft);
|
|
10335
|
-
const canonicalRight = fs6.realpathSync(normalizedRight);
|
|
10336
|
-
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
10337
|
-
return true;
|
|
10338
|
-
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
10339
|
-
} catch {
|
|
10340
|
-
return false;
|
|
10623
|
+
let written = 0;
|
|
10624
|
+
while (written < line.length)
|
|
10625
|
+
written += fs7.writeSync(fd, line, written, line.length - written);
|
|
10626
|
+
fs7.fsyncSync(fd);
|
|
10627
|
+
} finally {
|
|
10628
|
+
fs7.closeSync(fd);
|
|
10341
10629
|
}
|
|
10342
|
-
|
|
10343
|
-
|
|
10630
|
+
if (created)
|
|
10631
|
+
fsyncDirectory(path8.dirname(file));
|
|
10632
|
+
return validEnd;
|
|
10633
|
+
}
|
|
10634
|
+
function replaceFile(file, bytes, durable) {
|
|
10635
|
+
const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
10636
|
+
try {
|
|
10637
|
+
const fd = fs7.openSync(temporary, "wx", 384);
|
|
10638
|
+
try {
|
|
10639
|
+
fs7.writeSync(fd, bytes);
|
|
10640
|
+
if (durable)
|
|
10641
|
+
fs7.fsyncSync(fd);
|
|
10642
|
+
} finally {
|
|
10643
|
+
fs7.closeSync(fd);
|
|
10644
|
+
}
|
|
10645
|
+
renameWithRetry(temporary, file);
|
|
10646
|
+
if (durable)
|
|
10647
|
+
fsyncDirectory(path8.dirname(file));
|
|
10648
|
+
} catch (error) {
|
|
10649
|
+
try {
|
|
10650
|
+
fs7.unlinkSync(temporary);
|
|
10651
|
+
} catch {}
|
|
10652
|
+
throw error;
|
|
10653
|
+
}
|
|
10654
|
+
}
|
|
10655
|
+
var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
10344
10656
|
var isTransientWindowsError = (error) => {
|
|
10345
10657
|
if (process.platform !== "win32")
|
|
10346
10658
|
return false;
|
|
@@ -10360,138 +10672,596 @@ var retryTransient = (run) => {
|
|
|
10360
10672
|
}
|
|
10361
10673
|
}
|
|
10362
10674
|
};
|
|
10675
|
+
var renameWithRetry = (from, to) => retryTransient(() => fs7.renameSync(from, to));
|
|
10676
|
+
|
|
10677
|
+
// packages/workit-core/src/store/patch.ts
|
|
10678
|
+
var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10679
|
+
var equal = (left, right) => {
|
|
10680
|
+
if (left === right)
|
|
10681
|
+
return true;
|
|
10682
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null)
|
|
10683
|
+
return false;
|
|
10684
|
+
if (Array.isArray(left) !== Array.isArray(right))
|
|
10685
|
+
return false;
|
|
10686
|
+
if (Array.isArray(left)) {
|
|
10687
|
+
const other = right;
|
|
10688
|
+
return left.length === other.length && left.every((item, index) => equal(item, other[index]));
|
|
10689
|
+
}
|
|
10690
|
+
const leftKeys = Object.keys(left).filter((key) => left[key] !== undefined);
|
|
10691
|
+
const rightKeys = Object.keys(right).filter((key) => right[key] !== undefined);
|
|
10692
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => equal(left[key], right[key]));
|
|
10693
|
+
};
|
|
10694
|
+
function diff(before, after, path = []) {
|
|
10695
|
+
if (equal(before, after))
|
|
10696
|
+
return [];
|
|
10697
|
+
if (isPlainObject2(before) && isPlainObject2(after)) {
|
|
10698
|
+
const ops = [];
|
|
10699
|
+
for (const key of Object.keys(before))
|
|
10700
|
+
if (before[key] !== undefined && after[key] === undefined)
|
|
10701
|
+
ops.push({ op: "del", path: [...path, key] });
|
|
10702
|
+
for (const key of Object.keys(after)) {
|
|
10703
|
+
if (after[key] === undefined)
|
|
10704
|
+
continue;
|
|
10705
|
+
if (before[key] === undefined)
|
|
10706
|
+
ops.push({ op: "set", path: [...path, key], value: after[key] });
|
|
10707
|
+
else
|
|
10708
|
+
ops.push(...diff(before[key], after[key], [...path, key]));
|
|
10709
|
+
}
|
|
10710
|
+
return ops;
|
|
10711
|
+
}
|
|
10712
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
10713
|
+
const ops = [];
|
|
10714
|
+
if (after.length < before.length)
|
|
10715
|
+
ops.push({ op: "cut", path, length: after.length });
|
|
10716
|
+
const shared = Math.min(before.length, after.length);
|
|
10717
|
+
let changed = 0;
|
|
10718
|
+
for (let index = 0;index < shared; index += 1)
|
|
10719
|
+
if (!equal(before[index], after[index]))
|
|
10720
|
+
changed += 1;
|
|
10721
|
+
if (shared > 0 && changed > shared / 2 && changed > 4)
|
|
10722
|
+
return [{ op: "set", path, value: after }];
|
|
10723
|
+
for (let index = 0;index < shared; index += 1)
|
|
10724
|
+
ops.push(...diff(before[index], after[index], [...path, index]));
|
|
10725
|
+
if (after.length > before.length)
|
|
10726
|
+
ops.push({ op: "push", path, values: after.slice(before.length) });
|
|
10727
|
+
return ops;
|
|
10728
|
+
}
|
|
10729
|
+
return [{ op: "set", path, value: after }];
|
|
10730
|
+
}
|
|
10731
|
+
var clone2 = (value) => structuredClone(value);
|
|
10732
|
+
function apply(target, ops) {
|
|
10733
|
+
let root = target;
|
|
10734
|
+
for (const op of ops) {
|
|
10735
|
+
if (op.path.length === 0) {
|
|
10736
|
+
if (op.op === "set")
|
|
10737
|
+
root = clone2(op.value);
|
|
10738
|
+
else if (op.op === "push" && Array.isArray(root))
|
|
10739
|
+
root.push(...clone2(op.values));
|
|
10740
|
+
else if (op.op === "cut" && Array.isArray(root))
|
|
10741
|
+
root.length = op.length;
|
|
10742
|
+
else
|
|
10743
|
+
throw new Error(`patch: ${op.op} at the root is invalid`);
|
|
10744
|
+
continue;
|
|
10745
|
+
}
|
|
10746
|
+
let parent = root;
|
|
10747
|
+
for (const key of op.path.slice(0, -1)) {
|
|
10748
|
+
if (typeof parent !== "object" || parent === null)
|
|
10749
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
10750
|
+
parent = parent[key];
|
|
10751
|
+
}
|
|
10752
|
+
if (typeof parent !== "object" || parent === null)
|
|
10753
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
10754
|
+
const key = op.path.at(-1);
|
|
10755
|
+
const container = parent;
|
|
10756
|
+
if (op.op === "set")
|
|
10757
|
+
container[key] = clone2(op.value);
|
|
10758
|
+
else if (op.op === "del")
|
|
10759
|
+
delete container[key];
|
|
10760
|
+
else {
|
|
10761
|
+
const array = container[key];
|
|
10762
|
+
if (!Array.isArray(array))
|
|
10763
|
+
throw new Error(`patch: ${op.path.join(".")} is not an array`);
|
|
10764
|
+
if (op.op === "push")
|
|
10765
|
+
array.push(...clone2(op.values));
|
|
10766
|
+
else
|
|
10767
|
+
array.length = op.length;
|
|
10768
|
+
}
|
|
10769
|
+
}
|
|
10770
|
+
return root;
|
|
10771
|
+
}
|
|
10772
|
+
var isPath = (value) => Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isInteger(key) && key >= 0);
|
|
10773
|
+
var isPatch = (value) => Array.isArray(value) && value.every((op) => {
|
|
10774
|
+
if (!isPlainObject2(op) || !isPath(op.path))
|
|
10775
|
+
return false;
|
|
10776
|
+
if (op.op === "set")
|
|
10777
|
+
return "value" in op;
|
|
10778
|
+
if (op.op === "del")
|
|
10779
|
+
return op.path.length > 0;
|
|
10780
|
+
if (op.op === "push")
|
|
10781
|
+
return Array.isArray(op.values);
|
|
10782
|
+
if (op.op === "cut")
|
|
10783
|
+
return typeof op.length === "number" && Number.isInteger(op.length) && op.length >= 0;
|
|
10784
|
+
return false;
|
|
10785
|
+
});
|
|
10786
|
+
|
|
10787
|
+
// packages/workit-core/src/store/reduce.ts
|
|
10788
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
10789
|
+
var OPENING_TYPES = new Set([
|
|
10790
|
+
"task.opened",
|
|
10791
|
+
"task.imported",
|
|
10792
|
+
"migrated.from_v2",
|
|
10793
|
+
"task.checkpoint"
|
|
10794
|
+
]);
|
|
10795
|
+
var KNOWN_TYPES = new Set([
|
|
10796
|
+
...OPENING_TYPES,
|
|
10797
|
+
"task.bound",
|
|
10798
|
+
"task.noted",
|
|
10799
|
+
"task.revised",
|
|
10800
|
+
"task.judged",
|
|
10801
|
+
"task.paused",
|
|
10802
|
+
"task.resumed",
|
|
10803
|
+
"task.closed",
|
|
10804
|
+
"task.updated",
|
|
10805
|
+
"check.ran",
|
|
10806
|
+
"evidence.recorded",
|
|
10807
|
+
"finding.recorded",
|
|
10808
|
+
"finding.updated",
|
|
10809
|
+
"decision.recorded",
|
|
10810
|
+
"decision.updated",
|
|
10811
|
+
"worker.updated"
|
|
10812
|
+
]);
|
|
10813
|
+
var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10814
|
+
var isKey = (value) => isObject3(value) && typeof value.key === "string" && (value.kind === "branch" || value.kind === "detached" || value.kind === "dir") && (value.branch === null || typeof value.branch === "string");
|
|
10815
|
+
var BLOB_REF = "$blob";
|
|
10816
|
+
var blobRef = (value) => isObject3(value) && Object.keys(value).length === 1 && typeof value[BLOB_REF] === "string" && /^[0-9a-f]{64}$/.test(value[BLOB_REF]) ? value[BLOB_REF] : null;
|
|
10817
|
+
var blobDigest = (value) => createHash5("sha256").update(JSON.stringify(value)).digest("hex");
|
|
10818
|
+
var toBlob = (value, blobs) => {
|
|
10819
|
+
if (!isObject3(value))
|
|
10820
|
+
return value;
|
|
10821
|
+
const digest = blobDigest(value);
|
|
10822
|
+
blobs.write(digest, value);
|
|
10823
|
+
return { [BLOB_REF]: digest };
|
|
10824
|
+
};
|
|
10825
|
+
var encodeRecord = (record, blobs) => Array.isArray(record.candidates) ? { ...record, candidates: record.candidates.map((item) => toBlob(item, blobs)) } : record;
|
|
10826
|
+
var encodeOps = (ops, blobs) => ops.map((op) => {
|
|
10827
|
+
if (op.path[0] !== "candidates")
|
|
10828
|
+
return op;
|
|
10829
|
+
if (op.op === "push" && op.path.length === 1)
|
|
10830
|
+
return { ...op, values: op.values.map((item) => toBlob(item, blobs)) };
|
|
10831
|
+
if (op.op === "set" && op.path.length === 1 && Array.isArray(op.value))
|
|
10832
|
+
return { ...op, value: op.value.map((item) => toBlob(item, blobs)) };
|
|
10833
|
+
if (op.op === "set" && op.path.length === 2)
|
|
10834
|
+
return { ...op, value: toBlob(op.value, blobs) };
|
|
10835
|
+
return op;
|
|
10836
|
+
});
|
|
10837
|
+
var decode2 = (value, blobs) => {
|
|
10838
|
+
const digest = blobRef(value);
|
|
10839
|
+
if (digest)
|
|
10840
|
+
return blobs.read(digest);
|
|
10841
|
+
if (Array.isArray(value))
|
|
10842
|
+
return value.map((item) => decode2(item, blobs));
|
|
10843
|
+
if (isObject3(value)) {
|
|
10844
|
+
const out = {};
|
|
10845
|
+
for (const [key, item] of Object.entries(value))
|
|
10846
|
+
out[key] = decode2(item, blobs);
|
|
10847
|
+
return out;
|
|
10848
|
+
}
|
|
10849
|
+
return value;
|
|
10850
|
+
};
|
|
10851
|
+
function reduce(state, event, blobs) {
|
|
10852
|
+
const data = event.data;
|
|
10853
|
+
const known = KNOWN_TYPES.has(event.type);
|
|
10854
|
+
const carries = "record" in data || "ops" in data || "key" in data;
|
|
10855
|
+
if (!known && !carries) {
|
|
10856
|
+
if (event.critical === true)
|
|
10857
|
+
throw new LogDamage(`event ${event.type} was written by a newer Workit and must be understood; upgrade Workit to read this task`, true);
|
|
10858
|
+
return state;
|
|
10859
|
+
}
|
|
10860
|
+
let next = state;
|
|
10861
|
+
if ("record" in data) {
|
|
10862
|
+
if (!isObject3(data.record))
|
|
10863
|
+
throw new LogDamage(`event ${event.seq} carries a malformed record`);
|
|
10864
|
+
next = {
|
|
10865
|
+
record: decode2(data.record, blobs),
|
|
10866
|
+
key: null,
|
|
10867
|
+
legacy: null
|
|
10868
|
+
};
|
|
10869
|
+
} else if (next === null) {
|
|
10870
|
+
throw new LogDamage(`the log does not start with an opening event (found ${event.type})`);
|
|
10871
|
+
} else
|
|
10872
|
+
next = { ...next };
|
|
10873
|
+
if ("ops" in data) {
|
|
10874
|
+
if (!isPatch(data.ops))
|
|
10875
|
+
throw new LogDamage(`event ${event.seq} carries a malformed patch`);
|
|
10876
|
+
try {
|
|
10877
|
+
next.record = apply(next.record, decode2(data.ops, blobs));
|
|
10878
|
+
} catch (error) {
|
|
10879
|
+
throw new LogDamage(`event ${event.seq} does not apply: ${String(error)}`);
|
|
10880
|
+
}
|
|
10881
|
+
}
|
|
10882
|
+
if ("key" in data)
|
|
10883
|
+
next.key = isKey(data.key) ? data.key : null;
|
|
10884
|
+
if ("legacy" in data && isObject3(data.legacy))
|
|
10885
|
+
next.legacy = {
|
|
10886
|
+
path: String(data.legacy.path ?? ""),
|
|
10887
|
+
digest: String(data.legacy.digest ?? "")
|
|
10888
|
+
};
|
|
10889
|
+
return next;
|
|
10890
|
+
}
|
|
10891
|
+
function eventType(before, after) {
|
|
10892
|
+
const count = (value) => Array.isArray(value) ? value.length : 0;
|
|
10893
|
+
if (before.status !== after.status)
|
|
10894
|
+
return after.status === "closed" ? "task.closed" : after.status === "paused" ? "task.paused" : "task.resumed";
|
|
10895
|
+
if (count(after.evidence) > count(before.evidence)) {
|
|
10896
|
+
const last = after.evidence.at(-1);
|
|
10897
|
+
return last?.data?.observation ? "check.ran" : "evidence.recorded";
|
|
10898
|
+
}
|
|
10899
|
+
if (count(after.findings) > count(before.findings))
|
|
10900
|
+
return "finding.recorded";
|
|
10901
|
+
if (count(after.decisions) > count(before.decisions))
|
|
10902
|
+
return "decision.recorded";
|
|
10903
|
+
if (JSON.stringify(before.findings) !== JSON.stringify(after.findings))
|
|
10904
|
+
return "finding.updated";
|
|
10905
|
+
if (JSON.stringify(before.decisions) !== JSON.stringify(after.decisions))
|
|
10906
|
+
return "decision.updated";
|
|
10907
|
+
if (JSON.stringify(before.workers) !== JSON.stringify(after.workers))
|
|
10908
|
+
return "worker.updated";
|
|
10909
|
+
if (JSON.stringify(before.intent) !== JSON.stringify(after.intent))
|
|
10910
|
+
return "task.revised";
|
|
10911
|
+
if (JSON.stringify(before.assessments) !== JSON.stringify(after.assessments) || JSON.stringify(before.policy) !== JSON.stringify(after.policy))
|
|
10912
|
+
return "task.judged";
|
|
10913
|
+
if (JSON.stringify(before.progress) !== JSON.stringify(after.progress))
|
|
10914
|
+
return "task.noted";
|
|
10915
|
+
return "task.updated";
|
|
10916
|
+
}
|
|
10917
|
+
|
|
10918
|
+
// packages/workit-core/src/store/notes.ts
|
|
10919
|
+
var SLOT = Symbol.for("workit.migrationReporter");
|
|
10920
|
+
var reportMigration = (report) => {
|
|
10921
|
+
try {
|
|
10922
|
+
const reporter = globalThis[SLOT];
|
|
10923
|
+
if (typeof reporter === "function")
|
|
10924
|
+
reporter(report);
|
|
10925
|
+
} catch {}
|
|
10926
|
+
};
|
|
10927
|
+
var migrationReporterInstalled = () => typeof globalThis[SLOT] === "function";
|
|
10928
|
+
|
|
10929
|
+
// packages/workit-core/src/core/task-store.ts
|
|
10930
|
+
var MIGRATION_PENDING = "workit migration pending";
|
|
10931
|
+
var STORE_FORMAT = 3;
|
|
10932
|
+
var STORE_MIN_RUNTIME = "3.0.0";
|
|
10933
|
+
var SNAPSHOT_EVERY = 32;
|
|
10934
|
+
var COMPACT_ABOVE = 200;
|
|
10935
|
+
var COMPACT_KEEP = 50;
|
|
10936
|
+
var AUTO_COMPACT_BYTES = 2 * 1024 * 1024;
|
|
10937
|
+
var TOMBSTONE = Symbol("tombstone");
|
|
10938
|
+
var STALE_TEMPORARY_MS = 60 * 60000;
|
|
10939
|
+
var INDEX_VERSION = 3;
|
|
10940
|
+
var isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10941
|
+
var hostSession = (value) => isObject4(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
10942
|
+
var indexEntry = (task, key, legacy, file) => {
|
|
10943
|
+
const sessions = [];
|
|
10944
|
+
const intent = hostSession(task.intent.provenance.session);
|
|
10945
|
+
if (intent)
|
|
10946
|
+
sessions.push({ ...intent, workerId: null });
|
|
10947
|
+
for (const worker of task.workers) {
|
|
10948
|
+
const session = hostSession(worker.data.session);
|
|
10949
|
+
if (session)
|
|
10950
|
+
sessions.push({ ...session, workerId: worker.id });
|
|
10951
|
+
}
|
|
10952
|
+
return {
|
|
10953
|
+
id: task.id,
|
|
10954
|
+
revision: task.revision,
|
|
10955
|
+
status: task.status,
|
|
10956
|
+
createdAt: task.createdAt,
|
|
10957
|
+
updatedAt: task.updatedAt,
|
|
10958
|
+
objective: task.intent.data.objective,
|
|
10959
|
+
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
10960
|
+
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
10961
|
+
sessions,
|
|
10962
|
+
workspaceId: task.workspaceId,
|
|
10963
|
+
key: key?.key ?? null,
|
|
10964
|
+
branch: key?.branch ?? null,
|
|
10965
|
+
boundAt: key?.boundAt ?? null,
|
|
10966
|
+
legacy,
|
|
10967
|
+
file
|
|
10968
|
+
};
|
|
10969
|
+
};
|
|
10970
|
+
var fileSignature = (file) => {
|
|
10971
|
+
try {
|
|
10972
|
+
const stat = fs8.statSync(file, { bigint: true });
|
|
10973
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
10974
|
+
} catch {
|
|
10975
|
+
return null;
|
|
10976
|
+
}
|
|
10977
|
+
};
|
|
10978
|
+
var RACY_WINDOW_NS = 2000000000n;
|
|
10979
|
+
var racySignature = (signature) => {
|
|
10980
|
+
const [mtime = 0n, ctime = 0n] = signature.split(":").slice(3, 5).map(BigInt);
|
|
10981
|
+
const changed = mtime > ctime ? mtime : ctime;
|
|
10982
|
+
return BigInt(Date.now()) * 1000000n - changed < RACY_WINDOW_NS;
|
|
10983
|
+
};
|
|
10984
|
+
var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
10985
|
+
var oldestFirst = (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
|
|
10986
|
+
var cachedRuntimeVersion = null;
|
|
10987
|
+
var runtimeVersion = () => {
|
|
10988
|
+
if (cachedRuntimeVersion === null) {
|
|
10989
|
+
try {
|
|
10990
|
+
const pkg = JSON.parse(fs8.readFileSync(path9.join(packageRoot(), "package.json"), "utf8"));
|
|
10991
|
+
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
10992
|
+
} catch {
|
|
10993
|
+
cachedRuntimeVersion = "0.0.0";
|
|
10994
|
+
}
|
|
10995
|
+
}
|
|
10996
|
+
return cachedRuntimeVersion;
|
|
10997
|
+
};
|
|
10998
|
+
var stampNew = (record) => ({
|
|
10999
|
+
createdWith: record.runtime?.createdWith ?? null,
|
|
11000
|
+
updatedWith: runtimeVersion()
|
|
11001
|
+
});
|
|
11002
|
+
var isNewerVersion = (candidate, current) => {
|
|
11003
|
+
const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
|
|
11004
|
+
const [candidateParts, currentParts] = [parts(candidate), parts(current)];
|
|
11005
|
+
for (let index = 0;index < 3; index += 1) {
|
|
11006
|
+
const left = candidateParts[index] ?? 0;
|
|
11007
|
+
const right = currentParts[index] ?? 0;
|
|
11008
|
+
if (left !== right)
|
|
11009
|
+
return left > right;
|
|
11010
|
+
}
|
|
11011
|
+
return false;
|
|
11012
|
+
};
|
|
11013
|
+
var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
11014
|
+
var sameDirectoryIdentity = (left, right) => {
|
|
11015
|
+
if (!path9.isAbsolute(left) || !path9.isAbsolute(right))
|
|
11016
|
+
return false;
|
|
11017
|
+
const normalizedLeft = path9.resolve(left);
|
|
11018
|
+
const normalizedRight = path9.resolve(right);
|
|
11019
|
+
try {
|
|
11020
|
+
const a = fs8.statSync(normalizedLeft, { bigint: true });
|
|
11021
|
+
const b = fs8.statSync(normalizedRight, { bigint: true });
|
|
11022
|
+
if (!a.isDirectory() || !b.isDirectory())
|
|
11023
|
+
return false;
|
|
11024
|
+
if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
|
|
11025
|
+
return true;
|
|
11026
|
+
const canonicalLeft = fs8.realpathSync(normalizedLeft);
|
|
11027
|
+
const canonicalRight = fs8.realpathSync(normalizedRight);
|
|
11028
|
+
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
11029
|
+
return true;
|
|
11030
|
+
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
11031
|
+
} catch {
|
|
11032
|
+
return false;
|
|
11033
|
+
}
|
|
11034
|
+
};
|
|
10363
11035
|
var externalActionLockRoots = new AsyncLocalStorage;
|
|
10364
11036
|
var heldInProcess = new Map;
|
|
10365
|
-
var
|
|
10366
|
-
var
|
|
10367
|
-
const count = (heldInProcess.get(
|
|
11037
|
+
var hold = (lock) => heldInProcess.set(lock, (heldInProcess.get(lock) ?? 0) + 1);
|
|
11038
|
+
var drop = (lock) => {
|
|
11039
|
+
const count = (heldInProcess.get(lock) ?? 1) - 1;
|
|
10368
11040
|
if (count > 0)
|
|
10369
|
-
heldInProcess.set(
|
|
11041
|
+
heldInProcess.set(lock, count);
|
|
10370
11042
|
else
|
|
10371
|
-
heldInProcess.delete(
|
|
11043
|
+
heldInProcess.delete(lock);
|
|
10372
11044
|
};
|
|
10373
|
-
var
|
|
10374
|
-
const
|
|
10375
|
-
return
|
|
11045
|
+
var envActor = () => {
|
|
11046
|
+
const value = (key) => process.env[key]?.trim() || null;
|
|
11047
|
+
return {
|
|
11048
|
+
host: value("WORKIT_HOST") ?? "runtime",
|
|
11049
|
+
session: value("WORKIT_SESSION_ID"),
|
|
11050
|
+
agentId: value("WORKIT_AGENT_ID")
|
|
11051
|
+
};
|
|
10376
11052
|
};
|
|
10377
11053
|
|
|
10378
11054
|
class TaskStore {
|
|
10379
11055
|
root;
|
|
11056
|
+
origin;
|
|
10380
11057
|
lockTimeoutMs;
|
|
11058
|
+
actor;
|
|
11059
|
+
migrateOnRead;
|
|
11060
|
+
located;
|
|
11061
|
+
migrationChecked = false;
|
|
11062
|
+
compactDue = new Set;
|
|
10381
11063
|
constructor(root, options = {}) {
|
|
10382
|
-
|
|
11064
|
+
const resolved = path9.resolve(root);
|
|
11065
|
+
this.origin = fs8.existsSync(resolved) ? fs8.realpathSync(resolved) : resolved;
|
|
11066
|
+
this.located = resolveStore(this.origin);
|
|
11067
|
+
this.root = checkoutRootOf(this.origin, this.located);
|
|
10383
11068
|
this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
|
|
11069
|
+
this.actor = options.actor ?? envActor();
|
|
11070
|
+
this.migrateOnRead = options.migrateOnRead;
|
|
11071
|
+
}
|
|
11072
|
+
location() {
|
|
11073
|
+
if (this.located !== undefined && !(this.located instanceof Error) && !this.located.shared && fs8.existsSync(path9.join(this.root, ".git"))) {
|
|
11074
|
+
this.located = undefined;
|
|
11075
|
+
this.migrationChecked = false;
|
|
11076
|
+
}
|
|
11077
|
+
if (this.located === undefined)
|
|
11078
|
+
this.located = resolveStore(this.root);
|
|
11079
|
+
return this.located instanceof Error ? failure2("storage_error", this.located.message, { path: this.root }) : success(null, null, this.located);
|
|
11080
|
+
}
|
|
11081
|
+
currentKey() {
|
|
11082
|
+
const location = this.location();
|
|
11083
|
+
if (!location.ok)
|
|
11084
|
+
return location;
|
|
11085
|
+
return success(null, null, resolveTaskKey(this.root, location.data));
|
|
10384
11086
|
}
|
|
10385
11087
|
readTask(taskId) {
|
|
10386
11088
|
if (!validId(taskId))
|
|
10387
11089
|
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
10388
|
-
const
|
|
10389
|
-
if (!
|
|
11090
|
+
const ready = this.ready();
|
|
11091
|
+
if (!ready.ok)
|
|
11092
|
+
return ready;
|
|
11093
|
+
const loaded = this.load(taskId);
|
|
11094
|
+
if (!loaded.ok)
|
|
11095
|
+
return loaded;
|
|
11096
|
+
if (!loaded.data)
|
|
10390
11097
|
return failure2("not_found", "task not found", { taskId });
|
|
10391
|
-
if (result.result.ok && result.result.data.id !== taskId)
|
|
10392
|
-
return failure2("recovery_required", "task filename and record ID differ", { taskId });
|
|
10393
|
-
if (result.result.ok) {
|
|
10394
|
-
const workspace = this.readWorkspace();
|
|
10395
|
-
if (!workspace.ok)
|
|
10396
|
-
return workspace;
|
|
10397
|
-
if (!workspace.data || result.result.data.workspaceId !== workspace.data.id)
|
|
10398
|
-
return failure2("recovery_required", "task workspace binding is invalid", { taskId });
|
|
10399
|
-
}
|
|
10400
|
-
return result.result;
|
|
10401
|
-
}
|
|
10402
|
-
listTasks() {
|
|
10403
|
-
if (!fs6.existsSync(this.tasksDir))
|
|
10404
|
-
return success(null, null, []);
|
|
10405
|
-
let names;
|
|
10406
|
-
try {
|
|
10407
|
-
names = fs6.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
|
|
10408
|
-
} catch (error) {
|
|
10409
|
-
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
10410
|
-
path: this.tasksDir
|
|
10411
|
-
});
|
|
10412
|
-
}
|
|
10413
11098
|
const workspace = this.readWorkspace();
|
|
10414
11099
|
if (!workspace.ok)
|
|
10415
11100
|
return workspace;
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
11101
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
11102
|
+
return bound ?? success(null, null, loaded.data.record);
|
|
11103
|
+
}
|
|
11104
|
+
listTasks() {
|
|
11105
|
+
const listed = this.listTaskIndex();
|
|
11106
|
+
if (!listed.ok)
|
|
11107
|
+
return listed;
|
|
10420
11108
|
const tasks = [];
|
|
10421
|
-
for (const
|
|
10422
|
-
const
|
|
10423
|
-
if (!
|
|
10424
|
-
|
|
10425
|
-
if (
|
|
10426
|
-
|
|
10427
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
10428
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
10429
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
10430
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
10431
|
-
tasks.push(item.result.data);
|
|
11109
|
+
for (const entry of listed.data.toSorted((left, right) => left.id.localeCompare(right.id))) {
|
|
11110
|
+
const loaded = this.load(entry.id);
|
|
11111
|
+
if (!loaded.ok)
|
|
11112
|
+
return loaded;
|
|
11113
|
+
if (loaded.data)
|
|
11114
|
+
tasks.push(loaded.data.record);
|
|
10432
11115
|
}
|
|
10433
11116
|
return success(null, null, tasks);
|
|
10434
11117
|
}
|
|
10435
11118
|
listTaskIndex() {
|
|
10436
|
-
|
|
11119
|
+
const all = this.listStoreIndex();
|
|
11120
|
+
if (!all.ok)
|
|
11121
|
+
return all;
|
|
11122
|
+
const workspace = this.readWorkspace();
|
|
11123
|
+
if (!workspace.ok)
|
|
11124
|
+
return workspace;
|
|
11125
|
+
if (!workspace.data)
|
|
10437
11126
|
return success(null, null, []);
|
|
11127
|
+
const id = workspace.data.id;
|
|
11128
|
+
return success(null, null, all.data.filter((entry) => entry.workspaceId === id));
|
|
11129
|
+
}
|
|
11130
|
+
listStoreIndex() {
|
|
11131
|
+
const ready = this.ready();
|
|
11132
|
+
if (!ready.ok)
|
|
11133
|
+
return ready;
|
|
11134
|
+
const tasksDir = this.paths().tasks;
|
|
10438
11135
|
let names;
|
|
10439
11136
|
try {
|
|
10440
|
-
names =
|
|
11137
|
+
names = fs8.existsSync(tasksDir) ? fs8.readdirSync(tasksDir).filter((name) => validId(name)) : [];
|
|
10441
11138
|
} catch (error) {
|
|
10442
11139
|
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
10443
|
-
path:
|
|
11140
|
+
path: tasksDir
|
|
10444
11141
|
});
|
|
10445
11142
|
}
|
|
10446
|
-
const
|
|
10447
|
-
if (!workspace.ok)
|
|
10448
|
-
return workspace;
|
|
10449
|
-
if (!workspace.data)
|
|
10450
|
-
return failure2("recovery_required", "task workspace binding is invalid", {
|
|
10451
|
-
path: this.workspacePath
|
|
10452
|
-
});
|
|
10453
|
-
const stored = this.readIndex(workspace.data.id);
|
|
11143
|
+
const stored = this.readIndex();
|
|
10454
11144
|
const entries = [];
|
|
10455
11145
|
let changed = Object.keys(stored).length !== names.length;
|
|
10456
11146
|
for (const name of names.toSorted()) {
|
|
10457
|
-
const
|
|
10458
|
-
const signature = fileSignature(file);
|
|
11147
|
+
const signature = fileSignature(this.eventsPath(name));
|
|
10459
11148
|
if (signature === null) {
|
|
10460
11149
|
changed = true;
|
|
10461
11150
|
continue;
|
|
10462
11151
|
}
|
|
10463
|
-
const cached = stored[name
|
|
11152
|
+
const cached = stored[name];
|
|
10464
11153
|
if (cached && cached.file === signature && !racySignature(signature)) {
|
|
10465
11154
|
entries.push(cached);
|
|
10466
11155
|
continue;
|
|
10467
11156
|
}
|
|
10468
|
-
const
|
|
10469
|
-
if (!
|
|
11157
|
+
const loaded = this.load(name);
|
|
11158
|
+
if (!loaded.ok)
|
|
11159
|
+
return loaded;
|
|
11160
|
+
if (!loaded.data)
|
|
10470
11161
|
continue;
|
|
10471
|
-
|
|
10472
|
-
return item.result;
|
|
10473
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
10474
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
10475
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
10476
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
10477
|
-
const entry = indexEntry(item.result.data, signature);
|
|
11162
|
+
const entry = indexEntry(loaded.data.record, loaded.data.key, loaded.data.legacy !== null, signature);
|
|
10478
11163
|
if (!cached || canonicalJson(cached) !== canonicalJson(entry))
|
|
10479
11164
|
changed = true;
|
|
10480
11165
|
entries.push(entry);
|
|
10481
11166
|
}
|
|
10482
11167
|
if (changed)
|
|
10483
|
-
this.writeIndex(
|
|
11168
|
+
this.writeIndex(entries);
|
|
10484
11169
|
return success(null, null, entries);
|
|
10485
11170
|
}
|
|
10486
11171
|
readWorkspace() {
|
|
10487
|
-
const
|
|
10488
|
-
if (!
|
|
10489
|
-
return
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
11172
|
+
const ready = this.ready();
|
|
11173
|
+
if (!ready.ok)
|
|
11174
|
+
return ready;
|
|
11175
|
+
const file = this.paths().workspace;
|
|
11176
|
+
let bytes;
|
|
11177
|
+
try {
|
|
11178
|
+
bytes = retryTransient(() => fs8.readFileSync(file, "utf8"));
|
|
11179
|
+
} catch (error) {
|
|
11180
|
+
if (error.code === "ENOENT")
|
|
11181
|
+
return success(null, null, null);
|
|
11182
|
+
return failure2("recovery_required", `workspace record cannot be read: ${String(error)}`, {
|
|
11183
|
+
path: file
|
|
10493
11184
|
});
|
|
10494
|
-
|
|
11185
|
+
}
|
|
11186
|
+
const parsed = this.parseRecord(bytes, workspaceRecordSchema, "workspace");
|
|
11187
|
+
if (!parsed.ok)
|
|
11188
|
+
return parsed;
|
|
11189
|
+
if (parsed.data.root !== this.root && !sameDirectoryIdentity(parsed.data.root, this.root))
|
|
11190
|
+
return failure2("recovery_required", "workspace root binding is invalid", { path: file });
|
|
11191
|
+
return parsed;
|
|
11192
|
+
}
|
|
11193
|
+
implicitTask(input) {
|
|
11194
|
+
const ready = this.ready(input.create !== false);
|
|
11195
|
+
if (!ready.ok)
|
|
11196
|
+
return ready;
|
|
11197
|
+
const key = this.currentKey();
|
|
11198
|
+
if (!key.ok)
|
|
11199
|
+
return key;
|
|
11200
|
+
const found = this.openTaskForKey(key.data.key);
|
|
11201
|
+
if (!found.ok)
|
|
11202
|
+
return found;
|
|
11203
|
+
const workspace = this.readWorkspace();
|
|
11204
|
+
if (!workspace.ok)
|
|
11205
|
+
return workspace;
|
|
11206
|
+
if (found.data && workspace.data && found.data.workspaceId === workspace.data.id && (found.data.key === key.data.key || input.create === false)) {
|
|
11207
|
+
const task = this.readTask(found.data.id);
|
|
11208
|
+
return task.ok ? success(task.revision, null, { task: task.data, created: false, key: key.data }) : task;
|
|
11209
|
+
}
|
|
11210
|
+
if (input.create === false) {
|
|
11211
|
+
if (!found.data)
|
|
11212
|
+
return success(null, null, null);
|
|
11213
|
+
const loaded = this.load(found.data.id);
|
|
11214
|
+
if (!loaded.ok)
|
|
11215
|
+
return loaded;
|
|
11216
|
+
return loaded.data ? success(null, null, { task: loaded.data.record, created: false, key: key.data }) : success(null, null, null);
|
|
11217
|
+
}
|
|
11218
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => this.implicitLocked(input, key.data)));
|
|
11219
|
+
}
|
|
11220
|
+
implicitLocked(input, key) {
|
|
11221
|
+
{
|
|
11222
|
+
const again = this.openTaskForKey(key.key);
|
|
11223
|
+
if (!again.ok)
|
|
11224
|
+
return again;
|
|
11225
|
+
if (again.data) {
|
|
11226
|
+
const moved = this.bindLocked(again.data.id, key, input.now);
|
|
11227
|
+
return moved.ok ? success(moved.data.revision, null, { task: moved.data, created: false, key }) : moved;
|
|
11228
|
+
}
|
|
11229
|
+
const label = key.kind === "branch" ? `branch ${key.branch}` : key.kind === "detached" ? "a detached HEAD" : path9.basename(this.root) || this.root;
|
|
11230
|
+
const intent = {
|
|
11231
|
+
objective: input.objective?.trim() || `Work on ${label}`,
|
|
11232
|
+
scope: { description: `the checkout (${label})`, paths: ["."], exclusions: [] },
|
|
11233
|
+
authorityRefs: []
|
|
11234
|
+
};
|
|
11235
|
+
const created = this.createLocked({ intent, provenance: input.provenance, expectedWorkspaceRevision: null, now: input.now }, false, key, false);
|
|
11236
|
+
return created.ok ? success(created.data.revision, null, { task: created.data, created: true, key }) : created;
|
|
11237
|
+
}
|
|
11238
|
+
}
|
|
11239
|
+
adoptTask(taskId) {
|
|
11240
|
+
if (!validId(taskId))
|
|
11241
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
11242
|
+
const ready = this.ready(true);
|
|
11243
|
+
if (!ready.ok)
|
|
11244
|
+
return ready;
|
|
11245
|
+
const key = this.currentKey();
|
|
11246
|
+
if (!key.ok)
|
|
11247
|
+
return key;
|
|
11248
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => {
|
|
11249
|
+
const holder = this.openTaskForKey(key.data.key);
|
|
11250
|
+
if (!holder.ok)
|
|
11251
|
+
return holder;
|
|
11252
|
+
if (holder.data && holder.data.id !== taskId)
|
|
11253
|
+
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 });
|
|
11254
|
+
return this.bindLocked(taskId, key.data);
|
|
11255
|
+
}));
|
|
11256
|
+
}
|
|
11257
|
+
keyReport() {
|
|
11258
|
+
const key = this.currentKey();
|
|
11259
|
+
if (!key.ok)
|
|
11260
|
+
return key;
|
|
11261
|
+
const bound = this.openTasksForKey(key.data.key);
|
|
11262
|
+
if (!bound.ok)
|
|
11263
|
+
return bound;
|
|
11264
|
+
return success(null, null, { key: key.data, bound: bound.data });
|
|
10495
11265
|
}
|
|
10496
11266
|
create(input, provenance, expectedWorkspaceRevision) {
|
|
10497
11267
|
const value = "intent" in input ? input : {
|
|
@@ -10499,82 +11269,16 @@ class TaskStore {
|
|
|
10499
11269
|
provenance,
|
|
10500
11270
|
expectedWorkspaceRevision: expectedWorkspaceRevision ?? null
|
|
10501
11271
|
};
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
|
|
10507
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
10508
|
-
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
10509
|
-
actualWorkspaceRevision: current.data?.revision ?? null
|
|
10510
|
-
});
|
|
10511
|
-
}
|
|
10512
|
-
if (!value.provenance)
|
|
10513
|
-
return failure2("invalid_input", "provenance is required");
|
|
10514
|
-
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
10515
|
-
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
10516
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
10517
|
-
if (current.data && !previousWorkspaceBytes)
|
|
10518
|
-
return failure2("storage_error", "workspace snapshot disappeared during creation");
|
|
10519
|
-
const workspace = current.data ? {
|
|
10520
|
-
...current.data,
|
|
10521
|
-
revision: newRevision(),
|
|
10522
|
-
root: this.root,
|
|
10523
|
-
runtime: stampNew(current.data)
|
|
10524
|
-
} : {
|
|
10525
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10526
|
-
id: newId(),
|
|
10527
|
-
revision: newRevision(),
|
|
10528
|
-
root: this.root,
|
|
10529
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10530
|
-
writer: null
|
|
10531
|
-
};
|
|
10532
|
-
const timestamp = value.now ?? now();
|
|
10533
|
-
const task = {
|
|
10534
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10535
|
-
id: newId(),
|
|
10536
|
-
workspaceId: workspace.id,
|
|
10537
|
-
revision: newRevision(),
|
|
10538
|
-
createdAt: timestamp,
|
|
10539
|
-
updatedAt: timestamp,
|
|
10540
|
-
origin: null,
|
|
10541
|
-
intent: {
|
|
10542
|
-
id: newId(),
|
|
10543
|
-
recordedAt: timestamp,
|
|
10544
|
-
provenance: value.provenance,
|
|
10545
|
-
data: value.intent
|
|
10546
|
-
},
|
|
10547
|
-
constraints: [],
|
|
10548
|
-
status: "active",
|
|
10549
|
-
closure: null,
|
|
10550
|
-
progress: { summary: "", nextAction: null, blockers: [] },
|
|
10551
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10552
|
-
assessments: [],
|
|
10553
|
-
policy: null,
|
|
10554
|
-
policyChanges: [],
|
|
10555
|
-
candidates: [],
|
|
10556
|
-
evidence: [],
|
|
10557
|
-
decisions: [],
|
|
10558
|
-
findings: [],
|
|
10559
|
-
workers: []
|
|
10560
|
-
};
|
|
10561
|
-
const writtenWorkspace = this.replaceSnapshot(this.workspacePath, workspace, previousWorkspaceBytes);
|
|
10562
|
-
if (!writtenWorkspace.ok)
|
|
10563
|
-
return writtenWorkspace;
|
|
10564
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), task, null);
|
|
10565
|
-
if (!writtenTask.ok) {
|
|
10566
|
-
if (current.data && previousWorkspaceBytes) {
|
|
10567
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
10568
|
-
if (!restored.ok)
|
|
10569
|
-
return failure2("external_outcome_unknown", "task creation failed and workspace restoration is uncertain", { operation: "create", outcome: "unknown" });
|
|
10570
|
-
}
|
|
10571
|
-
return failure2("external_outcome_unknown", "workspace created but task write is uncertain", { operation: "create", outcome: "unknown" });
|
|
10572
|
-
}
|
|
10573
|
-
return success(task.revision, workspace.revision, task);
|
|
10574
|
-
});
|
|
11272
|
+
const ready = this.ready(true);
|
|
11273
|
+
if (!ready.ok)
|
|
11274
|
+
return ready;
|
|
11275
|
+
return this.withLock(this.paths().checkoutLock, () => this.createLocked(value, true, null));
|
|
10575
11276
|
}
|
|
10576
11277
|
importTask(input) {
|
|
10577
|
-
|
|
11278
|
+
const ready = this.ready(true);
|
|
11279
|
+
if (!ready.ok)
|
|
11280
|
+
return ready;
|
|
11281
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
10578
11282
|
const current = this.readWorkspace();
|
|
10579
11283
|
if (!current.ok)
|
|
10580
11284
|
return current;
|
|
@@ -10606,23 +11310,8 @@ class TaskStore {
|
|
|
10606
11310
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
10607
11311
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
10608
11312
|
});
|
|
10609
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
10610
|
-
if (current.data && !previousWorkspaceBytes)
|
|
10611
|
-
return failure2("storage_error", "workspace snapshot disappeared during import");
|
|
10612
11313
|
const timestamp = input.now ?? now();
|
|
10613
|
-
const workspace = current.data
|
|
10614
|
-
...current.data,
|
|
10615
|
-
revision: newRevision(),
|
|
10616
|
-
root: this.root,
|
|
10617
|
-
runtime: stampNew(current.data)
|
|
10618
|
-
} : {
|
|
10619
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10620
|
-
id: input.workspaceId ?? newId(),
|
|
10621
|
-
revision: newRevision(),
|
|
10622
|
-
root: this.root,
|
|
10623
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10624
|
-
writer: null
|
|
10625
|
-
};
|
|
11314
|
+
const workspace = this.nextWorkspace(current.data, input.workspaceId);
|
|
10626
11315
|
const task = {
|
|
10627
11316
|
...input.task,
|
|
10628
11317
|
workspaceId: workspace.id,
|
|
@@ -10637,62 +11326,46 @@ class TaskStore {
|
|
|
10637
11326
|
const validTask = taskRecordSchema.safeParse(task);
|
|
10638
11327
|
if (!validTask.success)
|
|
10639
11328
|
return failure2("invalid_input", "imported task does not satisfy its schema");
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), validTask.data, null);
|
|
10644
|
-
if (!writtenTask.ok) {
|
|
10645
|
-
if (current.data && previousWorkspaceBytes) {
|
|
10646
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
10647
|
-
if (!restored.ok)
|
|
10648
|
-
return failure2("external_outcome_unknown", "import failed and workspace restoration is uncertain", { operation: "import", outcome: "unknown" });
|
|
10649
|
-
}
|
|
10650
|
-
return failure2("external_outcome_unknown", `workspace created but task import is uncertain: ${writtenTask.error}`, {
|
|
10651
|
-
operation: "import",
|
|
10652
|
-
outcome: "unknown",
|
|
10653
|
-
path: writtenTask.details.path
|
|
10654
|
-
});
|
|
10655
|
-
}
|
|
10656
|
-
return success(validTask.data.revision, workspace.revision, validTask.data);
|
|
11329
|
+
return this.writeNewTask(current.data, workspace, validTask.data, "task.imported", null, {
|
|
11330
|
+
operation: "import"
|
|
11331
|
+
});
|
|
10657
11332
|
});
|
|
10658
11333
|
}
|
|
10659
11334
|
mutateTask(taskId, expected, update, timestamp) {
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
const
|
|
10667
|
-
if (!
|
|
10668
|
-
return
|
|
11335
|
+
if (!validId(taskId))
|
|
11336
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
11337
|
+
const ready = this.ready(true);
|
|
11338
|
+
if (!ready.ok)
|
|
11339
|
+
return ready;
|
|
11340
|
+
const result = this.withLock(this.taskLockPath(taskId), () => {
|
|
11341
|
+
const loaded = this.loadBound(taskId);
|
|
11342
|
+
if (!loaded.ok)
|
|
11343
|
+
return loaded;
|
|
11344
|
+
if (loaded.data.record.revision !== expected)
|
|
11345
|
+
return this.conflict(expected, loaded.data.record.revision);
|
|
10669
11346
|
const context = { now: timestamp ?? now(), revision: newRevision() };
|
|
10670
11347
|
let changed;
|
|
10671
11348
|
try {
|
|
10672
|
-
changed = update(
|
|
11349
|
+
changed = update(structuredClone(loaded.data.record), Object.freeze({ ...context }));
|
|
10673
11350
|
} catch (error) {
|
|
10674
11351
|
return failure2("storage_error", `task mutation failed: ${String(error)}`);
|
|
10675
11352
|
}
|
|
10676
11353
|
if (!changed.ok)
|
|
10677
11354
|
return changed;
|
|
10678
|
-
const
|
|
10679
|
-
|
|
10680
|
-
id: taskId,
|
|
10681
|
-
workspaceId: current.data.workspaceId,
|
|
10682
|
-
createdAt: current.data.createdAt,
|
|
10683
|
-
revision: context.revision,
|
|
10684
|
-
updatedAt: context.now,
|
|
10685
|
-
runtime: stampNew(current.data)
|
|
10686
|
-
};
|
|
10687
|
-
const valid = taskRecordSchema.safeParse(record);
|
|
10688
|
-
if (!valid.success)
|
|
11355
|
+
const valid = this.nextTask(loaded.data.record, changed.data, context);
|
|
11356
|
+
if (!valid)
|
|
10689
11357
|
return failure2("invalid_input", "task mutation produced an invalid record");
|
|
10690
|
-
const written = this.
|
|
10691
|
-
return written.ok ? success(valid.
|
|
11358
|
+
const written = this.appendPatch(loaded.data, valid);
|
|
11359
|
+
return written.ok ? success(valid.revision, null, valid) : written;
|
|
10692
11360
|
});
|
|
11361
|
+
this.compactPending();
|
|
11362
|
+
return result;
|
|
10693
11363
|
}
|
|
10694
11364
|
mutateWorkspace(expected, update) {
|
|
10695
|
-
|
|
11365
|
+
const ready = this.ready(true);
|
|
11366
|
+
if (!ready.ok)
|
|
11367
|
+
return ready;
|
|
11368
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
10696
11369
|
const current = this.readWorkspace();
|
|
10697
11370
|
if (!current.ok)
|
|
10698
11371
|
return current;
|
|
@@ -10700,9 +11373,6 @@ class TaskStore {
|
|
|
10700
11373
|
return failure2("not_found", "workspace not found");
|
|
10701
11374
|
if (current.data.revision !== expected)
|
|
10702
11375
|
return this.workspaceConflict(expected, current.data.revision);
|
|
10703
|
-
const previousBytes = this.snapshotBytes(this.workspacePath);
|
|
10704
|
-
if (!previousBytes)
|
|
10705
|
-
return failure2("storage_error", "workspace snapshot disappeared during mutation");
|
|
10706
11376
|
const context = { now: now(), revision: newRevision() };
|
|
10707
11377
|
let changed;
|
|
10708
11378
|
try {
|
|
@@ -10712,23 +11382,27 @@ class TaskStore {
|
|
|
10712
11382
|
}
|
|
10713
11383
|
if (!changed.ok)
|
|
10714
11384
|
return changed;
|
|
10715
|
-
const
|
|
11385
|
+
const valid = workspaceRecordSchema.safeParse({
|
|
10716
11386
|
...changed.data,
|
|
10717
11387
|
id: current.data.id,
|
|
10718
11388
|
root: this.root,
|
|
10719
11389
|
revision: context.revision,
|
|
10720
11390
|
runtime: stampNew(current.data)
|
|
10721
|
-
};
|
|
10722
|
-
const valid = workspaceRecordSchema.safeParse(record);
|
|
11391
|
+
});
|
|
10723
11392
|
if (!valid.success)
|
|
10724
11393
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
10725
|
-
const written = this.
|
|
11394
|
+
const written = this.writeWorkspace(valid.data);
|
|
10726
11395
|
return written.ok ? success(valid.data.revision, valid.data.revision, valid.data) : written;
|
|
10727
11396
|
});
|
|
10728
11397
|
}
|
|
10729
11398
|
mutateTaskAndWorkspace(input) {
|
|
10730
|
-
|
|
10731
|
-
|
|
11399
|
+
if (!validId(input.taskId))
|
|
11400
|
+
return failure2("invalid_input", "task ID is invalid", { taskId: input.taskId });
|
|
11401
|
+
const ready = this.ready(true);
|
|
11402
|
+
if (!ready.ok)
|
|
11403
|
+
return ready;
|
|
11404
|
+
const result = this.withLock(this.paths().checkoutLock, () => this.withLock(this.taskLockPath(input.taskId), () => {
|
|
11405
|
+
const task = this.loadBound(input.taskId);
|
|
10732
11406
|
if (!task.ok)
|
|
10733
11407
|
return task;
|
|
10734
11408
|
const workspace = this.readWorkspace();
|
|
@@ -10739,14 +11413,10 @@ class TaskStore {
|
|
|
10739
11413
|
const expected = input.expectedRevision;
|
|
10740
11414
|
if (!expected)
|
|
10741
11415
|
return failure2("invalid_input", "task revision is required");
|
|
10742
|
-
if (task.data.revision !== expected)
|
|
10743
|
-
return this.conflict(expected, task.data.revision);
|
|
11416
|
+
if (task.data.record.revision !== expected)
|
|
11417
|
+
return this.conflict(expected, task.data.record.revision);
|
|
10744
11418
|
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
10745
11419
|
return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
|
|
10746
|
-
const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
|
|
10747
|
-
const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
|
|
10748
|
-
if (!previousWorkspaceBytes || !previousTaskBytes)
|
|
10749
|
-
return failure2("storage_error", "snapshot disappeared during coupled mutation");
|
|
10750
11420
|
const workspaceContext = { now: input.now ?? now(), revision: newRevision() };
|
|
10751
11421
|
let changedWorkspace;
|
|
10752
11422
|
try {
|
|
@@ -10765,286 +11435,69 @@ class TaskStore {
|
|
|
10765
11435
|
});
|
|
10766
11436
|
if (!nextWorkspace.success)
|
|
10767
11437
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
10768
|
-
const reserved = this.
|
|
11438
|
+
const reserved = this.writeWorkspace(nextWorkspace.data);
|
|
10769
11439
|
if (!reserved.ok)
|
|
10770
11440
|
return reserved;
|
|
11441
|
+
const uncertain = (message) => {
|
|
11442
|
+
this.markUncertain(nextWorkspace.data);
|
|
11443
|
+
return failure2("external_outcome_unknown", message, {
|
|
11444
|
+
operation: "coupled_mutation",
|
|
11445
|
+
outcome: "unknown"
|
|
11446
|
+
});
|
|
11447
|
+
};
|
|
10771
11448
|
const taskContext = { now: input.now ?? now(), revision: newRevision() };
|
|
10772
11449
|
let changedTask;
|
|
10773
11450
|
try {
|
|
10774
|
-
changedTask = input.task(task.data, Object.freeze({ ...taskContext }));
|
|
11451
|
+
changedTask = input.task(structuredClone(task.data.record), Object.freeze({ ...taskContext }));
|
|
10775
11452
|
} catch (error) {
|
|
10776
|
-
|
|
10777
|
-
return failure2("external_outcome_unknown", `workspace reserved but task mutation threw: ${String(error)}`, { operation: "coupled_mutation", outcome: "unknown" });
|
|
10778
|
-
}
|
|
10779
|
-
if (!changedTask.ok) {
|
|
10780
|
-
this.markUncertain(nextWorkspace.data);
|
|
10781
|
-
return failure2("external_outcome_unknown", "workspace reserved but task update is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
11453
|
+
return uncertain(`workspace reserved but task mutation threw: ${String(error)}`);
|
|
10782
11454
|
}
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
11455
|
+
if (!changedTask.ok)
|
|
11456
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
11457
|
+
const nextTask = this.nextTask(task.data.record, changedTask.data, taskContext);
|
|
11458
|
+
if (!nextTask)
|
|
11459
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
11460
|
+
const written = this.appendPatch(task.data, nextTask);
|
|
11461
|
+
if (!written.ok)
|
|
11462
|
+
return uncertain("workspace reserved but task append is uncertain");
|
|
11463
|
+
return success(nextTask.revision, nextWorkspace.data.revision, {
|
|
11464
|
+
task: nextTask,
|
|
11465
|
+
workspace: nextWorkspace.data
|
|
10791
11466
|
});
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
}
|
|
10796
|
-
const written = this.replaceSnapshot(this.taskPath(input.taskId), nextTask.data, previousTaskBytes);
|
|
10797
|
-
if (!written.ok) {
|
|
10798
|
-
this.markUncertain(nextWorkspace.data);
|
|
10799
|
-
return failure2("external_outcome_unknown", "workspace reserved but task replacement is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
10800
|
-
}
|
|
10801
|
-
return success(nextTask.data.revision, nextWorkspace.data.revision, {
|
|
10802
|
-
task: nextTask.data,
|
|
10803
|
-
workspace: nextWorkspace.data
|
|
10804
|
-
});
|
|
10805
|
-
});
|
|
10806
|
-
}
|
|
10807
|
-
recoveryCandidates() {
|
|
10808
|
-
if (!fs6.existsSync(this.recoveryDir))
|
|
10809
|
-
return success(null, null, []);
|
|
10810
|
-
try {
|
|
10811
|
-
const candidates = [];
|
|
10812
|
-
for (const name of fs6.readdirSync(this.recoveryDir)) {
|
|
10813
|
-
const match = RECOVERY_NAME.exec(name);
|
|
10814
|
-
if (match)
|
|
10815
|
-
candidates.push({
|
|
10816
|
-
target: match[1],
|
|
10817
|
-
path: path7.join(this.recoveryDir, name),
|
|
10818
|
-
digest: match[3]
|
|
10819
|
-
});
|
|
10820
|
-
}
|
|
10821
|
-
return success(null, null, candidates);
|
|
10822
|
-
} catch (error) {
|
|
10823
|
-
return failure2("storage_error", `unable to list recovery: ${String(error)}`, {
|
|
10824
|
-
path: this.recoveryDir
|
|
10825
|
-
});
|
|
10826
|
-
}
|
|
10827
|
-
}
|
|
10828
|
-
recoverTask(taskId, input) {
|
|
10829
|
-
return this.recover("task", taskId, input);
|
|
10830
|
-
}
|
|
10831
|
-
recoverWorkspace(input) {
|
|
10832
|
-
return this.recover("workspace", null, input);
|
|
10833
|
-
}
|
|
10834
|
-
recover(target, taskId, input) {
|
|
10835
|
-
if (taskId !== null && !validId(taskId))
|
|
10836
|
-
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
10837
|
-
if (!input.reason || !Array.isArray(input.authorityRefs) || typeof input.processEvidence !== "function" || !validDigest(input.expectedBytes) || !validDigest(input.snapshotDigest))
|
|
10838
|
-
return failure2("invalid_input", "recovery authority is invalid");
|
|
10839
|
-
if (input.authorityRefs.some((ref) => !refSchema.safeParse(ref).success))
|
|
10840
|
-
return failure2("invalid_input", "recovery references are invalid");
|
|
10841
|
-
try {
|
|
10842
|
-
const workspace = this.readWorkspace();
|
|
10843
|
-
if (!workspace.ok && target === "task")
|
|
10844
|
-
return workspace;
|
|
10845
|
-
const workspaceValue = workspace.ok ? workspace.data : null;
|
|
10846
|
-
if (workspaceValue && workspaceValue.revision !== input.expectedWorkspaceRevision)
|
|
10847
|
-
return this.conflict(input.expectedWorkspaceRevision, workspaceValue.revision);
|
|
10848
|
-
const file = target === "workspace" ? this.workspacePath : this.taskPath(taskId);
|
|
10849
|
-
const currentBytes = this.snapshotBytes(file);
|
|
10850
|
-
if (!currentBytes)
|
|
10851
|
-
return failure2("not_found", "snapshot not found");
|
|
10852
|
-
if (digestBytes(currentBytes) !== input.expectedBytes)
|
|
10853
|
-
return failure2("revision_conflict", "snapshot bytes do not match expected bytes");
|
|
10854
|
-
let selectedBytes = currentBytes;
|
|
10855
|
-
if (digestBytes(currentBytes) !== input.snapshotDigest) {
|
|
10856
|
-
const candidate = this.findRecovery(target, taskId, input.snapshotDigest, workspaceValue);
|
|
10857
|
-
if (!candidate)
|
|
10858
|
-
return failure2("recovery_required", "validated recovery snapshot not found");
|
|
10859
|
-
selectedBytes = candidate;
|
|
10860
|
-
}
|
|
10861
|
-
const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
10862
|
-
if (!selectedRecord.ok)
|
|
10863
|
-
return selectedRecord;
|
|
10864
|
-
if (target === "workspace") {
|
|
10865
|
-
const parsed = selectedRecord;
|
|
10866
|
-
if (!parsed.ok || parsed.data.root !== this.root)
|
|
10867
|
-
return failure2("recovery_required", "workspace recovery binding is invalid");
|
|
10868
|
-
} else {
|
|
10869
|
-
const parsed = selectedRecord;
|
|
10870
|
-
if (!parsed.ok || parsed.data.id !== taskId || !workspaceValue || parsed.data.workspaceId !== workspaceValue.id)
|
|
10871
|
-
return failure2("recovery_required", "task recovery binding is invalid");
|
|
10872
|
-
}
|
|
10873
|
-
const lock = this.readLockSnapshot();
|
|
10874
|
-
if (!lock.ok)
|
|
10875
|
-
return lock;
|
|
10876
|
-
const writer = target === "workspace" ? selectedRecord.data.writer : workspaceValue?.writer ?? null;
|
|
10877
|
-
const evidence = this.processEvidence(input, lock.data?.data ?? null, writer);
|
|
10878
|
-
if (!evidence.ok)
|
|
10879
|
-
return evidence;
|
|
10880
|
-
if (lock.data && (evidence.data.pid !== lock.data.data.pid || evidence.data.processStart !== lock.data.data.processStart))
|
|
10881
|
-
return failure2("recovery_required", "process evidence does not match metadata lock");
|
|
10882
|
-
if (writer && (evidence.data.state !== "accounted_for" || evidence.data.ownerDigest !== sha256(canonicalJson(writer))))
|
|
10883
|
-
return failure2("recovery_required", "workspace writer is not accounted for");
|
|
10884
|
-
const recoveryGate = { reclaimed: false };
|
|
10885
|
-
const result = this.withLock((handle) => {
|
|
10886
|
-
if (lock.data && !recoveryGate.reclaimed)
|
|
10887
|
-
return failure2("recovery_required", "metadata lock changed during recovery", {
|
|
10888
|
-
path: this.lockPath
|
|
10889
|
-
});
|
|
10890
|
-
if (!handle.verifyStillHeld())
|
|
10891
|
-
return failure2("recovery_required", "metadata lock was compromised", {
|
|
10892
|
-
path: this.lockPath
|
|
10893
|
-
});
|
|
10894
|
-
const reacquiredBytes = this.snapshotBytes(file);
|
|
10895
|
-
if (!reacquiredBytes)
|
|
10896
|
-
return failure2("recovery_required", "snapshot disappeared during recovery");
|
|
10897
|
-
if (digestBytes(reacquiredBytes) !== input.expectedBytes)
|
|
10898
|
-
return failure2("revision_conflict", "snapshot changed during recovery");
|
|
10899
|
-
if (target === "task") {
|
|
10900
|
-
const currentWorkspace = this.readWorkspace();
|
|
10901
|
-
if (!currentWorkspace.ok || !currentWorkspace.data)
|
|
10902
|
-
return failure2("recovery_required", "workspace changed during recovery");
|
|
10903
|
-
if (currentWorkspace.data.revision !== input.expectedWorkspaceRevision)
|
|
10904
|
-
return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
|
|
10905
|
-
}
|
|
10906
|
-
if (target === "workspace") {
|
|
10907
|
-
const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
|
|
10908
|
-
if (!parsed.ok)
|
|
10909
|
-
return parsed;
|
|
10910
|
-
const value = { ...parsed.data, revision: newRevision(), writer: null };
|
|
10911
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
10912
|
-
return replaced.ok ? success(value.revision, value.revision, value) : replaced;
|
|
10913
|
-
}
|
|
10914
|
-
const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
10915
|
-
if (!parsed.ok)
|
|
10916
|
-
return parsed;
|
|
10917
|
-
const value = {
|
|
10918
|
-
...parsed.data,
|
|
10919
|
-
revision: newRevision(),
|
|
10920
|
-
updatedAt: now(),
|
|
10921
|
-
status: parsed.data.status === "active" ? "paused" : parsed.data.status
|
|
10922
|
-
};
|
|
10923
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
10924
|
-
return replaced.ok ? success(value.revision, null, value) : replaced;
|
|
10925
|
-
}, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
|
|
10926
|
-
return result;
|
|
10927
|
-
} catch (error) {
|
|
10928
|
-
return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
|
|
10929
|
-
}
|
|
10930
|
-
}
|
|
10931
|
-
processEvidence(input, lock, writer) {
|
|
10932
|
-
const lockCopy = lock ? this.deepFreeze(structuredClone(lock)) : null;
|
|
10933
|
-
const writerCopy = writer ? this.deepFreeze(structuredClone(writer)) : null;
|
|
10934
|
-
const lockDigest = lockCopy ? sha256(canonicalJson(lockCopy)) : null;
|
|
10935
|
-
const writerDigest = writerCopy ? sha256(canonicalJson(writerCopy)) : null;
|
|
10936
|
-
try {
|
|
10937
|
-
const result = input.processEvidence(lockCopy, writerCopy);
|
|
10938
|
-
if (lockCopy && sha256(canonicalJson(lockCopy)) !== lockDigest)
|
|
10939
|
-
return failure2("recovery_required", "process evidence mutated lock identity");
|
|
10940
|
-
if (writerCopy && sha256(canonicalJson(writerCopy)) !== writerDigest)
|
|
10941
|
-
return failure2("recovery_required", "process evidence mutated writer identity");
|
|
10942
|
-
if (!result.ok)
|
|
10943
|
-
return result;
|
|
10944
|
-
const parsed = processEvidenceSchema.safeParse(result.data);
|
|
10945
|
-
return parsed.success ? success(null, null, parsed.data) : failure2("recovery_required", "process evidence is invalid");
|
|
10946
|
-
} catch (error) {
|
|
10947
|
-
return failure2("recovery_required", `process evidence failed: ${String(error)}`);
|
|
10948
|
-
}
|
|
10949
|
-
}
|
|
10950
|
-
deepFreeze(value) {
|
|
10951
|
-
if (value && typeof value === "object") {
|
|
10952
|
-
for (const child of Object.values(value))
|
|
10953
|
-
this.deepFreeze(child);
|
|
10954
|
-
Object.freeze(value);
|
|
10955
|
-
}
|
|
10956
|
-
return value;
|
|
10957
|
-
}
|
|
10958
|
-
readLockSnapshot() {
|
|
10959
|
-
try {
|
|
10960
|
-
const stat = fs6.lstatSync(this.lockPath);
|
|
10961
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
10962
|
-
return failure2("recovery_required", "metadata lock is not a regular file", {
|
|
10963
|
-
path: this.lockPath
|
|
10964
|
-
});
|
|
10965
|
-
const raw = fs6.readFileSync(this.lockPath, "utf8");
|
|
10966
|
-
return success(null, null, { raw, data: parseMetadataLock(raw) });
|
|
10967
|
-
} catch (error) {
|
|
10968
|
-
if (error?.code === "ENOENT")
|
|
10969
|
-
return success(null, null, null);
|
|
10970
|
-
return failure2("recovery_required", `metadata lock is invalid: ${String(error)}`, {
|
|
10971
|
-
path: this.lockPath
|
|
10972
|
-
});
|
|
10973
|
-
}
|
|
10974
|
-
}
|
|
10975
|
-
metadataLockOptions() {
|
|
10976
|
-
const inProcess = heldInProcess.has(this.root);
|
|
10977
|
-
return {
|
|
10978
|
-
lockPath: this.lockPath,
|
|
10979
|
-
staleMs: Number.MAX_SAFE_INTEGER,
|
|
10980
|
-
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
10981
|
-
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
10982
|
-
staleRecovery: "remove-if-unchanged",
|
|
10983
|
-
shouldReclaim: ({ payload, nowMs }) => {
|
|
10984
|
-
let ageMs = null;
|
|
10985
|
-
try {
|
|
10986
|
-
ageMs = nowMs - fs6.lstatSync(this.lockPath).mtimeMs;
|
|
10987
|
-
} catch {}
|
|
10988
|
-
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
10989
|
-
},
|
|
10990
|
-
shouldRemoveStaleLock: () => true,
|
|
10991
|
-
parsePayload: parseMetadataLockOrNull,
|
|
10992
|
-
payload: () => ({
|
|
10993
|
-
pid: process.pid,
|
|
10994
|
-
processStart: processStartOf(process.pid),
|
|
10995
|
-
host: localLockHost(),
|
|
10996
|
-
nonce: randomUUID2()
|
|
10997
|
-
})
|
|
10998
|
-
};
|
|
10999
|
-
}
|
|
11000
|
-
acquireMetadataLock(options) {
|
|
11001
|
-
clearAbandonedReclaimGuard(this.lockPath);
|
|
11002
|
-
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
11003
|
-
while (true) {
|
|
11004
|
-
try {
|
|
11005
|
-
return acquireFileLockSync(this.workspacePath, {
|
|
11006
|
-
...options,
|
|
11007
|
-
timeoutMs: Math.max(0, deadline - Date.now())
|
|
11008
|
-
});
|
|
11009
|
-
} catch (error) {
|
|
11010
|
-
const code = error?.code;
|
|
11011
|
-
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
11012
|
-
throw error;
|
|
11013
|
-
if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
|
|
11014
|
-
throw error;
|
|
11015
|
-
if (code !== "file_lock_stale")
|
|
11016
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
11017
|
-
}
|
|
11018
|
-
}
|
|
11019
|
-
}
|
|
11020
|
-
externalActionLockOptions() {
|
|
11021
|
-
const options = this.metadataLockOptions();
|
|
11022
|
-
return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
|
|
11467
|
+
}));
|
|
11468
|
+
this.compactPending();
|
|
11469
|
+
return result;
|
|
11023
11470
|
}
|
|
11024
11471
|
async withExternalActionLock(operation, reentrant = false) {
|
|
11025
11472
|
const activeRoots = externalActionLockRoots.getStore();
|
|
11026
11473
|
if (activeRoots && [...activeRoots].some((root) => root === this.root || sameDirectoryIdentity(root, this.root)))
|
|
11027
11474
|
return operation();
|
|
11475
|
+
const ready = this.ready(true);
|
|
11476
|
+
if (!ready.ok)
|
|
11477
|
+
return ready;
|
|
11478
|
+
const lockPath = this.paths().checkoutLock;
|
|
11028
11479
|
try {
|
|
11029
|
-
this.
|
|
11480
|
+
this.initializeStorage();
|
|
11030
11481
|
} catch (error) {
|
|
11031
11482
|
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
11032
|
-
path: this.
|
|
11483
|
+
path: this.paths().dir
|
|
11033
11484
|
});
|
|
11034
11485
|
}
|
|
11035
11486
|
let handle;
|
|
11036
11487
|
try {
|
|
11037
|
-
|
|
11488
|
+
const options = this.lockOptions(lockPath);
|
|
11489
|
+
handle = this.acquire(lockPath, {
|
|
11490
|
+
...options,
|
|
11491
|
+
payload: () => ({ ...options.payload(), externalAction: true })
|
|
11492
|
+
});
|
|
11038
11493
|
} catch (error) {
|
|
11039
|
-
return this.lockFailure(error);
|
|
11494
|
+
return this.lockFailure(lockPath, error);
|
|
11040
11495
|
}
|
|
11041
|
-
|
|
11496
|
+
hold(lockPath);
|
|
11042
11497
|
let result;
|
|
11043
11498
|
try {
|
|
11044
11499
|
if (!handle.verifyStillHeld())
|
|
11045
|
-
result = failure2("recovery_required", "metadata lock was compromised", {
|
|
11046
|
-
path: this.lockPath
|
|
11047
|
-
});
|
|
11500
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
11048
11501
|
else {
|
|
11049
11502
|
try {
|
|
11050
11503
|
const run = () => operation();
|
|
@@ -11058,526 +11511,1144 @@ class TaskStore {
|
|
|
11058
11511
|
if (!handle.verifyStillHeld())
|
|
11059
11512
|
result = failure2("external_outcome_unknown", "metadata lock was compromised", {
|
|
11060
11513
|
outcome: "unknown",
|
|
11061
|
-
path:
|
|
11514
|
+
path: lockPath
|
|
11062
11515
|
});
|
|
11063
11516
|
} catch {
|
|
11064
11517
|
result = failure2("external_outcome_unknown", "metadata lock could not be verified", {
|
|
11065
11518
|
outcome: "unknown",
|
|
11066
|
-
path:
|
|
11519
|
+
path: lockPath
|
|
11067
11520
|
});
|
|
11068
11521
|
}
|
|
11069
11522
|
}
|
|
11070
11523
|
} catch {
|
|
11071
11524
|
result = failure2("external_outcome_unknown", "external action lock operation failed", {
|
|
11072
11525
|
outcome: "unknown",
|
|
11073
|
-
path:
|
|
11526
|
+
path: lockPath
|
|
11074
11527
|
});
|
|
11075
11528
|
}
|
|
11076
|
-
|
|
11529
|
+
drop(lockPath);
|
|
11077
11530
|
try {
|
|
11078
11531
|
retryTransient(() => handle.release());
|
|
11079
11532
|
} catch (error) {
|
|
11080
|
-
const released = this.lockFailure(error);
|
|
11533
|
+
const released = this.lockFailure(lockPath, error);
|
|
11081
11534
|
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
11082
11535
|
return result.ok ? failure2("external_outcome_unknown", "external action metadata lock release is uncertain", {
|
|
11083
11536
|
outcome: "unknown",
|
|
11084
|
-
path:
|
|
11537
|
+
path: lockPath
|
|
11085
11538
|
}) : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
11086
11539
|
}
|
|
11087
11540
|
return result;
|
|
11088
11541
|
}
|
|
11089
|
-
|
|
11090
|
-
const
|
|
11091
|
-
options.
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
return true;
|
|
11542
|
+
collectGarbage(options = {}) {
|
|
11543
|
+
const dryRun = options.dryRun === true;
|
|
11544
|
+
const compactAbove = options.compactAbove ?? COMPACT_ABOVE;
|
|
11545
|
+
const report = {
|
|
11546
|
+
dryRun,
|
|
11547
|
+
compacted: { tasks: [], eventsFolded: 0, bytesBefore: 0, bytesAfter: 0 },
|
|
11548
|
+
blobs: { removed: 0, removedBytes: 0, kept: 0 },
|
|
11549
|
+
temporary: { removed: 0 },
|
|
11550
|
+
legacyRecovery: null,
|
|
11551
|
+
retried: [],
|
|
11552
|
+
failed: []
|
|
11101
11553
|
};
|
|
11102
|
-
|
|
11554
|
+
const ready = this.ready(!dryRun);
|
|
11555
|
+
if (!ready.ok)
|
|
11556
|
+
return ready;
|
|
11557
|
+
const dirs = this.paths();
|
|
11558
|
+
const recovery = path9.join(this.root, ".workit", "recovery");
|
|
11559
|
+
if (fs8.existsSync(recovery)) {
|
|
11560
|
+
let files = 0;
|
|
11561
|
+
let bytes = 0;
|
|
11562
|
+
try {
|
|
11563
|
+
for (const name of fs8.readdirSync(recovery)) {
|
|
11564
|
+
const stat = fs8.lstatSync(path9.join(recovery, name));
|
|
11565
|
+
if (!stat.isFile())
|
|
11566
|
+
continue;
|
|
11567
|
+
files += 1;
|
|
11568
|
+
bytes += stat.size;
|
|
11569
|
+
}
|
|
11570
|
+
} catch {}
|
|
11571
|
+
const remove = options.pruneRecovery === true && !dryRun;
|
|
11572
|
+
if (remove)
|
|
11573
|
+
fs8.rmSync(recovery, { recursive: true, force: true });
|
|
11574
|
+
report.legacyRecovery = { path: recovery, files, bytes, removed: remove };
|
|
11575
|
+
}
|
|
11576
|
+
if (!fs8.existsSync(dirs.tasks))
|
|
11577
|
+
return success(null, null, report);
|
|
11578
|
+
const names = fs8.readdirSync(dirs.tasks).filter((name) => validId(name));
|
|
11579
|
+
for (const name of names) {
|
|
11580
|
+
const file = this.eventsPath(name);
|
|
11581
|
+
let size = 0;
|
|
11582
|
+
try {
|
|
11583
|
+
size = fs8.statSync(file).size;
|
|
11584
|
+
} catch {
|
|
11585
|
+
continue;
|
|
11586
|
+
}
|
|
11587
|
+
const lines = this.countEvents(file);
|
|
11588
|
+
if (lines > compactAbove) {
|
|
11589
|
+
if (dryRun) {
|
|
11590
|
+
report.compacted.tasks.push(name);
|
|
11591
|
+
report.compacted.eventsFolded += lines - COMPACT_KEEP;
|
|
11592
|
+
report.compacted.bytesBefore += size;
|
|
11593
|
+
} else {
|
|
11594
|
+
const compacted = this.compact(name);
|
|
11595
|
+
if (!compacted.ok)
|
|
11596
|
+
(compacted.code === "busy" ? report.retried : report.failed).push(name);
|
|
11597
|
+
else {
|
|
11598
|
+
report.compacted.tasks.push(name);
|
|
11599
|
+
report.compacted.eventsFolded += compacted.data.folded;
|
|
11600
|
+
report.compacted.bytesBefore += size;
|
|
11601
|
+
report.compacted.bytesAfter += compacted.data.bytes;
|
|
11602
|
+
}
|
|
11603
|
+
}
|
|
11604
|
+
}
|
|
11605
|
+
}
|
|
11606
|
+
const nowMs = Date.now();
|
|
11607
|
+
const sweep = () => {
|
|
11608
|
+
const references = new Set;
|
|
11609
|
+
for (const name of names)
|
|
11610
|
+
try {
|
|
11611
|
+
for (const match of fs8.readFileSync(this.eventsPath(name), "utf8").matchAll(/"\$blob":"([0-9a-f]{64})"/g))
|
|
11612
|
+
references.add(match[1]);
|
|
11613
|
+
} catch {}
|
|
11614
|
+
if (fs8.existsSync(dirs.blobs))
|
|
11615
|
+
for (const name of fs8.readdirSync(dirs.blobs)) {
|
|
11616
|
+
const digest = name.replace(/\.json$/, "");
|
|
11617
|
+
const file = path9.join(dirs.blobs, name);
|
|
11618
|
+
let stat;
|
|
11619
|
+
try {
|
|
11620
|
+
stat = fs8.lstatSync(file);
|
|
11621
|
+
} catch {
|
|
11622
|
+
continue;
|
|
11623
|
+
}
|
|
11624
|
+
if (references.has(digest) || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS) {
|
|
11625
|
+
report.blobs.kept += 1;
|
|
11626
|
+
continue;
|
|
11627
|
+
}
|
|
11628
|
+
report.blobs.removed += 1;
|
|
11629
|
+
report.blobs.removedBytes += stat.size;
|
|
11630
|
+
if (!dryRun)
|
|
11631
|
+
fs8.rmSync(file, { force: true });
|
|
11632
|
+
}
|
|
11633
|
+
return success(null, null, null);
|
|
11634
|
+
};
|
|
11635
|
+
const swept = dryRun ? sweep() : this.withLock(path9.join(dirs.dir, "gc.lock"), sweep);
|
|
11636
|
+
if (!swept.ok)
|
|
11637
|
+
return swept;
|
|
11638
|
+
const temporaryDirs = [
|
|
11639
|
+
dirs.dir,
|
|
11640
|
+
dirs.tasks,
|
|
11641
|
+
dirs.blobs,
|
|
11642
|
+
dirs.checkout,
|
|
11643
|
+
...names.map((name) => path9.join(dirs.tasks, name))
|
|
11644
|
+
];
|
|
11645
|
+
for (const directory of temporaryDirs) {
|
|
11646
|
+
if (!fs8.existsSync(directory))
|
|
11647
|
+
continue;
|
|
11648
|
+
for (const name of fs8.readdirSync(directory)) {
|
|
11649
|
+
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
11650
|
+
continue;
|
|
11651
|
+
const file = path9.join(directory, name);
|
|
11652
|
+
try {
|
|
11653
|
+
const stat = fs8.lstatSync(file);
|
|
11654
|
+
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
11655
|
+
continue;
|
|
11656
|
+
} catch {
|
|
11657
|
+
continue;
|
|
11658
|
+
}
|
|
11659
|
+
report.temporary.removed += 1;
|
|
11660
|
+
if (!dryRun)
|
|
11661
|
+
fs8.rmSync(file, { force: true });
|
|
11662
|
+
}
|
|
11663
|
+
}
|
|
11664
|
+
return success(null, null, report);
|
|
11103
11665
|
}
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
const
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11666
|
+
paths() {
|
|
11667
|
+
const location = this.location();
|
|
11668
|
+
const dir = location.ok ? location.data.dir : path9.join(this.root, ".workit");
|
|
11669
|
+
const checkout = path9.join(dir, "checkouts", checkoutSlug(this.root));
|
|
11670
|
+
return {
|
|
11671
|
+
dir,
|
|
11672
|
+
shared: location.ok ? location.data.shared : false,
|
|
11673
|
+
marker: path9.join(dir, "store.json"),
|
|
11674
|
+
checkout,
|
|
11675
|
+
workspace: path9.join(checkout, "workspace.json"),
|
|
11676
|
+
checkoutLock: path9.join(checkout, "metadata.lock"),
|
|
11677
|
+
tasks: path9.join(dir, "tasks"),
|
|
11678
|
+
index: path9.join(dir, "task-index.json"),
|
|
11679
|
+
blobs: path9.join(dir, "blobs", "candidates"),
|
|
11680
|
+
legacy: path9.join(dir, "legacy", checkoutSlug(this.root), "v2"),
|
|
11681
|
+
v2: path9.join(this.root, ".workit")
|
|
11112
11682
|
};
|
|
11113
|
-
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
11114
11683
|
}
|
|
11115
|
-
|
|
11684
|
+
eventsPath(taskId) {
|
|
11685
|
+
return path9.join(this.paths().tasks, taskId, "events.jsonl");
|
|
11686
|
+
}
|
|
11687
|
+
snapshotPath(taskId) {
|
|
11688
|
+
return path9.join(this.paths().tasks, taskId, "snapshot.json");
|
|
11689
|
+
}
|
|
11690
|
+
taskLockPath(taskId) {
|
|
11691
|
+
return path9.join(this.paths().tasks, taskId, "lock");
|
|
11692
|
+
}
|
|
11693
|
+
ready(write = false) {
|
|
11694
|
+
const location = this.location();
|
|
11695
|
+
if (!location.ok)
|
|
11696
|
+
return location;
|
|
11697
|
+
if (this.migrationChecked)
|
|
11698
|
+
return success(null, null, null);
|
|
11699
|
+
const dirs = this.paths();
|
|
11116
11700
|
try {
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11701
|
+
const marker = JSON.parse(fs8.readFileSync(dirs.marker, "utf8"));
|
|
11702
|
+
if (typeof marker.version === "number" && marker.version > STORE_FORMAT)
|
|
11703
|
+
return failure2("recovery_required", `the workit store at ${dirs.dir} has format ${marker.version}; upgrade Workit to use it`, { path: dirs.marker });
|
|
11704
|
+
} catch {}
|
|
11705
|
+
this.migrationChecked = true;
|
|
11706
|
+
const settled = this.settleMigrations(write);
|
|
11707
|
+
if (!settled.ok)
|
|
11708
|
+
this.migrationChecked = false;
|
|
11709
|
+
return settled;
|
|
11710
|
+
}
|
|
11711
|
+
settleMigrations(write) {
|
|
11712
|
+
const moved = this.adoptLocalStore();
|
|
11713
|
+
if (!moved.ok)
|
|
11714
|
+
return moved;
|
|
11715
|
+
const pending = this.pendingMigrations();
|
|
11716
|
+
if (pending.length > 0 && !write && !(this.migrateOnRead ?? migrationReporterInstalled()))
|
|
11717
|
+
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" });
|
|
11718
|
+
for (const v2 of this.v2Dirs()) {
|
|
11719
|
+
const migrated = this.migrateV2(v2);
|
|
11720
|
+
if (!migrated.ok)
|
|
11721
|
+
return migrated;
|
|
11122
11722
|
}
|
|
11123
|
-
|
|
11124
|
-
|
|
11723
|
+
return success(null, null, null);
|
|
11724
|
+
}
|
|
11725
|
+
blobStore() {
|
|
11726
|
+
const dir = this.paths().blobs;
|
|
11727
|
+
return {
|
|
11728
|
+
read: (digest) => {
|
|
11729
|
+
try {
|
|
11730
|
+
return JSON.parse(fs8.readFileSync(path9.join(dir, `${digest}.json`), "utf8"));
|
|
11731
|
+
} catch (error) {
|
|
11732
|
+
throw new LogDamage(`stored candidate ${digest} is missing or unreadable: ${String(error)}`);
|
|
11733
|
+
}
|
|
11734
|
+
},
|
|
11735
|
+
write: (digest, value) => {
|
|
11736
|
+
const file = path9.join(dir, `${digest}.json`);
|
|
11737
|
+
if (fs8.existsSync(file)) {
|
|
11738
|
+
const stamp = new Date;
|
|
11739
|
+
try {
|
|
11740
|
+
fs8.utimesSync(file, stamp, stamp);
|
|
11741
|
+
return;
|
|
11742
|
+
} catch {}
|
|
11743
|
+
}
|
|
11744
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
11745
|
+
replaceFile(file, JSON.stringify(value), true);
|
|
11746
|
+
}
|
|
11747
|
+
};
|
|
11748
|
+
}
|
|
11749
|
+
load(taskId) {
|
|
11125
11750
|
try {
|
|
11126
|
-
|
|
11751
|
+
const loaded = this.loadOrThrow(taskId);
|
|
11752
|
+
if (!loaded)
|
|
11753
|
+
return success(null, null, null);
|
|
11754
|
+
const parsed = this.parseTask(loaded.state.record, taskId);
|
|
11755
|
+
if (!parsed.ok)
|
|
11756
|
+
return parsed;
|
|
11757
|
+
return success(null, null, {
|
|
11758
|
+
record: parsed.data,
|
|
11759
|
+
key: loaded.state.key,
|
|
11760
|
+
legacy: loaded.state.legacy,
|
|
11761
|
+
seq: loaded.seq,
|
|
11762
|
+
lastId: loaded.lastId,
|
|
11763
|
+
lastStart: loaded.lastStart,
|
|
11764
|
+
end: loaded.end,
|
|
11765
|
+
size: loaded.size,
|
|
11766
|
+
snapshotSeq: loaded.snapshotSeq
|
|
11767
|
+
});
|
|
11127
11768
|
} catch (error) {
|
|
11128
|
-
|
|
11769
|
+
if (error instanceof LogDamage)
|
|
11770
|
+
return failure2("recovery_required", error.upgrade ? error.message : `task ${taskId} log is damaged: ${error.message}`, { taskId, path: this.eventsPath(taskId) });
|
|
11771
|
+
return failure2("recovery_required", `task ${taskId} cannot be read: ${String(error)}`, {
|
|
11772
|
+
taskId
|
|
11773
|
+
});
|
|
11129
11774
|
}
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11775
|
+
}
|
|
11776
|
+
loadOrThrow(taskId) {
|
|
11777
|
+
const blobs = this.blobStore();
|
|
11778
|
+
const file = this.eventsPath(taskId);
|
|
11779
|
+
const snapshot = this.readSnapshot(taskId);
|
|
11780
|
+
if (snapshot) {
|
|
11133
11781
|
try {
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
}
|
|
11142
|
-
|
|
11143
|
-
|
|
11782
|
+
const tail = readLog(file, snapshot.lastStart, snapshot.seq);
|
|
11783
|
+
const first = tail?.events[0];
|
|
11784
|
+
if (tail && first && first.id === snapshot.lastId) {
|
|
11785
|
+
let state = {
|
|
11786
|
+
record: snapshot.record,
|
|
11787
|
+
key: snapshot.key,
|
|
11788
|
+
legacy: snapshot.legacy
|
|
11789
|
+
};
|
|
11790
|
+
for (const event of tail.events.slice(1))
|
|
11791
|
+
state = reduce(state, event, blobs);
|
|
11792
|
+
return this.loadedFrom(state, tail, snapshot.seq);
|
|
11144
11793
|
}
|
|
11145
11794
|
} catch (error) {
|
|
11146
|
-
|
|
11795
|
+
if (error instanceof LogDamage && error.upgrade)
|
|
11796
|
+
throw error;
|
|
11147
11797
|
}
|
|
11148
11798
|
}
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
const releaseFailure = this.lockFailure(error);
|
|
11155
|
-
const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
|
|
11156
|
-
result = result.ok ? releaseFailure : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
11157
|
-
}
|
|
11799
|
+
const log = readLog(file, 0, null);
|
|
11800
|
+
if (!log || log.events.length === 0) {
|
|
11801
|
+
if (log && log.size > 0)
|
|
11802
|
+
throw new LogDamage("the log holds no complete event");
|
|
11803
|
+
return null;
|
|
11158
11804
|
}
|
|
11159
|
-
|
|
11805
|
+
if (!OPENING_TYPES.has(log.events[0].type))
|
|
11806
|
+
throw new LogDamage(`the log starts with ${log.events[0].type}, not an opening event`);
|
|
11807
|
+
let state = null;
|
|
11808
|
+
for (const event of log.events)
|
|
11809
|
+
state = reduce(state, event, blobs);
|
|
11810
|
+
return this.loadedFrom(state, log, 0);
|
|
11811
|
+
}
|
|
11812
|
+
loadedFrom(state, log, snapshotSeq) {
|
|
11813
|
+
const last = log.events.at(-1);
|
|
11814
|
+
return {
|
|
11815
|
+
state,
|
|
11816
|
+
seq: last.seq,
|
|
11817
|
+
lastId: last.id,
|
|
11818
|
+
lastStart: log.lastStart,
|
|
11819
|
+
end: log.end,
|
|
11820
|
+
size: log.size,
|
|
11821
|
+
snapshotSeq
|
|
11822
|
+
};
|
|
11160
11823
|
}
|
|
11161
|
-
|
|
11162
|
-
const probe = `${this.lockPath}.${process.pid}.${randomUUID2()}.probe`;
|
|
11824
|
+
readSnapshot(taskId) {
|
|
11163
11825
|
try {
|
|
11164
|
-
|
|
11826
|
+
const value = JSON.parse(fs8.readFileSync(this.snapshotPath(taskId), "utf8"));
|
|
11827
|
+
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))
|
|
11828
|
+
return null;
|
|
11829
|
+
return value;
|
|
11165
11830
|
} catch {
|
|
11166
|
-
return
|
|
11831
|
+
return null;
|
|
11167
11832
|
}
|
|
11833
|
+
}
|
|
11834
|
+
writeSnapshot(taskId, loaded) {
|
|
11835
|
+
const snapshot = {
|
|
11836
|
+
v: 1,
|
|
11837
|
+
task: taskId,
|
|
11838
|
+
seq: loaded.seq,
|
|
11839
|
+
lastId: loaded.lastId,
|
|
11840
|
+
lastStart: loaded.lastStart,
|
|
11841
|
+
end: loaded.end,
|
|
11842
|
+
key: loaded.key,
|
|
11843
|
+
legacy: loaded.legacy,
|
|
11844
|
+
record: loaded.record
|
|
11845
|
+
};
|
|
11168
11846
|
try {
|
|
11169
|
-
|
|
11847
|
+
replaceFile(this.snapshotPath(taskId), JSON.stringify(snapshot), false);
|
|
11170
11848
|
} catch {}
|
|
11171
|
-
return true;
|
|
11172
11849
|
}
|
|
11173
|
-
|
|
11850
|
+
loadBound(taskId) {
|
|
11851
|
+
const loaded = this.load(taskId);
|
|
11852
|
+
if (!loaded.ok)
|
|
11853
|
+
return loaded;
|
|
11854
|
+
if (!loaded.data)
|
|
11855
|
+
return failure2("not_found", "task not found", { taskId });
|
|
11856
|
+
const workspace = this.readWorkspace();
|
|
11857
|
+
if (!workspace.ok)
|
|
11858
|
+
return workspace;
|
|
11859
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
11860
|
+
return bound ?? success(null, null, loaded.data);
|
|
11861
|
+
}
|
|
11862
|
+
bindingError(task, workspace) {
|
|
11863
|
+
if (workspace && task.workspaceId === workspace.id)
|
|
11864
|
+
return null;
|
|
11865
|
+
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 });
|
|
11866
|
+
}
|
|
11867
|
+
nextTask(current, changed, context) {
|
|
11868
|
+
const valid = taskRecordSchema.safeParse({
|
|
11869
|
+
...changed,
|
|
11870
|
+
id: current.id,
|
|
11871
|
+
workspaceId: current.workspaceId,
|
|
11872
|
+
createdAt: current.createdAt,
|
|
11873
|
+
revision: context.revision,
|
|
11874
|
+
updatedAt: context.now,
|
|
11875
|
+
runtime: stampNew(current)
|
|
11876
|
+
});
|
|
11877
|
+
return valid.success ? valid.data : null;
|
|
11878
|
+
}
|
|
11879
|
+
event(taskId, seq, type, data) {
|
|
11880
|
+
return {
|
|
11881
|
+
v: EVENT_VERSION,
|
|
11882
|
+
seq,
|
|
11883
|
+
at: new Date().toISOString(),
|
|
11884
|
+
id: newId(),
|
|
11885
|
+
task: taskId,
|
|
11886
|
+
actor: this.actor,
|
|
11887
|
+
type,
|
|
11888
|
+
data
|
|
11889
|
+
};
|
|
11890
|
+
}
|
|
11891
|
+
appendPatch(loaded, next, extra = {}) {
|
|
11892
|
+
const taskId = next.id;
|
|
11174
11893
|
try {
|
|
11175
|
-
|
|
11176
|
-
|
|
11894
|
+
const blobs = this.blobStore();
|
|
11895
|
+
const before = loaded.record;
|
|
11896
|
+
const after = next;
|
|
11897
|
+
const ops = encodeOps(diff(before, after), blobs);
|
|
11898
|
+
const { type, ...data } = extra;
|
|
11899
|
+
if (data.key)
|
|
11900
|
+
data.key = { ...data.key, boundAt: data.key.boundAt ?? new Date().toISOString() };
|
|
11901
|
+
const event = this.event(taskId, loaded.seq + 1, type ?? ("key" in extra ? "task.bound" : eventType(before, after)), { ops, ...data });
|
|
11902
|
+
const start = appendEvent(this.eventsPath(taskId), event, loaded.end);
|
|
11903
|
+
const end = start + Buffer.byteLength(`${JSON.stringify(event)}
|
|
11904
|
+
`);
|
|
11905
|
+
const key = "key" in data ? data.key ?? null : loaded.key;
|
|
11906
|
+
const state = {
|
|
11907
|
+
record: next,
|
|
11908
|
+
key,
|
|
11909
|
+
legacy: extra.legacy ?? loaded.legacy,
|
|
11910
|
+
seq: event.seq,
|
|
11911
|
+
lastId: event.id,
|
|
11912
|
+
lastStart: start,
|
|
11913
|
+
end
|
|
11914
|
+
};
|
|
11915
|
+
if (event.seq - loaded.snapshotSeq >= SNAPSHOT_EVERY || next.status !== loaded.record.status || loaded.snapshotSeq === 0)
|
|
11916
|
+
this.writeSnapshot(taskId, state);
|
|
11917
|
+
if (end > AUTO_COMPACT_BYTES)
|
|
11918
|
+
this.compactDue.add(taskId);
|
|
11919
|
+
this.indexTask(next, key, (extra.legacy ?? loaded.legacy) !== null);
|
|
11920
|
+
return success(null, null, null);
|
|
11177
11921
|
} catch (error) {
|
|
11178
|
-
return error
|
|
11922
|
+
return failure2("storage_error", `task append failed: ${String(error)}`, {
|
|
11923
|
+
path: this.eventsPath(taskId)
|
|
11924
|
+
});
|
|
11179
11925
|
}
|
|
11180
11926
|
}
|
|
11181
|
-
|
|
11182
|
-
const
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11927
|
+
openTask(record, type, key, legacy = null) {
|
|
11928
|
+
const dir = path9.join(this.paths().tasks, record.id);
|
|
11929
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
11930
|
+
const blobs = this.blobStore();
|
|
11931
|
+
const bound = key ? { ...key, boundAt: key.boundAt ?? new Date().toISOString() } : null;
|
|
11932
|
+
const event = this.event(record.id, 1, type, {
|
|
11933
|
+
record: encodeRecord(record, blobs),
|
|
11934
|
+
key: bound,
|
|
11935
|
+
...legacy ? { legacy } : {}
|
|
11936
|
+
});
|
|
11937
|
+
const file = this.eventsPath(record.id);
|
|
11938
|
+
const existing = readLog(file, 0, null);
|
|
11939
|
+
if (existing && existing.events.length > 0)
|
|
11940
|
+
throw new Error(`task ${record.id} already has a log`);
|
|
11941
|
+
appendEvent(file, event, 0);
|
|
11942
|
+
this.indexTask(record, bound, legacy !== null);
|
|
11943
|
+
}
|
|
11944
|
+
compactPending() {
|
|
11945
|
+
for (const taskId of this.compactDue) {
|
|
11946
|
+
this.compactDue.delete(taskId);
|
|
11947
|
+
this.compact(taskId);
|
|
11948
|
+
}
|
|
11949
|
+
}
|
|
11950
|
+
compact(taskId) {
|
|
11951
|
+
try {
|
|
11952
|
+
const file = this.eventsPath(taskId);
|
|
11953
|
+
const log = readLog(file, 0, null);
|
|
11954
|
+
if (!log || log.events.length <= COMPACT_KEEP + 1)
|
|
11955
|
+
return success(null, null, { folded: 0, bytes: log?.size ?? 0 });
|
|
11956
|
+
const blobs = this.blobStore();
|
|
11957
|
+
const cut = log.events.length - COMPACT_KEEP;
|
|
11958
|
+
let state = null;
|
|
11959
|
+
for (const event of log.events.slice(0, cut))
|
|
11960
|
+
state = reduce(state, event, blobs);
|
|
11961
|
+
const folded = log.events[cut - 1];
|
|
11962
|
+
const checkpoint = {
|
|
11963
|
+
...this.event(taskId, folded.seq, "task.checkpoint", {
|
|
11964
|
+
record: encodeRecord(state.record, blobs),
|
|
11965
|
+
key: state.key,
|
|
11966
|
+
...state.legacy ? { legacy: state.legacy } : {},
|
|
11967
|
+
folded: { from: log.events[0].seq, to: folded.seq }
|
|
11968
|
+
})
|
|
11969
|
+
};
|
|
11970
|
+
const prefix = `${[checkpoint, ...log.events.slice(cut)].map((event) => JSON.stringify(event)).join(`
|
|
11971
|
+
`)}
|
|
11972
|
+
`;
|
|
11973
|
+
const last = log.events.at(-1);
|
|
11974
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
11975
|
+
let latest = null;
|
|
11976
|
+
try {
|
|
11977
|
+
latest = readLog(file, log.lastStart, last.seq);
|
|
11978
|
+
} catch {}
|
|
11979
|
+
if (!latest || latest.events[0]?.id !== last.id)
|
|
11980
|
+
return failure2("busy", "the task log changed during compaction; retry", { taskId });
|
|
11981
|
+
const appended = fs8.readFileSync(file).subarray(log.end, latest.end);
|
|
11982
|
+
const bytes = Buffer.concat([Buffer.from(prefix, "utf8"), appended]);
|
|
11983
|
+
replaceFile(file, bytes.toString("utf8"), true);
|
|
11984
|
+
try {
|
|
11985
|
+
fs8.rmSync(this.snapshotPath(taskId), { force: true });
|
|
11986
|
+
} catch {}
|
|
11987
|
+
const reloaded = this.load(taskId);
|
|
11988
|
+
if (reloaded.ok && reloaded.data)
|
|
11989
|
+
this.writeSnapshot(taskId, reloaded.data);
|
|
11990
|
+
return success(null, null, { folded: cut, bytes: bytes.length });
|
|
11188
11991
|
});
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11203
|
-
|
|
11992
|
+
} catch (error) {
|
|
11993
|
+
return failure2("storage_error", `compaction failed: ${String(error)}`, { taskId });
|
|
11994
|
+
}
|
|
11995
|
+
}
|
|
11996
|
+
countEvents(file) {
|
|
11997
|
+
try {
|
|
11998
|
+
const bytes = fs8.readFileSync(file);
|
|
11999
|
+
let count = 0;
|
|
12000
|
+
for (let index = bytes.indexOf(10);index >= 0; index = bytes.indexOf(10, index + 1))
|
|
12001
|
+
count += 1;
|
|
12002
|
+
return count;
|
|
12003
|
+
} catch {
|
|
12004
|
+
return 0;
|
|
12005
|
+
}
|
|
12006
|
+
}
|
|
12007
|
+
openTasksForKey(key) {
|
|
12008
|
+
const all = this.listStoreIndex();
|
|
12009
|
+
if (!all.ok)
|
|
12010
|
+
return all;
|
|
12011
|
+
const location = this.location();
|
|
12012
|
+
const renames = location.ok ? branchRenames(location.data) : [];
|
|
12013
|
+
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));
|
|
12014
|
+
}
|
|
12015
|
+
openTaskForKey(key) {
|
|
12016
|
+
const bound = this.openTasksForKey(key);
|
|
12017
|
+
return bound.ok ? success(null, null, bound.data[0] ?? null) : bound;
|
|
12018
|
+
}
|
|
12019
|
+
withKeyLock(key, operation) {
|
|
12020
|
+
const digest = createHash6("sha256").update(key).digest("hex").slice(0, 32);
|
|
12021
|
+
return this.withLock(path9.join(this.paths().dir, "keys", `${digest}.lock`), operation);
|
|
12022
|
+
}
|
|
12023
|
+
bindLocked(taskId, key, timestamp) {
|
|
12024
|
+
const current = this.readWorkspace();
|
|
12025
|
+
if (!current.ok)
|
|
12026
|
+
return current;
|
|
12027
|
+
let workspace = current.data;
|
|
12028
|
+
if (!workspace) {
|
|
12029
|
+
workspace = this.nextWorkspace(null);
|
|
12030
|
+
const written = this.writeWorkspace(workspace);
|
|
12031
|
+
if (!written.ok)
|
|
12032
|
+
return written;
|
|
12033
|
+
}
|
|
12034
|
+
const target = workspace;
|
|
12035
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
12036
|
+
const loaded = this.load(taskId);
|
|
12037
|
+
if (!loaded.ok)
|
|
12038
|
+
return loaded;
|
|
12039
|
+
if (!loaded.data)
|
|
12040
|
+
return failure2("not_found", "task not found", { taskId });
|
|
12041
|
+
if (loaded.data.record.status === "closed")
|
|
12042
|
+
return failure2("invalid_transition", "a closed task cannot be adopted", { taskId });
|
|
12043
|
+
if (loaded.data.record.workspaceId === target.id && loaded.data.key?.key === key.key)
|
|
12044
|
+
return success(loaded.data.record.revision, null, loaded.data.record);
|
|
12045
|
+
const next = taskRecordSchema.safeParse({
|
|
12046
|
+
...loaded.data.record,
|
|
12047
|
+
workspaceId: target.id,
|
|
12048
|
+
revision: newRevision(),
|
|
12049
|
+
updatedAt: timestamp ?? now(),
|
|
12050
|
+
runtime: stampNew(loaded.data.record)
|
|
12051
|
+
});
|
|
12052
|
+
if (!next.success)
|
|
12053
|
+
return failure2("invalid_input", "rebound task is invalid");
|
|
12054
|
+
const written = this.appendPatch(loaded.data, next.data, { key });
|
|
12055
|
+
return written.ok ? success(next.data.revision, null, next.data) : written;
|
|
12056
|
+
});
|
|
12057
|
+
}
|
|
12058
|
+
unbindLocked(taskId) {
|
|
12059
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
12060
|
+
const loaded = this.load(taskId);
|
|
12061
|
+
if (!loaded.ok)
|
|
12062
|
+
return loaded;
|
|
12063
|
+
if (!loaded.data || loaded.data.key === null)
|
|
12064
|
+
return success(null, null, null);
|
|
12065
|
+
return this.appendPatch(loaded.data, loaded.data.record, { key: null });
|
|
12066
|
+
});
|
|
12067
|
+
}
|
|
12068
|
+
createLocked(value, bindCurrent, key, checkRevision = true) {
|
|
12069
|
+
const current = this.readWorkspace();
|
|
12070
|
+
if (!current.ok)
|
|
12071
|
+
return current;
|
|
12072
|
+
if (checkRevision && (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null))
|
|
12073
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
12074
|
+
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
12075
|
+
actualWorkspaceRevision: current.data?.revision ?? null
|
|
12076
|
+
});
|
|
12077
|
+
if (!value.provenance)
|
|
12078
|
+
return failure2("invalid_input", "provenance is required");
|
|
12079
|
+
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
12080
|
+
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
12081
|
+
if (bindCurrent) {
|
|
12082
|
+
const currentKey = this.currentKey();
|
|
12083
|
+
if (currentKey.ok)
|
|
12084
|
+
return this.withKeyLock(currentKey.data.key, () => {
|
|
12085
|
+
const holder = this.openTaskForKey(currentKey.data.key);
|
|
12086
|
+
if (!holder.ok)
|
|
12087
|
+
return holder;
|
|
12088
|
+
if (holder.data) {
|
|
12089
|
+
const released = this.unbindLocked(holder.data.id);
|
|
12090
|
+
if (!released.ok)
|
|
12091
|
+
return released;
|
|
12092
|
+
}
|
|
12093
|
+
return this.newTask(value, current.data, currentKey.data);
|
|
11204
12094
|
});
|
|
11205
12095
|
}
|
|
11206
|
-
|
|
11207
|
-
|
|
12096
|
+
return this.newTask(value, current.data, key);
|
|
12097
|
+
}
|
|
12098
|
+
newTask(value, previous, binding) {
|
|
12099
|
+
const current = { data: previous };
|
|
12100
|
+
const workspace = this.nextWorkspace(current.data);
|
|
12101
|
+
const timestamp = value.now ?? now();
|
|
12102
|
+
const task = {
|
|
12103
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12104
|
+
id: newId(),
|
|
12105
|
+
workspaceId: workspace.id,
|
|
12106
|
+
revision: newRevision(),
|
|
12107
|
+
createdAt: timestamp,
|
|
12108
|
+
updatedAt: timestamp,
|
|
12109
|
+
origin: null,
|
|
12110
|
+
intent: {
|
|
12111
|
+
id: newId(),
|
|
12112
|
+
recordedAt: timestamp,
|
|
12113
|
+
provenance: value.provenance,
|
|
12114
|
+
data: value.intent
|
|
12115
|
+
},
|
|
12116
|
+
constraints: [],
|
|
12117
|
+
status: "active",
|
|
12118
|
+
closure: null,
|
|
12119
|
+
progress: { summary: "", nextAction: null, blockers: [] },
|
|
12120
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
12121
|
+
assessments: [],
|
|
12122
|
+
policy: null,
|
|
12123
|
+
policyChanges: [],
|
|
12124
|
+
candidates: [],
|
|
12125
|
+
evidence: [],
|
|
12126
|
+
decisions: [],
|
|
12127
|
+
findings: [],
|
|
12128
|
+
workers: []
|
|
12129
|
+
};
|
|
12130
|
+
return this.writeNewTask(current.data, workspace, task, "task.opened", binding, {
|
|
12131
|
+
operation: "create"
|
|
12132
|
+
});
|
|
12133
|
+
}
|
|
12134
|
+
nextWorkspace(current, id) {
|
|
12135
|
+
return current ? { ...current, revision: newRevision(), root: this.root, runtime: stampNew(current) } : {
|
|
12136
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12137
|
+
id: id ?? newId(),
|
|
12138
|
+
revision: newRevision(),
|
|
12139
|
+
root: this.root,
|
|
12140
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
12141
|
+
writer: null
|
|
12142
|
+
};
|
|
12143
|
+
}
|
|
12144
|
+
writeNewTask(previous, workspace, task, type, key, details) {
|
|
12145
|
+
const written = this.writeWorkspace(workspace);
|
|
12146
|
+
if (!written.ok)
|
|
12147
|
+
return written;
|
|
12148
|
+
try {
|
|
12149
|
+
this.openTask(task, type, key);
|
|
12150
|
+
} catch (error) {
|
|
12151
|
+
if (previous) {
|
|
12152
|
+
const restored = this.writeWorkspace(previous);
|
|
12153
|
+
if (!restored.ok)
|
|
12154
|
+
return failure2("external_outcome_unknown", `task ${details.operation} failed and workspace restoration is uncertain`, { ...details, outcome: "unknown" });
|
|
12155
|
+
}
|
|
12156
|
+
return failure2("external_outcome_unknown", `workspace written but the task log is uncertain: ${String(error)}`, { ...details, outcome: "unknown" });
|
|
12157
|
+
}
|
|
12158
|
+
return success(task.revision, workspace.revision, task);
|
|
12159
|
+
}
|
|
12160
|
+
writeWorkspace(record) {
|
|
12161
|
+
const file = this.paths().workspace;
|
|
12162
|
+
try {
|
|
12163
|
+
fs8.mkdirSync(path9.dirname(file), { recursive: true });
|
|
12164
|
+
replaceFile(file, `${canonicalJson(record)}
|
|
12165
|
+
`, true);
|
|
12166
|
+
return success(null, null, null);
|
|
12167
|
+
} catch (error) {
|
|
12168
|
+
return failure2("storage_error", `workspace write failed: ${String(error)}`, { path: file });
|
|
12169
|
+
}
|
|
12170
|
+
}
|
|
12171
|
+
markUncertain(workspace) {
|
|
12172
|
+
if (!workspace.writer || workspace.writer.state === "uncertain")
|
|
12173
|
+
return;
|
|
12174
|
+
this.writeWorkspace({
|
|
12175
|
+
...workspace,
|
|
12176
|
+
writer: { ...workspace.writer, state: "uncertain" },
|
|
12177
|
+
revision: newRevision(),
|
|
12178
|
+
runtime: stampNew(workspace)
|
|
12179
|
+
});
|
|
11208
12180
|
}
|
|
11209
|
-
|
|
11210
|
-
retryTransient(() => fs6.mkdirSync(this.tasksDir, { recursive: true }));
|
|
11211
|
-
retryTransient(() => fs6.mkdirSync(this.recoveryDir, { recursive: true }));
|
|
11212
|
-
let current = null;
|
|
12181
|
+
readIndex() {
|
|
11213
12182
|
try {
|
|
11214
|
-
|
|
12183
|
+
const value = JSON.parse(fs8.readFileSync(this.paths().index, "utf8"));
|
|
12184
|
+
if (!isObject4(value) || value.version !== INDEX_VERSION || !isObject4(value.tasks))
|
|
12185
|
+
return {};
|
|
12186
|
+
const tasks = {};
|
|
12187
|
+
for (const [id, entry] of Object.entries(value.tasks))
|
|
12188
|
+
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))
|
|
12189
|
+
tasks[id] = entry;
|
|
12190
|
+
return tasks;
|
|
12191
|
+
} catch {
|
|
12192
|
+
return {};
|
|
12193
|
+
}
|
|
12194
|
+
}
|
|
12195
|
+
writeIndex(entries) {
|
|
12196
|
+
try {
|
|
12197
|
+
fs8.mkdirSync(this.paths().tasks, { recursive: true });
|
|
12198
|
+
replaceFile(this.paths().index, JSON.stringify({
|
|
12199
|
+
version: INDEX_VERSION,
|
|
12200
|
+
tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
|
|
12201
|
+
}), false);
|
|
11215
12202
|
} catch {}
|
|
11216
|
-
if (current !== `*
|
|
11217
|
-
`)
|
|
11218
|
-
retryTransient(() => fs6.writeFileSync(this.gitignorePath, `*
|
|
11219
|
-
`));
|
|
11220
12203
|
}
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
if (
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
12204
|
+
indexTask(task, key, legacy) {
|
|
12205
|
+
try {
|
|
12206
|
+
const signature = fileSignature(this.eventsPath(task.id));
|
|
12207
|
+
if (signature === null)
|
|
12208
|
+
return;
|
|
12209
|
+
const tasks = this.readIndex();
|
|
12210
|
+
tasks[task.id] = indexEntry(task, key, legacy, signature);
|
|
12211
|
+
this.writeIndex(Object.values(tasks));
|
|
12212
|
+
} catch {}
|
|
12213
|
+
}
|
|
12214
|
+
parseTask(record, taskId) {
|
|
12215
|
+
const parsed = this.parseValue(record, taskRecordSchema, "task");
|
|
12216
|
+
if (!parsed.ok)
|
|
12217
|
+
return { ...parsed, details: { ...parsed.details, taskId } };
|
|
12218
|
+
if (parsed.data.id !== taskId)
|
|
12219
|
+
return failure2("recovery_required", "task log and record ID differ", { taskId });
|
|
12220
|
+
return parsed;
|
|
12221
|
+
}
|
|
12222
|
+
parseRecord(bytes, schema, label) {
|
|
12223
|
+
let value;
|
|
12224
|
+
try {
|
|
12225
|
+
value = JSON.parse(bytes);
|
|
12226
|
+
} catch {
|
|
12227
|
+
return failure2("recovery_required", `${label} record JSON is corrupt`);
|
|
12228
|
+
}
|
|
12229
|
+
return this.parseValue(value, schema, label);
|
|
12230
|
+
}
|
|
12231
|
+
parseValue(value, schema, label) {
|
|
12232
|
+
if (isObject4(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
12233
|
+
return failure2("unsupported_version", `unsupported ${label} schema version`);
|
|
12234
|
+
const parsed = parseStoredRecord(schema, value);
|
|
12235
|
+
if (parsed.success)
|
|
12236
|
+
return success(null, null, parsed.data);
|
|
12237
|
+
const writerVersion = isObject4(value) && isObject4(value.runtime) ? value.runtime.updatedWith : null;
|
|
12238
|
+
if (parsed.critical.length > 0)
|
|
12239
|
+
return failure2("recovery_required", `${label} record requires fields this Workit cannot read (${parsed.critical.join(", ")}); upgrade Workit before mutating this checkout`);
|
|
12240
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
12241
|
+
return failure2("recovery_required", `${label} record was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
12242
|
+
return failure2("recovery_required", `${label} record does not satisfy its schema`);
|
|
12243
|
+
}
|
|
12244
|
+
conflict(expected, actual) {
|
|
12245
|
+
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", { expectedRevision: expected, actualRevision: actual });
|
|
12246
|
+
}
|
|
12247
|
+
workspaceConflict(expected, actual) {
|
|
12248
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
12249
|
+
}
|
|
12250
|
+
v2Dirs() {
|
|
12251
|
+
return [...new Set([this.root, this.origin])].map((dir) => path9.join(dir, ".workit"));
|
|
12252
|
+
}
|
|
12253
|
+
pendingMigrations() {
|
|
12254
|
+
return this.v2Dirs().filter((dir) => {
|
|
12255
|
+
const raw = this.readV2Workspace(path9.join(dir, "workspace.json"));
|
|
12256
|
+
return raw !== null && raw !== TOMBSTONE;
|
|
12257
|
+
});
|
|
12258
|
+
}
|
|
12259
|
+
migrateV2(v2) {
|
|
12260
|
+
const dirs = this.paths();
|
|
12261
|
+
const v2Workspace = path9.join(v2, "workspace.json");
|
|
12262
|
+
const v2Tasks = path9.join(v2, "tasks");
|
|
12263
|
+
const legacy = path9.join(dirs.dir, "legacy", checkoutSlug(path9.dirname(v2)), "v2");
|
|
12264
|
+
const raw = this.readV2Workspace(v2Workspace);
|
|
12265
|
+
if (raw === null || raw === TOMBSTONE)
|
|
12266
|
+
return success(null, null, null);
|
|
12267
|
+
return this.withLock(dirs.checkoutLock, () => this.withLock(path9.join(v2, "metadata.lock"), () => {
|
|
12268
|
+
const workspace = this.readV2Workspace(v2Workspace);
|
|
12269
|
+
if (workspace === null || workspace === TOMBSTONE)
|
|
12270
|
+
return success(null, null, null);
|
|
12271
|
+
const parsedWorkspace = this.parseRecord(workspace.bytes, workspaceRecordSchema, "2.x workspace");
|
|
12272
|
+
if (!parsedWorkspace.ok)
|
|
12273
|
+
return failure2("recovery_required", `cannot migrate the 2.x store at ${v2}: ${parsedWorkspace.error}; move it aside to start fresh`, { path: v2Workspace });
|
|
12274
|
+
let files = [];
|
|
12275
|
+
try {
|
|
12276
|
+
files = fs8.readdirSync(v2Tasks).filter((name) => name.endsWith(".json") && validId(name.slice(0, -5))).filter((name) => !this.isStub(path9.join(v2Tasks, name)));
|
|
12277
|
+
} catch {}
|
|
12278
|
+
const current = this.readWorkspace();
|
|
12279
|
+
if (!current.ok)
|
|
12280
|
+
return current;
|
|
12281
|
+
let workspaceMoved = false;
|
|
12282
|
+
let target = current.data;
|
|
12283
|
+
if (!target) {
|
|
12284
|
+
target = { ...parsedWorkspace.data, root: this.root };
|
|
12285
|
+
const written = this.writeWorkspace(target);
|
|
12286
|
+
if (!written.ok)
|
|
12287
|
+
return written;
|
|
12288
|
+
workspaceMoved = true;
|
|
12289
|
+
}
|
|
12290
|
+
let migrated = 0;
|
|
12291
|
+
let skipped = 0;
|
|
12292
|
+
const done = new Map;
|
|
12293
|
+
try {
|
|
12294
|
+
fs8.mkdirSync(path9.join(legacy, "tasks"), { recursive: true });
|
|
12295
|
+
for (const name of ["workspace.json", "index.json"])
|
|
12296
|
+
if (fs8.existsSync(path9.join(v2, name)))
|
|
12297
|
+
fs8.copyFileSync(path9.join(v2, name), path9.join(legacy, name));
|
|
12298
|
+
for (const name of files) {
|
|
12299
|
+
const id = name.slice(0, -5);
|
|
12300
|
+
const file = path9.join(v2Tasks, name);
|
|
12301
|
+
const bytes = fs8.readFileSync(file, "utf8");
|
|
12302
|
+
const digest = sha256(bytes);
|
|
12303
|
+
fs8.writeFileSync(path9.join(legacy, "tasks", name), bytes);
|
|
12304
|
+
const parsed = this.parseRecord(bytes, taskRecordSchema, "2.x task");
|
|
12305
|
+
if (!parsed.ok || parsed.data.id !== id)
|
|
12306
|
+
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 });
|
|
12307
|
+
const record = { ...parsed.data, workspaceId: target.id };
|
|
12308
|
+
const origin = { path: path9.join(legacy, "tasks", name), digest };
|
|
12309
|
+
if (!fs8.existsSync(this.eventsPath(id))) {
|
|
12310
|
+
this.openTask(record, "migrated.from_v2", null, origin);
|
|
12311
|
+
migrated += 1;
|
|
12312
|
+
} else {
|
|
12313
|
+
const resynced = this.withLock(this.taskLockPath(id), () => {
|
|
12314
|
+
const loaded = this.load(id);
|
|
12315
|
+
if (!loaded.ok)
|
|
12316
|
+
return loaded;
|
|
12317
|
+
if (!loaded.data)
|
|
12318
|
+
return failure2("not_found", "task not found", { taskId: id });
|
|
12319
|
+
if (loaded.data.legacy?.digest === digest)
|
|
12320
|
+
return success(null, null, false);
|
|
12321
|
+
const appended = this.appendPatch(loaded.data, record, {
|
|
12322
|
+
type: "migrated.from_v2",
|
|
12323
|
+
legacy: origin
|
|
12324
|
+
});
|
|
12325
|
+
return appended.ok ? success(null, null, true) : appended;
|
|
12326
|
+
});
|
|
12327
|
+
if (!resynced.ok)
|
|
12328
|
+
return resynced;
|
|
12329
|
+
if (resynced.data)
|
|
12330
|
+
migrated += 1;
|
|
12331
|
+
else
|
|
12332
|
+
skipped += 1;
|
|
12333
|
+
}
|
|
12334
|
+
done.set(name, digest);
|
|
12335
|
+
}
|
|
12336
|
+
} catch (error) {
|
|
12337
|
+
return failure2("storage_error", `cannot migrate the 2.x store at ${v2}: ${String(error)}`, {
|
|
12338
|
+
path: v2
|
|
12339
|
+
});
|
|
12340
|
+
}
|
|
12341
|
+
try {
|
|
12342
|
+
let complete = true;
|
|
12343
|
+
for (const [name, digest] of done) {
|
|
12344
|
+
const file = path9.join(v2Tasks, name);
|
|
12345
|
+
if (sha256(fs8.readFileSync(file, "utf8")) !== digest) {
|
|
12346
|
+
complete = false;
|
|
12347
|
+
continue;
|
|
12348
|
+
}
|
|
12349
|
+
replaceFile(file, this.stubFor(name.slice(0, -5)), false);
|
|
12350
|
+
}
|
|
12351
|
+
if (!complete)
|
|
12352
|
+
return failure2("busy", `the 2.x store at ${v2} changed during migration; retry`, {
|
|
12353
|
+
path: v2
|
|
12354
|
+
});
|
|
12355
|
+
fs8.rmSync(path9.join(v2, "index.json"), { force: true });
|
|
12356
|
+
this.writeTombstone(v2, target.id);
|
|
12357
|
+
} catch (error) {
|
|
12358
|
+
return failure2("storage_error", `cannot finish the 2.x migration: ${String(error)}`, {
|
|
12359
|
+
path: v2
|
|
12360
|
+
});
|
|
12361
|
+
}
|
|
12362
|
+
reportMigration({
|
|
12363
|
+
from: v2,
|
|
12364
|
+
to: dirs.dir,
|
|
12365
|
+
backup: legacy,
|
|
12366
|
+
tasks: migrated,
|
|
12367
|
+
skipped,
|
|
12368
|
+
workspace: workspaceMoved
|
|
12369
|
+
});
|
|
12370
|
+
return success(null, null, null);
|
|
12371
|
+
}));
|
|
12372
|
+
}
|
|
12373
|
+
adoptLocalStore() {
|
|
12374
|
+
const dirs = this.paths();
|
|
12375
|
+
const marker = path9.join(dirs.v2, "store.json");
|
|
12376
|
+
if (!dirs.shared || !fs8.existsSync(marker))
|
|
12377
|
+
return success(null, null, null);
|
|
12378
|
+
return this.withLock(dirs.checkoutLock, () => {
|
|
12379
|
+
if (!fs8.existsSync(marker))
|
|
12380
|
+
return success(null, null, null);
|
|
12381
|
+
let tasks = 0;
|
|
11232
12382
|
try {
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
12383
|
+
const merge = (from, to) => {
|
|
12384
|
+
if (!fs8.existsSync(to)) {
|
|
12385
|
+
fs8.mkdirSync(path9.dirname(to), { recursive: true });
|
|
12386
|
+
try {
|
|
12387
|
+
fs8.renameSync(from, to);
|
|
12388
|
+
} catch {
|
|
12389
|
+
fs8.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
|
|
12390
|
+
fs8.rmSync(from, { recursive: true, force: true });
|
|
12391
|
+
}
|
|
12392
|
+
return;
|
|
12393
|
+
}
|
|
12394
|
+
if (fs8.lstatSync(from).isDirectory() && fs8.lstatSync(to).isDirectory())
|
|
12395
|
+
for (const name of fs8.readdirSync(from))
|
|
12396
|
+
merge(path9.join(from, name), path9.join(to, name));
|
|
12397
|
+
};
|
|
12398
|
+
for (const sub of ["tasks", "checkouts", "blobs", "legacy"]) {
|
|
12399
|
+
const from = path9.join(dirs.v2, sub);
|
|
12400
|
+
if (!fs8.existsSync(from))
|
|
12401
|
+
continue;
|
|
12402
|
+
if (sub === "tasks") {
|
|
12403
|
+
for (const name of fs8.readdirSync(from))
|
|
12404
|
+
if (validId(name) && !fs8.existsSync(path9.join(dirs.dir, sub, name)))
|
|
12405
|
+
tasks += 1;
|
|
12406
|
+
}
|
|
12407
|
+
merge(from, path9.join(dirs.dir, sub));
|
|
12408
|
+
}
|
|
12409
|
+
const ledger = path9.join(dirs.v2, "ledger");
|
|
12410
|
+
if (fs8.existsSync(ledger) && !fs8.existsSync(path9.join(dirs.dir, "ledger")))
|
|
12411
|
+
merge(ledger, path9.join(dirs.dir, "ledger"));
|
|
12412
|
+
fs8.rmSync(path9.join(dirs.v2, "task-index.json"), { force: true });
|
|
12413
|
+
fs8.renameSync(marker, path9.join(dirs.v2, "store.moved.json"));
|
|
12414
|
+
} catch (error) {
|
|
12415
|
+
return failure2("storage_error", `cannot move ${dirs.v2} into ${dirs.dir}: ${String(error)}`, {
|
|
12416
|
+
path: dirs.v2
|
|
12417
|
+
});
|
|
11237
12418
|
}
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
return failure2("storage_error", `snapshot replacement failed: ${String(error)}`, {
|
|
11246
|
-
path: file
|
|
12419
|
+
reportMigration({
|
|
12420
|
+
from: dirs.v2,
|
|
12421
|
+
to: dirs.dir,
|
|
12422
|
+
backup: dirs.v2,
|
|
12423
|
+
tasks,
|
|
12424
|
+
skipped: 0,
|
|
12425
|
+
workspace: false
|
|
11247
12426
|
});
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
try {
|
|
11251
|
-
fs6.unlinkSync(temporary);
|
|
11252
|
-
} catch {}
|
|
11253
|
-
}
|
|
12427
|
+
return success(null, null, null);
|
|
12428
|
+
});
|
|
11254
12429
|
}
|
|
11255
|
-
|
|
12430
|
+
readV2Workspace(file) {
|
|
12431
|
+
let bytes;
|
|
11256
12432
|
try {
|
|
11257
|
-
|
|
11258
|
-
if (!isObject2(value) || value.version !== INDEX_VERSION || value.workspaceId !== workspaceId || !isObject2(value.tasks))
|
|
11259
|
-
return {};
|
|
11260
|
-
const tasks = {};
|
|
11261
|
-
for (const [id, entry] of Object.entries(value.tasks))
|
|
11262
|
-
if (isObject2(entry) && entry.id === id && typeof entry.file === "string" && typeof entry.status === "string" && typeof entry.updatedAt === "string" && Array.isArray(entry.sessions) && isObject2(entry.progress) && isObject2(entry.source))
|
|
11263
|
-
tasks[id] = entry;
|
|
11264
|
-
return tasks;
|
|
12433
|
+
bytes = fs8.readFileSync(file, "utf8");
|
|
11265
12434
|
} catch {
|
|
11266
|
-
return
|
|
12435
|
+
return null;
|
|
11267
12436
|
}
|
|
12437
|
+
try {
|
|
12438
|
+
const value = JSON.parse(bytes);
|
|
12439
|
+
if (isObject4(value) && isObject4(value.store) && value.store.format === "workit-store")
|
|
12440
|
+
return TOMBSTONE;
|
|
12441
|
+
} catch {}
|
|
12442
|
+
return { bytes };
|
|
12443
|
+
}
|
|
12444
|
+
writeTombstone(v2, workspaceId) {
|
|
12445
|
+
const dirs = this.paths();
|
|
12446
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
12447
|
+
const marker = {
|
|
12448
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12449
|
+
id: workspaceId ?? newId(),
|
|
12450
|
+
revision: newRevision(),
|
|
12451
|
+
root: path9.dirname(v2),
|
|
12452
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
12453
|
+
writer: null,
|
|
12454
|
+
store: {
|
|
12455
|
+
format: "workit-store",
|
|
12456
|
+
version: STORE_FORMAT,
|
|
12457
|
+
path: dirs.dir,
|
|
12458
|
+
note: `workit ${STORE_MIN_RUNTIME}+ keeps task state in ${dirs.dir}; upgrade Workit`
|
|
12459
|
+
},
|
|
12460
|
+
critical: ["store"]
|
|
12461
|
+
};
|
|
12462
|
+
fs8.mkdirSync(v2, { recursive: true });
|
|
12463
|
+
const ignore = path9.join(v2, ".gitignore");
|
|
12464
|
+
if (!fs8.existsSync(ignore))
|
|
12465
|
+
fs8.writeFileSync(ignore, `*
|
|
12466
|
+
`);
|
|
12467
|
+
replaceFile(path9.join(v2, "workspace.json"), `${JSON.stringify(marker, null, 2)}
|
|
12468
|
+
`, true);
|
|
12469
|
+
}
|
|
12470
|
+
stubFor(taskId) {
|
|
12471
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
12472
|
+
return `${JSON.stringify({
|
|
12473
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12474
|
+
id: taskId,
|
|
12475
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
12476
|
+
store: { format: "workit-store", version: STORE_FORMAT, path: this.paths().dir },
|
|
12477
|
+
critical: ["store"]
|
|
12478
|
+
})}
|
|
12479
|
+
`;
|
|
11268
12480
|
}
|
|
11269
|
-
|
|
11270
|
-
const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
12481
|
+
isStub(file) {
|
|
11271
12482
|
try {
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
workspaceId,
|
|
11275
|
-
tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
|
|
11276
|
-
}), { mode: 384, flag: "wx" });
|
|
11277
|
-
retryTransient(() => fs6.renameSync(temporary, this.indexPath));
|
|
12483
|
+
const value = JSON.parse(fs8.readFileSync(file, "utf8"));
|
|
12484
|
+
return isObject4(value) && isObject4(value.store) && value.store.format === "workit-store";
|
|
11278
12485
|
} catch {
|
|
11279
|
-
|
|
11280
|
-
fs6.unlinkSync(temporary);
|
|
11281
|
-
} catch {}
|
|
12486
|
+
return false;
|
|
11282
12487
|
}
|
|
11283
12488
|
}
|
|
11284
|
-
|
|
12489
|
+
writeTombstoneIfMissing() {
|
|
11285
12490
|
try {
|
|
11286
|
-
const
|
|
11287
|
-
if (
|
|
12491
|
+
const v2 = path9.join(this.root, ".workit");
|
|
12492
|
+
if (fs8.existsSync(path9.join(v2, "workspace.json")))
|
|
11288
12493
|
return;
|
|
11289
|
-
|
|
11290
|
-
tasks[task.id] = indexEntry(task, signature);
|
|
11291
|
-
this.writeIndex(task.workspaceId, Object.values(tasks));
|
|
11292
|
-
} catch {}
|
|
11293
|
-
}
|
|
11294
|
-
saveRecovery(file, bytes) {
|
|
11295
|
-
const target = path7.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
11296
|
-
const id = target === "task" ? path7.basename(file, ".json") : "workspace";
|
|
11297
|
-
const destination = path7.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
11298
|
-
if (fs6.existsSync(destination)) {
|
|
11299
|
-
if (digestBytes(retryTransient(() => fs6.readFileSync(destination))) !== digestBytes(bytes))
|
|
11300
|
-
throw new Error("recovery copy already exists with different bytes");
|
|
11301
|
-
const stamp = new Date;
|
|
11302
|
-
retryTransient(() => fs6.utimesSync(destination, stamp, stamp));
|
|
11303
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
11304
|
-
return;
|
|
11305
|
-
}
|
|
11306
|
-
let temporary;
|
|
11307
|
-
try {
|
|
11308
|
-
temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
|
|
11309
|
-
const fd = fs6.openSync(temporary, "wx", 384);
|
|
12494
|
+
let id = null;
|
|
11310
12495
|
try {
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
fs6.closeSync(fd);
|
|
11315
|
-
}
|
|
11316
|
-
retryTransient(() => fs6.renameSync(temporary, destination));
|
|
11317
|
-
temporary = undefined;
|
|
11318
|
-
this.fsyncDirectory(this.recoveryDir);
|
|
11319
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
11320
|
-
} finally {
|
|
11321
|
-
if (temporary)
|
|
11322
|
-
try {
|
|
11323
|
-
fs6.unlinkSync(temporary);
|
|
11324
|
-
} catch {}
|
|
11325
|
-
}
|
|
11326
|
-
}
|
|
11327
|
-
pruneRecovery(prefix, keep) {
|
|
11328
|
-
try {
|
|
11329
|
-
const copies = this.recoveryCopies(prefix);
|
|
11330
|
-
const surplus = this.surplusCopies(copies, path7.basename(keep));
|
|
11331
|
-
for (const copy of surplus)
|
|
11332
|
-
fs6.rmSync(copy.path, { force: true });
|
|
12496
|
+
id = JSON.parse(fs8.readFileSync(this.paths().workspace, "utf8")).id ?? null;
|
|
12497
|
+
} catch {}
|
|
12498
|
+
this.writeTombstone(v2, id);
|
|
11333
12499
|
} catch {}
|
|
11334
12500
|
}
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
const file = path7.join(this.recoveryDir, name);
|
|
12501
|
+
initializeStorage() {
|
|
12502
|
+
const dirs = this.paths();
|
|
12503
|
+
retryTransient(() => fs8.mkdirSync(dirs.checkout, { recursive: true }));
|
|
12504
|
+
retryTransient(() => fs8.mkdirSync(dirs.tasks, { recursive: true }));
|
|
12505
|
+
if (!fs8.existsSync(dirs.marker))
|
|
12506
|
+
replaceFile(dirs.marker, `${JSON.stringify({ format: "workit-store", version: STORE_FORMAT, createdWith: runtimeVersion() })}
|
|
12507
|
+
`, false);
|
|
12508
|
+
if (!dirs.shared) {
|
|
12509
|
+
const ignore = path9.join(dirs.dir, ".gitignore");
|
|
12510
|
+
let current = null;
|
|
11346
12511
|
try {
|
|
11347
|
-
|
|
11348
|
-
if (!stat.isFile())
|
|
11349
|
-
continue;
|
|
11350
|
-
copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
|
|
12512
|
+
current = fs8.readFileSync(ignore, "utf8");
|
|
11351
12513
|
} catch {}
|
|
12514
|
+
if (current !== `*
|
|
12515
|
+
`)
|
|
12516
|
+
retryTransient(() => fs8.writeFileSync(ignore, `*
|
|
12517
|
+
`));
|
|
11352
12518
|
}
|
|
11353
|
-
|
|
11354
|
-
}
|
|
11355
|
-
surplusCopies(copies, keep) {
|
|
11356
|
-
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);
|
|
11357
|
-
return ordered.slice(RECOVERY_COPIES_PER_RECORD);
|
|
12519
|
+
this.writeTombstoneIfMissing();
|
|
11358
12520
|
}
|
|
11359
|
-
|
|
11360
|
-
const
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
12521
|
+
lockOptions(lockPath) {
|
|
12522
|
+
const inProcess = heldInProcess.has(lockPath);
|
|
12523
|
+
return {
|
|
12524
|
+
lockPath,
|
|
12525
|
+
staleMs: Number.MAX_SAFE_INTEGER,
|
|
12526
|
+
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
12527
|
+
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
12528
|
+
staleRecovery: "remove-if-unchanged",
|
|
12529
|
+
shouldReclaim: ({ payload, nowMs }) => {
|
|
12530
|
+
let ageMs = null;
|
|
12531
|
+
try {
|
|
12532
|
+
ageMs = nowMs - fs8.lstatSync(lockPath).mtimeMs;
|
|
12533
|
+
} catch {}
|
|
12534
|
+
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
12535
|
+
},
|
|
12536
|
+
shouldRemoveStaleLock: () => true,
|
|
12537
|
+
parsePayload: parseMetadataLockOrNull,
|
|
12538
|
+
payload: () => ({
|
|
12539
|
+
pid: process.pid,
|
|
12540
|
+
processStart: processStartOf(process.pid),
|
|
12541
|
+
host: localLockHost(),
|
|
12542
|
+
nonce: randomUUID3()
|
|
12543
|
+
})
|
|
11366
12544
|
};
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
12545
|
+
}
|
|
12546
|
+
acquire(lockPath, options) {
|
|
12547
|
+
clearAbandonedReclaimGuard(lockPath);
|
|
12548
|
+
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
12549
|
+
while (true) {
|
|
11370
12550
|
try {
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
for (const copies of groups.values()) {
|
|
11375
|
-
const surplus = this.surplusCopies(copies);
|
|
11376
|
-
report.recovery.kept += copies.length - surplus.length;
|
|
11377
|
-
for (const copy of surplus) {
|
|
11378
|
-
report.recovery.removed += 1;
|
|
11379
|
-
try {
|
|
11380
|
-
report.recovery.removedBytes += fs6.statSync(copy.path).size;
|
|
11381
|
-
} catch {}
|
|
11382
|
-
if (!dryRun)
|
|
11383
|
-
fs6.rmSync(copy.path, { force: true });
|
|
11384
|
-
}
|
|
11385
|
-
}
|
|
11386
|
-
const nowMs = Date.now();
|
|
11387
|
-
for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
|
|
11388
|
-
if (!fs6.existsSync(directory))
|
|
11389
|
-
continue;
|
|
11390
|
-
for (const name of fs6.readdirSync(directory)) {
|
|
11391
|
-
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
11392
|
-
continue;
|
|
11393
|
-
const file = path7.join(directory, name);
|
|
11394
|
-
const stat = fs6.lstatSync(file);
|
|
11395
|
-
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
11396
|
-
continue;
|
|
11397
|
-
report.temporary.removed += 1;
|
|
11398
|
-
if (!dryRun)
|
|
11399
|
-
fs6.rmSync(file, { force: true });
|
|
11400
|
-
}
|
|
11401
|
-
}
|
|
11402
|
-
return success(null, null, null);
|
|
11403
|
-
} catch (error) {
|
|
11404
|
-
return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
|
|
11405
|
-
path: this.recoveryDir
|
|
12551
|
+
return acquireFileLockSync(lockPath, {
|
|
12552
|
+
...options,
|
|
12553
|
+
timeoutMs: Math.max(0, deadline - Date.now())
|
|
11406
12554
|
});
|
|
12555
|
+
} catch (error) {
|
|
12556
|
+
const code = error?.code;
|
|
12557
|
+
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
12558
|
+
throw error;
|
|
12559
|
+
if (code !== "file_lock_stale" && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
12560
|
+
throw error;
|
|
12561
|
+
if (code !== "file_lock_stale")
|
|
12562
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
11407
12563
|
}
|
|
11408
|
-
};
|
|
11409
|
-
const pruned = dryRun ? sweep() : this.withLock(sweep);
|
|
11410
|
-
if (!pruned.ok)
|
|
11411
|
-
return pruned;
|
|
11412
|
-
const tasks = this.listTasks();
|
|
11413
|
-
if (!tasks.ok)
|
|
11414
|
-
return tasks;
|
|
11415
|
-
for (const task of tasks.data) {
|
|
11416
|
-
const deduped = dedupeCandidates(task.candidates);
|
|
11417
|
-
const removed = task.candidates.length - deduped.length;
|
|
11418
|
-
if (removed === 0)
|
|
11419
|
-
continue;
|
|
11420
|
-
if (task.status === "active") {
|
|
11421
|
-
report.candidates.skippedActive.push(task.id);
|
|
11422
|
-
continue;
|
|
11423
|
-
}
|
|
11424
|
-
if (task.status === "closed") {
|
|
11425
|
-
report.candidates.skippedClosed.push(task.id);
|
|
11426
|
-
continue;
|
|
11427
|
-
}
|
|
11428
|
-
if (!dryRun) {
|
|
11429
|
-
const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
|
|
11430
|
-
...current,
|
|
11431
|
-
candidates: dedupeCandidates(current.candidates)
|
|
11432
|
-
}));
|
|
11433
|
-
if (!written.ok) {
|
|
11434
|
-
report.candidates.failed.push(task.id);
|
|
11435
|
-
continue;
|
|
11436
|
-
}
|
|
11437
|
-
}
|
|
11438
|
-
report.candidates.removed += removed;
|
|
11439
|
-
report.candidates.tasks.push(task.id);
|
|
11440
12564
|
}
|
|
11441
|
-
return success(null, null, report);
|
|
11442
12565
|
}
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
return
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
return null;
|
|
11452
|
-
for (const candidate of candidates.data) {
|
|
11453
|
-
if (candidate.target !== target || candidate.digest !== digest)
|
|
11454
|
-
continue;
|
|
11455
|
-
try {
|
|
11456
|
-
if (!fs6.lstatSync(candidate.path).isFile())
|
|
11457
|
-
continue;
|
|
11458
|
-
const bytes = fs6.readFileSync(candidate.path);
|
|
11459
|
-
if (digestBytes(bytes) !== digest)
|
|
11460
|
-
continue;
|
|
11461
|
-
if (target === "workspace") {
|
|
11462
|
-
const parsed = this.parseBytes(bytes, workspaceRecordSchema);
|
|
11463
|
-
if (parsed.ok && parsed.data.id === workspaceId && parsed.data.root === this.root)
|
|
11464
|
-
return bytes;
|
|
11465
|
-
} else {
|
|
11466
|
-
const parsed = this.parseBytes(bytes, taskRecordSchema);
|
|
11467
|
-
if (parsed.ok && parsed.data.id === taskId && parsed.data.workspaceId === workspace?.id)
|
|
11468
|
-
return bytes;
|
|
11469
|
-
}
|
|
11470
|
-
} catch {}
|
|
12566
|
+
withLock(lockPath, operation) {
|
|
12567
|
+
try {
|
|
12568
|
+
this.initializeStorage();
|
|
12569
|
+
fs8.mkdirSync(path9.dirname(lockPath), { recursive: true });
|
|
12570
|
+
} catch (error) {
|
|
12571
|
+
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
12572
|
+
path: this.paths().dir
|
|
12573
|
+
});
|
|
11471
12574
|
}
|
|
11472
|
-
|
|
11473
|
-
}
|
|
11474
|
-
fsyncDirectory(directory) {
|
|
12575
|
+
let handle;
|
|
11475
12576
|
try {
|
|
11476
|
-
|
|
11477
|
-
try {
|
|
11478
|
-
fs6.fsyncSync(fd);
|
|
11479
|
-
} finally {
|
|
11480
|
-
fs6.closeSync(fd);
|
|
11481
|
-
}
|
|
12577
|
+
handle = this.acquire(lockPath, this.lockOptions(lockPath));
|
|
11482
12578
|
} catch (error) {
|
|
11483
|
-
|
|
11484
|
-
throw error;
|
|
12579
|
+
return this.lockFailure(lockPath, error);
|
|
11485
12580
|
}
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
if (!fs6.existsSync(this.tasksDir))
|
|
11489
|
-
return null;
|
|
11490
|
-
const ids = new Set;
|
|
12581
|
+
hold(lockPath);
|
|
12582
|
+
let result;
|
|
11491
12583
|
try {
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
12584
|
+
if (!handle.verifyStillHeld())
|
|
12585
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
12586
|
+
else {
|
|
12587
|
+
try {
|
|
12588
|
+
result = operation();
|
|
12589
|
+
} catch (error) {
|
|
12590
|
+
result = failure2("storage_error", `mutation failed: ${String(error)}`);
|
|
12591
|
+
}
|
|
11499
12592
|
}
|
|
11500
|
-
} catch {
|
|
11501
|
-
|
|
12593
|
+
} catch (error) {
|
|
12594
|
+
result = this.lockFailure(lockPath, error);
|
|
12595
|
+
}
|
|
12596
|
+
drop(lockPath);
|
|
12597
|
+
try {
|
|
12598
|
+
retryTransient(() => handle.release());
|
|
12599
|
+
} catch (error) {
|
|
12600
|
+
const released = this.lockFailure(lockPath, error);
|
|
12601
|
+
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
12602
|
+
result = result.ok ? released : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
11502
12603
|
}
|
|
11503
|
-
return
|
|
12604
|
+
return result;
|
|
11504
12605
|
}
|
|
11505
|
-
|
|
12606
|
+
lockDirWritable(lockPath) {
|
|
12607
|
+
const probe = `${lockPath}.${process.pid}.${randomUUID3()}.probe`;
|
|
11506
12608
|
try {
|
|
11507
|
-
|
|
12609
|
+
fs8.closeSync(fs8.openSync(probe, "wx", 384));
|
|
11508
12610
|
} catch {
|
|
11509
|
-
return
|
|
12611
|
+
return false;
|
|
11510
12612
|
}
|
|
12613
|
+
try {
|
|
12614
|
+
fs8.rmSync(probe, { force: true });
|
|
12615
|
+
} catch {}
|
|
12616
|
+
return true;
|
|
11511
12617
|
}
|
|
11512
|
-
|
|
12618
|
+
lockHolderPresent(lockPath) {
|
|
11513
12619
|
try {
|
|
11514
|
-
|
|
11515
|
-
return
|
|
12620
|
+
fs8.lstatSync(lockPath);
|
|
12621
|
+
return true;
|
|
11516
12622
|
} catch (error) {
|
|
11517
|
-
|
|
11518
|
-
return { exists: false, result: success(null, null, null) };
|
|
11519
|
-
return {
|
|
11520
|
-
exists: true,
|
|
11521
|
-
result: failure2("recovery_required", `snapshot cannot be read: ${String(error)}`, {
|
|
11522
|
-
path: file
|
|
11523
|
-
})
|
|
11524
|
-
};
|
|
12623
|
+
return error?.code !== "ENOENT";
|
|
11525
12624
|
}
|
|
11526
12625
|
}
|
|
11527
|
-
|
|
11528
|
-
|
|
11529
|
-
|
|
11530
|
-
|
|
11531
|
-
|
|
11532
|
-
|
|
12626
|
+
lockFailure(lockPath, error) {
|
|
12627
|
+
const value = error;
|
|
12628
|
+
const code = typeof value?.code === "string" ? value.code : "";
|
|
12629
|
+
if (isTransientWindowsError(error) && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
12630
|
+
return failure2("storage_error", `cannot create the workit lock (${code}); no other Workit call holds it`, {
|
|
12631
|
+
path: lockPath,
|
|
12632
|
+
guidance: `Check permissions and the read-only attribute on ${path9.dirname(lockPath)}.`
|
|
12633
|
+
});
|
|
12634
|
+
if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
|
|
12635
|
+
let lock = null;
|
|
12636
|
+
try {
|
|
12637
|
+
lock = parseMetadataLockOrNull(fs8.readFileSync(lockPath, "utf8"));
|
|
12638
|
+
} catch {}
|
|
12639
|
+
if (lock?.externalAction)
|
|
12640
|
+
return failure2("writer_conflict", "workspace is reserved by a managed external action", {
|
|
12641
|
+
outcome: "not_started",
|
|
12642
|
+
path: lockPath
|
|
12643
|
+
});
|
|
12644
|
+
return failure2("busy", `workit lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
|
|
12645
|
+
outcome: "not_started",
|
|
12646
|
+
path: lockPath,
|
|
12647
|
+
guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
|
|
12648
|
+
});
|
|
11533
12649
|
}
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
const parsed = parseStoredRecord(schema, value);
|
|
11537
|
-
if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
|
|
11538
|
-
return success(null, null, parsed.data);
|
|
11539
|
-
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
11540
|
-
const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
|
|
11541
|
-
if (parsed.success)
|
|
11542
|
-
return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
|
|
11543
|
-
if (parsed.critical.length > 0)
|
|
11544
|
-
return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
|
|
11545
|
-
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
11546
|
-
return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
11547
|
-
return failure2("recovery_required", "snapshot does not satisfy its schema");
|
|
11548
|
-
}
|
|
11549
|
-
conflict(expected, actual) {
|
|
11550
|
-
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
11551
|
-
expectedRevision: expected,
|
|
11552
|
-
actualRevision: actual
|
|
11553
|
-
});
|
|
11554
|
-
}
|
|
11555
|
-
workspaceConflict(expected, actual) {
|
|
11556
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
11557
|
-
}
|
|
11558
|
-
taskPath(taskId) {
|
|
11559
|
-
return path7.join(this.tasksDir, `${taskId}.json`);
|
|
11560
|
-
}
|
|
11561
|
-
get workitDir() {
|
|
11562
|
-
return path7.join(this.root, ".workit");
|
|
11563
|
-
}
|
|
11564
|
-
get tasksDir() {
|
|
11565
|
-
return path7.join(this.workitDir, "tasks");
|
|
11566
|
-
}
|
|
11567
|
-
get recoveryDir() {
|
|
11568
|
-
return path7.join(this.workitDir, "recovery");
|
|
11569
|
-
}
|
|
11570
|
-
get workspacePath() {
|
|
11571
|
-
return path7.join(this.workitDir, "workspace.json");
|
|
11572
|
-
}
|
|
11573
|
-
get indexPath() {
|
|
11574
|
-
return path7.join(this.workitDir, "index.json");
|
|
11575
|
-
}
|
|
11576
|
-
get lockPath() {
|
|
11577
|
-
return path7.join(this.workitDir, "metadata.lock");
|
|
11578
|
-
}
|
|
11579
|
-
get gitignorePath() {
|
|
11580
|
-
return path7.join(this.workitDir, ".gitignore");
|
|
12650
|
+
const recovery = code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
|
|
12651
|
+
return failure2(recovery ? "recovery_required" : "storage_error", `lock operation failed: ${String(error)}`, { path: lockPath });
|
|
11581
12652
|
}
|
|
11582
12653
|
}
|
|
11583
12654
|
|
|
@@ -11654,7 +12725,7 @@ var operationAutoClass = (operation) => typeof operation === "string" ? OPERATIO
|
|
|
11654
12725
|
var digestOf = (configDirPath) => {
|
|
11655
12726
|
try {
|
|
11656
12727
|
const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
|
|
11657
|
-
return
|
|
12728
|
+
return createHash7("sha256").update(raw).digest("hex");
|
|
11658
12729
|
} catch {
|
|
11659
12730
|
return null;
|
|
11660
12731
|
}
|
|
@@ -11698,7 +12769,7 @@ var targetOf = (root, descriptor) => {
|
|
|
11698
12769
|
const cwd = parsed?.payload?.cwd;
|
|
11699
12770
|
if (cwd === undefined)
|
|
11700
12771
|
return root;
|
|
11701
|
-
if (typeof cwd !== "string" || !
|
|
12772
|
+
if (typeof cwd !== "string" || !path10.isAbsolute(cwd))
|
|
11702
12773
|
return null;
|
|
11703
12774
|
return realpathSync3(cwd);
|
|
11704
12775
|
} catch {
|
|
@@ -11923,11 +12994,11 @@ function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
|
11923
12994
|
return [];
|
|
11924
12995
|
return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
|
|
11925
12996
|
}
|
|
11926
|
-
var
|
|
12997
|
+
var digestBytes = (bytes) => createHash8("sha256").update(bytes).digest("hex");
|
|
11927
12998
|
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
11928
12999
|
let root;
|
|
11929
13000
|
try {
|
|
11930
|
-
root =
|
|
13001
|
+
root = fs9.realpathSync(checkoutRoot);
|
|
11931
13002
|
} catch {
|
|
11932
13003
|
return failure2("permission_denied", "checkout root is unavailable");
|
|
11933
13004
|
}
|
|
@@ -11936,23 +13007,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
11936
13007
|
continue;
|
|
11937
13008
|
if (!reference.digest)
|
|
11938
13009
|
return failure2("invalid_input", "document references require a byte digest");
|
|
11939
|
-
const target =
|
|
11940
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
13010
|
+
const target = path11.resolve(root, reference.path);
|
|
13011
|
+
if (target !== root && !target.startsWith(`${root}${path11.sep}`))
|
|
11941
13012
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
11942
13013
|
fields: [
|
|
11943
13014
|
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
11944
13015
|
]
|
|
11945
13016
|
});
|
|
11946
13017
|
try {
|
|
11947
|
-
const relative =
|
|
13018
|
+
const relative = path11.relative(root, target);
|
|
11948
13019
|
let current = root;
|
|
11949
|
-
for (const segment of relative.split(
|
|
13020
|
+
for (const segment of relative.split(path11.sep)) {
|
|
11950
13021
|
if (!segment)
|
|
11951
13022
|
continue;
|
|
11952
|
-
current =
|
|
11953
|
-
const stat =
|
|
11954
|
-
const real =
|
|
11955
|
-
if (real !== root && !real.startsWith(`${root}${
|
|
13023
|
+
current = path11.join(current, segment);
|
|
13024
|
+
const stat = fs9.lstatSync(current);
|
|
13025
|
+
const real = fs9.realpathSync(current);
|
|
13026
|
+
if (real !== root && !real.startsWith(`${root}${path11.sep}`))
|
|
11956
13027
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
11957
13028
|
fields: [
|
|
11958
13029
|
{
|
|
@@ -11968,7 +13039,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
11968
13039
|
if (current !== target && !stat.isDirectory())
|
|
11969
13040
|
return failure2("invalid_input", "document reference ancestor is not a directory");
|
|
11970
13041
|
}
|
|
11971
|
-
if (
|
|
13042
|
+
if (digestBytes(fs9.readFileSync(target)) !== reference.digest)
|
|
11972
13043
|
return failure2("permission_denied", "approved document bytes have changed");
|
|
11973
13044
|
} catch {
|
|
11974
13045
|
return failure2("invalid_input", "approved document is unavailable");
|
|
@@ -12332,9 +13403,9 @@ var bindingCovers = scopeCovers;
|
|
|
12332
13403
|
var verifyDecisionContent = verifyContentRefs;
|
|
12333
13404
|
|
|
12334
13405
|
// packages/workit-core/src/check-config.ts
|
|
12335
|
-
import { spawnSync as
|
|
12336
|
-
import
|
|
12337
|
-
import
|
|
13406
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
13407
|
+
import fs10 from "node:fs";
|
|
13408
|
+
import path12 from "node:path";
|
|
12338
13409
|
var SCRIPT_CHECKS = ["test", "lint", "typecheck", "check"];
|
|
12339
13410
|
var CHECKS_FILE = "workit.checks.json";
|
|
12340
13411
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -12376,7 +13447,7 @@ function splitCommand(command) {
|
|
|
12376
13447
|
return argv.length ? argv : new Error("empty command");
|
|
12377
13448
|
}
|
|
12378
13449
|
var git = (cwd, args) => {
|
|
12379
|
-
const run =
|
|
13450
|
+
const run = spawnSync4("git", args, {
|
|
12380
13451
|
cwd,
|
|
12381
13452
|
encoding: "utf8",
|
|
12382
13453
|
timeout: 20000,
|
|
@@ -12387,10 +13458,10 @@ var git = (cwd, args) => {
|
|
|
12387
13458
|
return value || null;
|
|
12388
13459
|
};
|
|
12389
13460
|
function checkRoot(cwd) {
|
|
12390
|
-
return
|
|
13461
|
+
return path12.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
|
|
12391
13462
|
}
|
|
12392
13463
|
var packageManager = (root) => {
|
|
12393
|
-
const has = (file) =>
|
|
13464
|
+
const has = (file) => fs10.existsSync(path12.join(root, file));
|
|
12394
13465
|
if (has("bun.lock") || has("bun.lockb"))
|
|
12395
13466
|
return "bun";
|
|
12396
13467
|
if (has("pnpm-lock.yaml"))
|
|
@@ -12410,7 +13481,7 @@ var CHECK_NAME = /^[a-z0-9][a-z0-9:._-]{0,63}$/u;
|
|
|
12410
13481
|
function detectedChecks(root) {
|
|
12411
13482
|
const read = (file) => {
|
|
12412
13483
|
try {
|
|
12413
|
-
return
|
|
13484
|
+
return fs10.readFileSync(path12.join(root, file), "utf8");
|
|
12414
13485
|
} catch {
|
|
12415
13486
|
return null;
|
|
12416
13487
|
}
|
|
@@ -12449,7 +13520,7 @@ function detectedChecks(root) {
|
|
|
12449
13520
|
var GATES = ["testing", "verification"];
|
|
12450
13521
|
function loadCheckConfig(cwd) {
|
|
12451
13522
|
const root = checkRoot(cwd);
|
|
12452
|
-
const file =
|
|
13523
|
+
const file = path12.join(root, CHECKS_FILE);
|
|
12453
13524
|
const none = (error = null) => ({
|
|
12454
13525
|
source: error ? CHECKS_FILE : "none",
|
|
12455
13526
|
root,
|
|
@@ -12459,7 +13530,7 @@ function loadCheckConfig(cwd) {
|
|
|
12459
13530
|
});
|
|
12460
13531
|
let text = null;
|
|
12461
13532
|
try {
|
|
12462
|
-
text =
|
|
13533
|
+
text = fs10.readFileSync(file, "utf8");
|
|
12463
13534
|
} catch (error) {
|
|
12464
13535
|
if (error.code !== "ENOENT")
|
|
12465
13536
|
return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
|
|
@@ -12522,10 +13593,10 @@ function workitAvailable(env = process.env) {
|
|
|
12522
13593
|
return workitOnPath;
|
|
12523
13594
|
const windows = process.platform === "win32";
|
|
12524
13595
|
const exts = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
12525
|
-
const dirs = (env.PATH ?? env.Path ?? "").split(
|
|
13596
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path12.delimiter).filter(Boolean);
|
|
12526
13597
|
workitOnPath = dirs.some((dir) => exts.some((ext) => {
|
|
12527
13598
|
try {
|
|
12528
|
-
return
|
|
13599
|
+
return fs10.statSync(path12.join(dir, `workit${ext}`)).isFile();
|
|
12529
13600
|
} catch {
|
|
12530
13601
|
return false;
|
|
12531
13602
|
}
|
|
@@ -12537,15 +13608,15 @@ function checkCommand(args, version, onPath = workitAvailable()) {
|
|
|
12537
13608
|
}
|
|
12538
13609
|
|
|
12539
13610
|
// packages/workit-core/src/git/rev.ts
|
|
12540
|
-
import { spawnSync as
|
|
12541
|
-
import { createHash as
|
|
12542
|
-
import
|
|
13611
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
13612
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
13613
|
+
import fs11 from "node:fs";
|
|
12543
13614
|
import os2 from "node:os";
|
|
12544
|
-
import
|
|
13615
|
+
import path13 from "node:path";
|
|
12545
13616
|
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
12546
13617
|
var GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
|
|
12547
13618
|
var git2 = (cwd, args, options = {}) => {
|
|
12548
|
-
const result =
|
|
13619
|
+
const result = spawnSync5("git", args, {
|
|
12549
13620
|
cwd,
|
|
12550
13621
|
encoding: "utf8",
|
|
12551
13622
|
env: options.env ?? process.env,
|
|
@@ -12575,10 +13646,10 @@ function headSha(cwd) {
|
|
|
12575
13646
|
var emptyTree = (cwd) => line(git2(cwd, ["mktree"], { input: "" }));
|
|
12576
13647
|
var MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
|
|
12577
13648
|
var seedIndex = (from, to) => {
|
|
12578
|
-
|
|
12579
|
-
const { atimeNs, mtimeNs } =
|
|
13649
|
+
fs11.copyFileSync(from, to);
|
|
13650
|
+
const { atimeNs, mtimeNs } = fs11.statSync(from, { bigint: true });
|
|
12580
13651
|
const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
|
|
12581
|
-
|
|
13652
|
+
fs11.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
|
|
12582
13653
|
};
|
|
12583
13654
|
function worktreeTree(cwd, options = {}) {
|
|
12584
13655
|
const top = line(git2(cwd, ["rev-parse", "--show-toplevel"]));
|
|
@@ -12587,10 +13658,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12587
13658
|
return null;
|
|
12588
13659
|
const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
|
|
12589
13660
|
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
|
|
12590
|
-
const scratch =
|
|
13661
|
+
const scratch = fs11.mkdtempSync(path13.join(os2.tmpdir(), "workit-tree-"));
|
|
12591
13662
|
try {
|
|
12592
|
-
const tempIndex =
|
|
12593
|
-
if (
|
|
13663
|
+
const tempIndex = path13.join(scratch, "index");
|
|
13664
|
+
if (fs11.existsSync(indexPath))
|
|
12594
13665
|
seedIndex(indexPath, tempIndex);
|
|
12595
13666
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
12596
13667
|
const untracked = git2(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
@@ -12602,7 +13673,7 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12602
13673
|
const skipped = [];
|
|
12603
13674
|
for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
|
|
12604
13675
|
try {
|
|
12605
|
-
const stat =
|
|
13676
|
+
const stat = fs11.statSync(path13.join(top, file));
|
|
12606
13677
|
if (stat.isFile() && stat.size > cap)
|
|
12607
13678
|
skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
|
|
12608
13679
|
} catch {}
|
|
@@ -12619,10 +13690,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12619
13690
|
if (!tree)
|
|
12620
13691
|
return null;
|
|
12621
13692
|
const base = line(git2(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
|
|
12622
|
-
const key = skipped.length ? `sha256:${
|
|
13693
|
+
const key = skipped.length ? `sha256:${createHash9("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
|
|
12623
13694
|
return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
|
|
12624
13695
|
} finally {
|
|
12625
|
-
|
|
13696
|
+
fs11.rmSync(scratch, { recursive: true, force: true });
|
|
12626
13697
|
}
|
|
12627
13698
|
}
|
|
12628
13699
|
function worktreeSignal(cwd, options = {}) {
|
|
@@ -12642,16 +13713,16 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
12642
13713
|
});
|
|
12643
13714
|
if (!status.ok)
|
|
12644
13715
|
return null;
|
|
12645
|
-
const hash =
|
|
13716
|
+
const hash = createHash9("sha256");
|
|
12646
13717
|
hash.update(`${headSha(top) ?? "unborn"}\x00`);
|
|
12647
13718
|
for (const entry of status.stdout.split("\x00")) {
|
|
12648
13719
|
if (entry.length < 4)
|
|
12649
13720
|
continue;
|
|
12650
13721
|
const file = entry.slice(3);
|
|
12651
|
-
const absolute =
|
|
13722
|
+
const absolute = path13.join(top, file);
|
|
12652
13723
|
let stat = "-";
|
|
12653
13724
|
try {
|
|
12654
|
-
const info =
|
|
13725
|
+
const info = fs11.lstatSync(absolute, { bigint: true });
|
|
12655
13726
|
stat = `${info.mode.toString(8)}:${info.size}:${info.mtimeNs}`;
|
|
12656
13727
|
if (info.isDirectory())
|
|
12657
13728
|
stat += `:${headSha(absolute) ?? "-"}`;
|
|
@@ -12662,15 +13733,15 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
12662
13733
|
}
|
|
12663
13734
|
|
|
12664
13735
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
12665
|
-
var
|
|
12666
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${
|
|
12667
|
-
var relative = (root, candidate) =>
|
|
13736
|
+
var digestBytes2 = (value) => createHash10("sha256").update(value).digest("hex");
|
|
13737
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path14.sep}`);
|
|
13738
|
+
var relative = (root, candidate) => path14.relative(root, candidate).split(path14.sep).join("/") || ".";
|
|
12668
13739
|
var canonicalPath = (value) => {
|
|
12669
13740
|
if (typeof value !== "string" || !value)
|
|
12670
13741
|
return null;
|
|
12671
13742
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
12672
13743
|
return null;
|
|
12673
|
-
const normalized =
|
|
13744
|
+
const normalized = path14.posix.normalize(value);
|
|
12674
13745
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
12675
13746
|
return null;
|
|
12676
13747
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -12706,7 +13777,7 @@ var compareCodeUnits2 = (left, right) => {
|
|
|
12706
13777
|
return left.length - right.length;
|
|
12707
13778
|
};
|
|
12708
13779
|
var gitPaths = (root) => {
|
|
12709
|
-
const result =
|
|
13780
|
+
const result = spawnSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
12710
13781
|
cwd: root,
|
|
12711
13782
|
encoding: "buffer"
|
|
12712
13783
|
});
|
|
@@ -12718,21 +13789,21 @@ var gitPaths = (root) => {
|
|
|
12718
13789
|
git: !/not a git repository/i.test(stderr)
|
|
12719
13790
|
};
|
|
12720
13791
|
}
|
|
12721
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
12722
|
-
const stagedDeleted =
|
|
13792
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")) : [];
|
|
13793
|
+
const stagedDeleted = spawnSync6("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
12723
13794
|
if (stagedDeleted.status !== 0) {
|
|
12724
13795
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
12725
13796
|
if (!/not a git repository/i.test(stderr))
|
|
12726
13797
|
return { paths, uncertain: true, git: true };
|
|
12727
13798
|
} else if (stagedDeleted.stdout) {
|
|
12728
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
13799
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")));
|
|
12729
13800
|
}
|
|
12730
13801
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
12731
13802
|
};
|
|
12732
13803
|
var walk = (root, directory, output) => {
|
|
12733
13804
|
let names;
|
|
12734
13805
|
try {
|
|
12735
|
-
names =
|
|
13806
|
+
names = fs12.readdirSync(directory);
|
|
12736
13807
|
} catch {
|
|
12737
13808
|
return true;
|
|
12738
13809
|
}
|
|
@@ -12740,10 +13811,10 @@ var walk = (root, directory, output) => {
|
|
|
12740
13811
|
for (const name of names) {
|
|
12741
13812
|
if (name === ".git" || name === ".workit")
|
|
12742
13813
|
continue;
|
|
12743
|
-
const target =
|
|
13814
|
+
const target = path14.join(directory, name);
|
|
12744
13815
|
let stat;
|
|
12745
13816
|
try {
|
|
12746
|
-
stat =
|
|
13817
|
+
stat = fs12.lstatSync(target);
|
|
12747
13818
|
} catch {
|
|
12748
13819
|
uncertain = true;
|
|
12749
13820
|
continue;
|
|
@@ -12758,14 +13829,14 @@ var walk = (root, directory, output) => {
|
|
|
12758
13829
|
var scopeRoots = (root, scope) => {
|
|
12759
13830
|
const roots = [];
|
|
12760
13831
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
12761
|
-
const target =
|
|
13832
|
+
const target = path14.resolve(root, item);
|
|
12762
13833
|
if (!inside(root, target))
|
|
12763
13834
|
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` }] });
|
|
12764
13835
|
let ancestor = target;
|
|
12765
|
-
while (!
|
|
12766
|
-
ancestor =
|
|
13836
|
+
while (!fs12.existsSync(ancestor) && ancestor !== root)
|
|
13837
|
+
ancestor = path14.dirname(ancestor);
|
|
12767
13838
|
try {
|
|
12768
|
-
if (!inside(root,
|
|
13839
|
+
if (!inside(root, fs12.realpathSync(ancestor)))
|
|
12769
13840
|
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` }] });
|
|
12770
13841
|
} catch {
|
|
12771
13842
|
return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
@@ -12777,7 +13848,7 @@ var scopeRoots = (root, scope) => {
|
|
|
12777
13848
|
function captureCandidate(root, scope, environment = []) {
|
|
12778
13849
|
let checkout;
|
|
12779
13850
|
try {
|
|
12780
|
-
checkout =
|
|
13851
|
+
checkout = fs12.realpathSync(root);
|
|
12781
13852
|
} catch {
|
|
12782
13853
|
return failure2("invalid_input", "candidate root does not exist");
|
|
12783
13854
|
}
|
|
@@ -12795,11 +13866,11 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12795
13866
|
if (item === ".git" || item === ".workit")
|
|
12796
13867
|
continue;
|
|
12797
13868
|
names.add(item);
|
|
12798
|
-
if (!
|
|
13869
|
+
if (!fs12.existsSync(target))
|
|
12799
13870
|
continue;
|
|
12800
13871
|
let stat;
|
|
12801
13872
|
try {
|
|
12802
|
-
stat =
|
|
13873
|
+
stat = fs12.lstatSync(target);
|
|
12803
13874
|
} catch {
|
|
12804
13875
|
uncertain = true;
|
|
12805
13876
|
continue;
|
|
@@ -12815,17 +13886,17 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12815
13886
|
for (const item of [...names].toSorted()) {
|
|
12816
13887
|
if (!scopeMatches(item, normalizedScope))
|
|
12817
13888
|
continue;
|
|
12818
|
-
const target =
|
|
13889
|
+
const target = path14.join(checkout, item);
|
|
12819
13890
|
try {
|
|
12820
|
-
const stat =
|
|
13891
|
+
const stat = fs12.lstatSync(target);
|
|
12821
13892
|
if (stat.isSymbolicLink()) {
|
|
12822
|
-
const link =
|
|
12823
|
-
files.push({ path: item, kind: "symlink", digest:
|
|
13893
|
+
const link = fs12.readlinkSync(target);
|
|
13894
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes2(link), executable: null });
|
|
12824
13895
|
} else if (stat.isFile()) {
|
|
12825
13896
|
files.push({
|
|
12826
13897
|
path: item,
|
|
12827
13898
|
kind: "file",
|
|
12828
|
-
digest:
|
|
13899
|
+
digest: digestBytes2(fs12.readFileSync(target)),
|
|
12829
13900
|
executable: (stat.mode & 73) !== 0
|
|
12830
13901
|
});
|
|
12831
13902
|
} else if (!stat.isDirectory()) {
|
|
@@ -12850,7 +13921,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12850
13921
|
const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
12851
13922
|
if (values.some(({ value }) => value === null))
|
|
12852
13923
|
completeness = "uncertain";
|
|
12853
|
-
const headResult =
|
|
13924
|
+
const headResult = spawnSync6("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
12854
13925
|
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
12855
13926
|
completeness = "uncertain";
|
|
12856
13927
|
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
@@ -13863,6 +14934,38 @@ var applyWorkerLifecycle = (input) => {
|
|
|
13863
14934
|
return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
|
|
13864
14935
|
};
|
|
13865
14936
|
var refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }));
|
|
14937
|
+
var TASK_SCOPED_ACTIONS = new Set([
|
|
14938
|
+
"task.inspect",
|
|
14939
|
+
"task.revise",
|
|
14940
|
+
"task.progress",
|
|
14941
|
+
"task.pause",
|
|
14942
|
+
"task.resume",
|
|
14943
|
+
"task.close",
|
|
14944
|
+
"policy.assess",
|
|
14945
|
+
"policy.preview",
|
|
14946
|
+
"policy.explain",
|
|
14947
|
+
"evidence.record",
|
|
14948
|
+
"finding.record",
|
|
14949
|
+
"finding.resolve",
|
|
14950
|
+
"decision.record",
|
|
14951
|
+
"decision.revoke",
|
|
14952
|
+
"worker.assign",
|
|
14953
|
+
"worker.report",
|
|
14954
|
+
"worker.cancel",
|
|
14955
|
+
"writer.acquire",
|
|
14956
|
+
"writer.release",
|
|
14957
|
+
"state.export"
|
|
14958
|
+
]);
|
|
14959
|
+
var CREATING_ACTIONS = new Set([
|
|
14960
|
+
"task.revise",
|
|
14961
|
+
"task.progress",
|
|
14962
|
+
"policy.assess",
|
|
14963
|
+
"evidence.record",
|
|
14964
|
+
"finding.record",
|
|
14965
|
+
"decision.record",
|
|
14966
|
+
"worker.assign",
|
|
14967
|
+
"writer.acquire"
|
|
14968
|
+
]);
|
|
13866
14969
|
var REVISION_RETRY_ATTEMPTS = 8;
|
|
13867
14970
|
var retryPause = new Int32Array(new SharedArrayBuffer(4));
|
|
13868
14971
|
var filledRevisions = (request) => {
|
|
@@ -14114,7 +15217,9 @@ class WorkitCore {
|
|
|
14114
15217
|
}
|
|
14115
15218
|
contextRootError() {
|
|
14116
15219
|
try {
|
|
14117
|
-
|
|
15220
|
+
realpathSync6(this.context.root);
|
|
15221
|
+
const root = checkoutRootOf(this.context.root);
|
|
15222
|
+
if (root !== this.store.root && !sameDirectoryIdentity(root, this.store.root))
|
|
14118
15223
|
return failure2("invalid_input", "operation context root does not match the task store root");
|
|
14119
15224
|
} catch {
|
|
14120
15225
|
return failure2("invalid_input", "operation context root cannot be resolved");
|
|
@@ -14136,6 +15241,30 @@ class WorkitCore {
|
|
|
14136
15241
|
retryOmittedRevisions(request, run) {
|
|
14137
15242
|
return retryFilledRevisions(filledRevisions(request), run);
|
|
14138
15243
|
}
|
|
15244
|
+
implicitTask(family, request) {
|
|
15245
|
+
if (typeof request !== "object" || request === null || Array.isArray(request))
|
|
15246
|
+
return success(null, null, request);
|
|
15247
|
+
const value = request;
|
|
15248
|
+
if (value.taskId !== undefined || typeof value.action !== "string")
|
|
15249
|
+
return success(null, null, request);
|
|
15250
|
+
const action = `${family}.${value.action}`;
|
|
15251
|
+
if (!TASK_SCOPED_ACTIONS.has(action))
|
|
15252
|
+
return success(null, null, request);
|
|
15253
|
+
const root = this.contextRootError();
|
|
15254
|
+
if (!root.ok)
|
|
15255
|
+
return root;
|
|
15256
|
+
const create = CREATING_ACTIONS.has(action) && (this.context.workerId ?? null) === null;
|
|
15257
|
+
const found = this.store.implicitTask({
|
|
15258
|
+
provenance: provenance(this.context),
|
|
15259
|
+
create,
|
|
15260
|
+
now: trustedNow(this.context)
|
|
15261
|
+
});
|
|
15262
|
+
if (!found.ok)
|
|
15263
|
+
return found;
|
|
15264
|
+
if (!found.data)
|
|
15265
|
+
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');
|
|
15266
|
+
return success(null, null, { ...request, taskId: found.data.task.id });
|
|
15267
|
+
}
|
|
14139
15268
|
helperEntry(task, requireSession = false, requireActive = false) {
|
|
14140
15269
|
const workerId = this.context.workerId ?? null;
|
|
14141
15270
|
if (workerId === null)
|
|
@@ -14167,7 +15296,11 @@ class WorkitCore {
|
|
|
14167
15296
|
return null;
|
|
14168
15297
|
}
|
|
14169
15298
|
state(request) {
|
|
14170
|
-
|
|
15299
|
+
const resolved = this.implicitTask("state", request);
|
|
15300
|
+
if (!resolved.ok)
|
|
15301
|
+
return resolved;
|
|
15302
|
+
const value = resolved.data;
|
|
15303
|
+
return value?.action === "import" ? this.retryOmittedRevisions(value, () => this.stateOnce(value)) : this.stateOnce(value);
|
|
14171
15304
|
}
|
|
14172
15305
|
stateOnce(request) {
|
|
14173
15306
|
const root = this.contextRootError();
|
|
@@ -14234,44 +15367,14 @@ class WorkitCore {
|
|
|
14234
15367
|
return imported;
|
|
14235
15368
|
return this.summary(imported.data);
|
|
14236
15369
|
}
|
|
14237
|
-
|
|
14238
|
-
return failure2("permission_denied", "native recovery authority is unavailable");
|
|
14239
|
-
const processEvidence = (lock, writer) => this.context.nativeRecovery({
|
|
14240
|
-
lock,
|
|
14241
|
-
writer,
|
|
14242
|
-
reason: input.reason,
|
|
14243
|
-
authorityRefs: input.authorityRefs
|
|
14244
|
-
});
|
|
14245
|
-
if (input.target === "workspace")
|
|
14246
|
-
return this.store.recoverWorkspace({
|
|
14247
|
-
expectedBytes: input.expectedBytes,
|
|
14248
|
-
snapshotDigest: input.snapshotDigest,
|
|
14249
|
-
reason: input.reason,
|
|
14250
|
-
authorityRefs: input.authorityRefs,
|
|
14251
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
14252
|
-
processEvidence
|
|
14253
|
-
});
|
|
14254
|
-
const workspace = this.store.readWorkspace();
|
|
14255
|
-
if (!workspace.ok)
|
|
14256
|
-
return workspace;
|
|
14257
|
-
if (!workspace.data)
|
|
14258
|
-
return failure2("not_found", "workspace not found");
|
|
14259
|
-
this.fillRevisions(input, undefined, workspace.data);
|
|
14260
|
-
if (input.target === "workspace" && input.taskId !== undefined)
|
|
14261
|
-
return failure2("invalid_input", "workspace recovery takes no task ID");
|
|
14262
|
-
if (input.target === "task" && input.taskId === undefined)
|
|
14263
|
-
return failure2("invalid_input", "task recovery requires a task ID");
|
|
14264
|
-
return this.store.recoverTask(input.taskId, {
|
|
14265
|
-
expectedBytes: input.expectedBytes,
|
|
14266
|
-
snapshotDigest: input.snapshotDigest,
|
|
14267
|
-
reason: input.reason,
|
|
14268
|
-
authorityRefs: input.authorityRefs,
|
|
14269
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
14270
|
-
processEvidence
|
|
14271
|
-
});
|
|
15370
|
+
return failure2("invalid_input", "unknown state action");
|
|
14272
15371
|
}
|
|
14273
15372
|
task(request) {
|
|
14274
|
-
|
|
15373
|
+
const resolved = this.implicitTask("task", request);
|
|
15374
|
+
if (!resolved.ok)
|
|
15375
|
+
return resolved;
|
|
15376
|
+
const value = resolved.data;
|
|
15377
|
+
return this.retryOmittedRevisions(value, () => this.taskOnce(value));
|
|
14275
15378
|
}
|
|
14276
15379
|
taskOnce(request) {
|
|
14277
15380
|
const root = this.contextRootError();
|
|
@@ -14373,7 +15476,11 @@ class WorkitCore {
|
|
|
14373
15476
|
}
|
|
14374
15477
|
}
|
|
14375
15478
|
policy(request) {
|
|
14376
|
-
|
|
15479
|
+
const resolved = this.implicitTask("policy", request);
|
|
15480
|
+
if (!resolved.ok)
|
|
15481
|
+
return resolved;
|
|
15482
|
+
const value = resolved.data;
|
|
15483
|
+
return this.retryOmittedRevisions(value, () => this.policyOnce(value));
|
|
14377
15484
|
}
|
|
14378
15485
|
policyOnce(request) {
|
|
14379
15486
|
const root = this.contextRootError();
|
|
@@ -14422,7 +15529,11 @@ class WorkitCore {
|
|
|
14422
15529
|
return success(changed.data.revision, null, changed.data.policy);
|
|
14423
15530
|
}
|
|
14424
15531
|
evidence(request) {
|
|
14425
|
-
|
|
15532
|
+
const resolved = this.implicitTask("evidence", request);
|
|
15533
|
+
if (!resolved.ok)
|
|
15534
|
+
return resolved;
|
|
15535
|
+
const value = resolved.data;
|
|
15536
|
+
return this.retryOmittedRevisions(value, () => this.evidenceOnce(value));
|
|
14426
15537
|
}
|
|
14427
15538
|
evidenceOnce(request) {
|
|
14428
15539
|
const root = this.contextRootError();
|
|
@@ -14563,13 +15674,16 @@ class WorkitCore {
|
|
|
14563
15674
|
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
14564
15675
|
}
|
|
14565
15676
|
decision(request) {
|
|
14566
|
-
|
|
15677
|
+
const resolved = this.implicitTask("decision", request);
|
|
15678
|
+
return resolved.ok ? this.recordDecision(resolved.data) : resolved;
|
|
14567
15679
|
}
|
|
14568
15680
|
observeDecision(request, observation) {
|
|
14569
|
-
|
|
15681
|
+
const resolved = this.implicitTask("decision", request);
|
|
15682
|
+
return resolved.ok ? this.recordDecision(resolved.data, observation, true) : resolved;
|
|
14570
15683
|
}
|
|
14571
15684
|
observeStandingDecision(request) {
|
|
14572
|
-
|
|
15685
|
+
const resolved = this.implicitTask("decision", request);
|
|
15686
|
+
return resolved.ok ? this.recordDecision(resolved.data, undefined, true, true) : resolved;
|
|
14573
15687
|
}
|
|
14574
15688
|
commitTask(taskId, expected, filled, recheck, update) {
|
|
14575
15689
|
let revision = expected;
|
|
@@ -14731,7 +15845,11 @@ class WorkitCore {
|
|
|
14731
15845
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
|
|
14732
15846
|
}
|
|
14733
15847
|
finding(request) {
|
|
14734
|
-
|
|
15848
|
+
const resolved = this.implicitTask("finding", request);
|
|
15849
|
+
if (!resolved.ok)
|
|
15850
|
+
return resolved;
|
|
15851
|
+
const value = resolved.data;
|
|
15852
|
+
return this.retryOmittedRevisions(value, () => this.findingOnce(value));
|
|
14735
15853
|
}
|
|
14736
15854
|
findingOnce(request) {
|
|
14737
15855
|
const root = this.contextRootError();
|
|
@@ -14857,7 +15975,11 @@ class WorkitCore {
|
|
|
14857
15975
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
|
|
14858
15976
|
}
|
|
14859
15977
|
worker(request) {
|
|
14860
|
-
|
|
15978
|
+
const resolved = this.implicitTask("worker", request);
|
|
15979
|
+
if (!resolved.ok)
|
|
15980
|
+
return resolved;
|
|
15981
|
+
const value = resolved.data;
|
|
15982
|
+
return this.retryOmittedRevisions(value, () => this.workerOnce(value));
|
|
14861
15983
|
}
|
|
14862
15984
|
workerOnce(request) {
|
|
14863
15985
|
const root = this.contextRootError();
|
|
@@ -15207,7 +16329,11 @@ class WorkitCore {
|
|
|
15207
16329
|
return applied;
|
|
15208
16330
|
}
|
|
15209
16331
|
writer(request) {
|
|
15210
|
-
|
|
16332
|
+
const resolved = this.implicitTask("writer", request);
|
|
16333
|
+
if (!resolved.ok)
|
|
16334
|
+
return resolved;
|
|
16335
|
+
const value = resolved.data;
|
|
16336
|
+
return this.retryOmittedRevisions(value, () => this.writerOnce(value));
|
|
15211
16337
|
}
|
|
15212
16338
|
writerOnce(request) {
|
|
15213
16339
|
const root = this.contextRootError();
|
|
@@ -15817,6 +16943,8 @@ class WorkitCore {
|
|
|
15817
16943
|
}
|
|
15818
16944
|
|
|
15819
16945
|
// packages/workit-core/src/hooks/context.ts
|
|
16946
|
+
var MIGRATION_PENDING_NOTE = "workit migration pending — run `workit task status`";
|
|
16947
|
+
var migrationPending = (result) => !result.ok && result.code === "needs_input" && (result.error ?? "").startsWith(MIGRATION_PENDING);
|
|
15820
16948
|
var boundTo = (entry, session) => entry.sessions.some((item) => item.host === session.host && item.handle === session.handle);
|
|
15821
16949
|
var newestFirst = (left, right) => right.updatedAt.localeCompare(left.updatedAt);
|
|
15822
16950
|
var sessionTaskEntry = (entries, session) => entries.filter((entry) => entry.status !== "closed" && boundTo(entry, session)).toSorted(newestFirst)[0] ?? null;
|
|
@@ -15827,6 +16955,15 @@ var currentTaskEntry = (entries, session, selection) => {
|
|
|
15827
16955
|
const active = entries.filter((entry) => entry.status === "active");
|
|
15828
16956
|
return active.length === 1 ? active[0] : null;
|
|
15829
16957
|
};
|
|
16958
|
+
var implicitTaskEntry = (store, entries, session) => {
|
|
16959
|
+
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)));
|
|
16960
|
+
if (open.length === 0)
|
|
16961
|
+
return null;
|
|
16962
|
+
const key = store.currentKey();
|
|
16963
|
+
if (!key.ok)
|
|
16964
|
+
return null;
|
|
16965
|
+
return open.filter((entry) => entry.key === key.data.key).toSorted(newestFirst)[0] ?? null;
|
|
16966
|
+
};
|
|
15830
16967
|
var unfinishedTaskOffer = (entries, session, excludeTaskId = null) => {
|
|
15831
16968
|
const tasks = unboundOpenTaskEntries(entries.filter((entry) => entry.id !== excludeTaskId), session);
|
|
15832
16969
|
if (tasks.length === 0)
|
|
@@ -15858,8 +16995,8 @@ var cache = new Map;
|
|
|
15858
16995
|
function sessionCompactContext(store, session, context, selection = "session-bound") {
|
|
15859
16996
|
const listed = store.listTaskIndex();
|
|
15860
16997
|
if (!listed.ok)
|
|
15861
|
-
return null;
|
|
15862
|
-
const entry = currentTaskEntry(listed.data, session, selection);
|
|
16998
|
+
return migrationPending(listed) ? MIGRATION_PENDING_NOTE : null;
|
|
16999
|
+
const entry = currentTaskEntry(listed.data, session, selection) ?? implicitTaskEntry(store, listed.data, session);
|
|
15863
17000
|
return entry ? entryCompactContext(store, entry, session, context) : null;
|
|
15864
17001
|
}
|
|
15865
17002
|
function entryCompactContext(store, entry, session, context) {
|
|
@@ -15884,7 +17021,7 @@ function entryCompactContext(store, entry, session, context) {
|
|
|
15884
17021
|
return null;
|
|
15885
17022
|
const freshness = { mode: "signal", current: () => recentSignal(store.root) };
|
|
15886
17023
|
const observed = task.data.evidence.some(isObservedCheck);
|
|
15887
|
-
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [
|
|
17024
|
+
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path15.resolve(store.root, ref.path)] : []));
|
|
15888
17025
|
const observedFiles = filesKey(files);
|
|
15889
17026
|
const compact = new WorkitCore(store, context).compactContext(entry.id, freshness);
|
|
15890
17027
|
if (!compact.ok)
|
|
@@ -15917,9 +17054,15 @@ var sessionContextText = (input, descriptor, options) => {
|
|
|
15917
17054
|
try {
|
|
15918
17055
|
const store = new TaskStore(input.cwd);
|
|
15919
17056
|
const listed = store.listTaskIndex();
|
|
17057
|
+
if (migrationPending(listed))
|
|
17058
|
+
return `<workit-contract>
|
|
17059
|
+
${invariantBootstrap()}
|
|
17060
|
+
${MIGRATION_PENDING_NOTE}${options.addendum ? `
|
|
17061
|
+
${options.addendum}` : ""}
|
|
17062
|
+
</workit-contract>`;
|
|
15920
17063
|
if (!listed.ok)
|
|
15921
17064
|
throw new Error(listed.error);
|
|
15922
|
-
const entry = currentTaskEntry(listed.data, session, descriptor.context.task);
|
|
17065
|
+
const entry = currentTaskEntry(listed.data, session, descriptor.context.task) ?? implicitTaskEntry(store, listed.data, session);
|
|
15923
17066
|
const text = entry ? entryCompactContext(store, entry, session, hookOperationContext(input, descriptor)) : null;
|
|
15924
17067
|
if (text)
|
|
15925
17068
|
compact = `
|
|
@@ -15939,6 +17082,8 @@ ${options.addendum}` : ""}
|
|
|
15939
17082
|
var turnContextText = (input, descriptor) => {
|
|
15940
17083
|
try {
|
|
15941
17084
|
const text = sessionCompactContext(new TaskStore(input.cwd), { host: input.host, handle: input.session.id }, hookOperationContext(input, descriptor), descriptor.context.task);
|
|
17085
|
+
if (text === MIGRATION_PENDING_NOTE)
|
|
17086
|
+
return text;
|
|
15942
17087
|
return text ? `<workit-task-context>${text}</workit-task-context>` : null;
|
|
15943
17088
|
} catch {
|
|
15944
17089
|
return null;
|
|
@@ -16041,12 +17186,12 @@ var dispatchHook = (adapter, raw, env = process.env) => {
|
|
|
16041
17186
|
};
|
|
16042
17187
|
// packages/workit-core/src/hooks/hosts/fields.ts
|
|
16043
17188
|
import { existsSync as existsSync5, realpathSync as realpathSync7, statSync as statSync2 } from "node:fs";
|
|
16044
|
-
import
|
|
17189
|
+
import path16 from "node:path";
|
|
16045
17190
|
var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16046
17191
|
var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
|
|
16047
17192
|
var optionalText = (value) => typeof value === "string" ? value : null;
|
|
16048
17193
|
var existingDirectory = (value) => {
|
|
16049
|
-
if (!nonEmpty2(value) || !
|
|
17194
|
+
if (!nonEmpty2(value) || !path16.isAbsolute(value) || !existsSync5(value))
|
|
16050
17195
|
return null;
|
|
16051
17196
|
try {
|
|
16052
17197
|
return statSync2(value).isDirectory() ? realpathSync7(value) : null;
|
|
@@ -16554,7 +17699,7 @@ var codexAdapter = {
|
|
|
16554
17699
|
};
|
|
16555
17700
|
|
|
16556
17701
|
// packages/workit-core/src/hooks/hosts/cursor.ts
|
|
16557
|
-
import
|
|
17702
|
+
import path17 from "node:path";
|
|
16558
17703
|
var undocumented2 = { support: "undocumented", native: null };
|
|
16559
17704
|
var none = { support: "none", native: null };
|
|
16560
17705
|
var CURSOR_DESCRIPTOR = {
|
|
@@ -16737,7 +17882,7 @@ var protocolEvent2 = (input) => {
|
|
|
16737
17882
|
return { kind: "compact.pre", trigger: "auto" };
|
|
16738
17883
|
}
|
|
16739
17884
|
};
|
|
16740
|
-
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(
|
|
17885
|
+
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path17.sep) ? root : `${root}${path17.sep}`);
|
|
16741
17886
|
var hookCwd = (input) => {
|
|
16742
17887
|
const cwd = existingDirectory(input.cwd);
|
|
16743
17888
|
if (cwd && input.hook_event_name === "beforeShellExecution")
|