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