@brainervirus/workit-mcp 3.0.0 → 4.0.0

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