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