@brainervirus/workit-cursor 3.0.0 → 5.0.0

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