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