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