@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
@@ -7873,7 +7873,7 @@ var SCHEMA_VERSION = 1, POLICY_VERSION = "1.0.0", OPERATION_FAMILIES, uuidPatter
7873
7873
  if (critical.length > 0)
7874
7874
  return { ...first, critical };
7875
7875
  return { success: true, data: parsed.data, stripped: [...new Set(stripped)] };
7876
- }, workspaceRecordSchema, capabilitySchema, requirementEvaluationSchema, taskViewSchema, taskSummarySchema, taskListItemSchema, exportBundleSchema, callerSchema, base, revisions, taskId, operation = (shape) => object({ ...base, ...shape }).strict(), taskOperations, policyOperations, evidenceOperations, findingOperations, decisionOperations, workerOperations, writerOperations, stateOperations, operationSchemas, _unadvertisedRecover, advertisedStateOperations, advertisedOperationSchemas, compiledOperationSchemas, success = (revisionValue, workspaceRevisionValue, data) => ({
7876
+ }, workspaceRecordSchema, capabilitySchema, requirementEvaluationSchema, taskViewSchema, taskSummarySchema, taskListItemSchema, exportBundleSchema, callerSchema, base, revisions, taskId, operation = (shape) => object({ ...base, ...shape }).strict(), taskOperations, policyOperations, evidenceOperations, findingOperations, decisionOperations, workerOperations, writerOperations, stateOperations, operationSchemas, withImplicitTask = (options) => Object.values(options).map((option) => ("taskId" in option.shape) ? option.extend({ taskId: id.optional() }) : option), advertised = (options) => discriminatedUnion("action", withImplicitTask(options)), advertisedOperationSchemas, compiledOperationSchemas, success = (revisionValue, workspaceRevisionValue, data) => ({
7877
7877
  ok: true,
7878
7878
  schemaVersion: 1,
7879
7879
  revision: revisionValue,
@@ -8548,16 +8548,6 @@ var init_task_contract = __esm(() => {
8548
8548
  expectedWorkspaceRevision: revision.nullable().optional(),
8549
8549
  bundle: exportBundleSchema,
8550
8550
  authorityRefs: array(refSchema)
8551
- }),
8552
- recover: operation({
8553
- action: literal("recover"),
8554
- taskId: id.optional(),
8555
- expectedWorkspaceRevision: revision.optional(),
8556
- target: _enum(["task", "workspace"]),
8557
- expectedBytes: digest,
8558
- snapshotDigest: digest,
8559
- reason: text,
8560
- authorityRefs: array(refSchema)
8561
8551
  })
8562
8552
  };
8563
8553
  operationSchemas = {
@@ -8570,10 +8560,15 @@ var init_task_contract = __esm(() => {
8570
8560
  writer: discriminatedUnion("action", Object.values(writerOperations)),
8571
8561
  state: discriminatedUnion("action", Object.values(stateOperations))
8572
8562
  };
8573
- ({ recover: _unadvertisedRecover, ...advertisedStateOperations } = stateOperations);
8574
8563
  advertisedOperationSchemas = {
8575
- ...operationSchemas,
8576
- state: discriminatedUnion("action", Object.values(advertisedStateOperations))
8564
+ task: advertised(taskOperations),
8565
+ policy: advertised(policyOperations),
8566
+ evidence: advertised(evidenceOperations),
8567
+ finding: advertised(findingOperations),
8568
+ decision: advertised(decisionOperations),
8569
+ worker: advertised(workerOperations),
8570
+ writer: advertised(writerOperations),
8571
+ state: advertised(stateOperations)
8577
8572
  };
8578
8573
  compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
8579
8574
  });
@@ -9375,21 +9370,6 @@ var init_branch = __esm(() => {
9375
9370
  init_vcs_config();
9376
9371
  });
9377
9372
 
9378
- // packages/workit-core/src/core/package-root.ts
9379
- import { existsSync as existsSync2 } from "node:fs";
9380
- import path4 from "node:path";
9381
- import { fileURLToPath } from "node:url";
9382
- var packageRoot = () => {
9383
- let dir = path4.dirname(fileURLToPath(import.meta.url));
9384
- while (true) {
9385
- const parent = path4.dirname(dir);
9386
- if (existsSync2(path4.join(dir, "package.json")) || parent === dir)
9387
- return dir;
9388
- dir = parent;
9389
- }
9390
- };
9391
- var init_package_root = () => {};
9392
-
9393
9373
  // node_modules/@openclaw/fs-safe/dist/errors.js
9394
9374
  function categorizeFsSafeError(code) {
9395
9375
  return OPERATIONAL_CODES.has(code) ? "operational" : "policy";
@@ -9500,7 +9480,7 @@ var init_read_open_flags = () => {};
9500
9480
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
9501
9481
  import { randomBytes } from "node:crypto";
9502
9482
  import fsSync from "node:fs";
9503
- import path5 from "node:path";
9483
+ import path4 from "node:path";
9504
9484
  function parseSidecarLockSnapshot(snapshot, parser) {
9505
9485
  return snapshot && { ...snapshot, payload: parseSidecarLockPayload(snapshot.raw, parser) };
9506
9486
  }
@@ -9528,13 +9508,13 @@ ${ownershipToken}
9528
9508
  return { raw, ownershipToken };
9529
9509
  }
9530
9510
  function relativeSidecarLockPath(lockRoot, lockPath) {
9531
- const resolved = path5.resolve(lockPath);
9532
- const lexicalRelative = path5.relative(lockRoot.rootDir, resolved);
9533
- const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path5.sep}`) && !path5.isAbsolute(lexicalRelative) ? lexicalRelative : path5.relative(lockRoot.rootReal, resolved);
9534
- if (!relative || relative === ".." || relative.startsWith(`..${path5.sep}`) || path5.isAbsolute(relative)) {
9511
+ const resolved = path4.resolve(lockPath);
9512
+ const lexicalRelative = path4.relative(lockRoot.rootDir, resolved);
9513
+ const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(lexicalRelative) ? lexicalRelative : path4.relative(lockRoot.rootReal, resolved);
9514
+ if (!relative || relative === ".." || relative.startsWith(`..${path4.sep}`) || path4.isAbsolute(relative)) {
9535
9515
  throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
9536
9516
  }
9537
- return relative.split(path5.sep).join(path5.posix.sep);
9517
+ return relative.split(path4.sep).join(path4.posix.sep);
9538
9518
  }
9539
9519
  function parseSidecarLockPayload(raw, parser) {
9540
9520
  if (parser) {
@@ -9704,7 +9684,7 @@ function sleepSync(ms) {
9704
9684
 
9705
9685
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
9706
9686
  import fs4 from "node:fs";
9707
- import path6 from "node:path";
9687
+ import path5 from "node:path";
9708
9688
  function getSyncHeldLocks() {
9709
9689
  const globalWithState = globalThis;
9710
9690
  if (!globalWithState[SYNC_HELD_LOCKS_KEY]) {
@@ -9782,26 +9762,26 @@ function canonicalLockParentSync(parent) {
9782
9762
  return process.platform === "win32" ? fs4.realpathSync.native(parent) : fs4.realpathSync(parent);
9783
9763
  }
9784
9764
  function normalizeTargetPath(targetPath) {
9785
- const resolved = path6.resolve(targetPath);
9786
- fs4.mkdirSync(path6.dirname(resolved), { recursive: true });
9765
+ const resolved = path5.resolve(targetPath);
9766
+ fs4.mkdirSync(path5.dirname(resolved), { recursive: true });
9787
9767
  try {
9788
- return path6.join(canonicalLockParentSync(path6.dirname(resolved)), path6.basename(resolved));
9768
+ return path5.join(canonicalLockParentSync(path5.dirname(resolved)), path5.basename(resolved));
9789
9769
  } catch {
9790
9770
  return resolved;
9791
9771
  }
9792
9772
  }
9793
9773
  function boundedLockPath(lockPath, lockRoot) {
9794
- const resolved = path6.resolve(lockPath);
9774
+ const resolved = path5.resolve(lockPath);
9795
9775
  if (!lockRoot)
9796
9776
  return resolved;
9797
9777
  relativeSidecarLockPath(lockRoot, resolved);
9798
- const parent = path6.dirname(resolved);
9778
+ const parent = path5.dirname(resolved);
9799
9779
  const parentReal = canonicalLockParentSync(parent);
9800
- const parentRelative = path6.relative(lockRoot.rootReal, parentReal);
9801
- if (parentRelative === ".." || parentRelative.startsWith(`..${path6.sep}`) || path6.isAbsolute(parentRelative)) {
9780
+ const parentRelative = path5.relative(lockRoot.rootReal, parentReal);
9781
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path5.sep}`) || path5.isAbsolute(parentRelative)) {
9802
9782
  throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
9803
9783
  }
9804
- return path6.join(parentReal, path6.basename(resolved));
9784
+ return path5.join(parentReal, path5.basename(resolved));
9805
9785
  }
9806
9786
  function defaultShouldReclaim(snapshot, staleMs, nowMs) {
9807
9787
  const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
@@ -10016,9 +9996,300 @@ var init_file_lock = __esm(() => {
10016
9996
  init_file_lock_sync();
10017
9997
  });
10018
9998
 
10019
- // packages/workit-core/src/core/store-lock.ts
9999
+ // packages/workit-core/src/core/package-root.ts
10000
+ import { existsSync as existsSync2 } from "node:fs";
10001
+ import path6 from "node:path";
10002
+ import { fileURLToPath } from "node:url";
10003
+ var packageRoot = () => {
10004
+ let dir = path6.dirname(fileURLToPath(import.meta.url));
10005
+ while (true) {
10006
+ const parent = path6.dirname(dir);
10007
+ if (existsSync2(path6.join(dir, "package.json")) || parent === dir)
10008
+ return dir;
10009
+ dir = parent;
10010
+ }
10011
+ };
10012
+ var init_package_root = () => {};
10013
+
10014
+ // packages/workit-core/src/store/paths.ts
10020
10015
  import { spawnSync as spawnSync2 } from "node:child_process";
10016
+ import { createHash as createHash4 } from "node:crypto";
10021
10017
  import fs5 from "node:fs";
10018
+ import path7 from "node:path";
10019
+ function fastGit(root) {
10020
+ if (DISCOVERY_ENV.some((name) => process.env[name]))
10021
+ return null;
10022
+ let dir = real(root);
10023
+ let device = null;
10024
+ for (;; ) {
10025
+ let stat;
10026
+ try {
10027
+ stat = fs5.statSync(dir);
10028
+ } catch {
10029
+ return null;
10030
+ }
10031
+ if (device !== null && stat.dev !== device)
10032
+ return { kind: "none" };
10033
+ device = stat.dev;
10034
+ const dotGit = path7.join(dir, ".git");
10035
+ let entry = null;
10036
+ try {
10037
+ entry = fs5.statSync(dotGit);
10038
+ } catch {}
10039
+ let gitDir = null;
10040
+ let top = null;
10041
+ if (entry) {
10042
+ top = dir;
10043
+ if (entry.isDirectory())
10044
+ gitDir = dotGit;
10045
+ else if (entry.isFile()) {
10046
+ let text = "";
10047
+ try {
10048
+ text = fs5.readFileSync(dotGit, "utf8");
10049
+ } catch {
10050
+ return null;
10051
+ }
10052
+ const match = /^gitdir: (.+)$/mu.exec(text);
10053
+ if (!match)
10054
+ return null;
10055
+ gitDir = path7.resolve(dir, match[1].trim());
10056
+ } else
10057
+ return null;
10058
+ if (!fs5.existsSync(path7.join(gitDir, "HEAD")))
10059
+ return null;
10060
+ } else if (isGitDir(dir))
10061
+ gitDir = dir;
10062
+ if (gitDir) {
10063
+ let common = gitDir;
10064
+ try {
10065
+ common = path7.resolve(gitDir, fs5.readFileSync(path7.join(gitDir, "commondir"), "utf8").trim());
10066
+ } catch {}
10067
+ if (!fs5.existsSync(path7.join(common, "objects")))
10068
+ return null;
10069
+ return {
10070
+ kind: "repo",
10071
+ common: real(common),
10072
+ gitDir: real(gitDir),
10073
+ top: top === null ? null : real(top),
10074
+ reftable: reftableRefs(common)
10075
+ };
10076
+ }
10077
+ const parent = path7.dirname(dir);
10078
+ if (parent === dir)
10079
+ return { kind: "none" };
10080
+ dir = parent;
10081
+ }
10082
+ }
10083
+ function resolveStore(root) {
10084
+ root = path7.resolve(root);
10085
+ let directory = false;
10086
+ try {
10087
+ directory = fs5.statSync(root).isDirectory();
10088
+ } catch {}
10089
+ if (!directory)
10090
+ return { dir: path7.join(root, ".workit"), shared: false, top: null };
10091
+ const fast = fastGit(root);
10092
+ if (fast?.kind === "none")
10093
+ return { dir: path7.join(root, ".workit"), shared: false, top: null };
10094
+ if (fast?.kind === "repo")
10095
+ return {
10096
+ dir: path7.join(fast.common, "workit"),
10097
+ shared: true,
10098
+ top: fast.top,
10099
+ git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
10100
+ };
10101
+ const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
10102
+ const common = run.stdout.trim();
10103
+ if (run.ok && common) {
10104
+ const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
10105
+ return {
10106
+ dir: path7.join(real(path7.resolve(root, common)), "workit"),
10107
+ shared: true,
10108
+ top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
10109
+ };
10110
+ }
10111
+ if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
10112
+ return { dir: path7.join(root, ".workit"), shared: false, top: null };
10113
+ return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
10114
+ `)[0] || "no output"}); fix the repository (git status should work here)`);
10115
+ }
10116
+ function resolveTaskKey(root, location) {
10117
+ if (!location.shared)
10118
+ return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
10119
+ const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
10120
+ if (location.git && !location.git.reftable) {
10121
+ let head = "";
10122
+ try {
10123
+ head = fs5.readFileSync(path7.join(location.git.gitDir, "HEAD"), "utf8").trim();
10124
+ } catch {}
10125
+ const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
10126
+ if (ref && ref[1] !== ".invalid")
10127
+ return branchKey(ref[1]);
10128
+ if (/^[0-9a-f]{40,64}$/u.test(head)) {
10129
+ const rebasing = rebasingBranch(location.git.gitDir);
10130
+ if (rebasing)
10131
+ return branchKey(rebasing);
10132
+ return {
10133
+ key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
10134
+ kind: "detached",
10135
+ branch: null
10136
+ };
10137
+ }
10138
+ }
10139
+ const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
10140
+ const name = branch.ok ? branch.stdout.trim() : "";
10141
+ if (name && name !== ".invalid")
10142
+ return branchKey(name);
10143
+ const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
10144
+ if (gitDir.ok && gitDir.stdout.trim()) {
10145
+ const rebasing = rebasingBranch(gitDir.stdout.trim());
10146
+ if (rebasing)
10147
+ return branchKey(rebasing);
10148
+ }
10149
+ const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
10150
+ const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
10151
+ return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
10152
+ }
10153
+ function checkoutRootOf(root, location) {
10154
+ const resolved = path7.resolve(root);
10155
+ let canonical = resolved;
10156
+ try {
10157
+ canonical = fs5.realpathSync(resolved);
10158
+ } catch {
10159
+ return resolved;
10160
+ }
10161
+ const found = location ?? resolveStore(canonical);
10162
+ if (found instanceof Error || !found.shared || !found.top)
10163
+ return canonical;
10164
+ if (real(canonical) === found.top)
10165
+ return canonical;
10166
+ try {
10167
+ return fs5.realpathSync(found.top);
10168
+ } catch {
10169
+ return canonical;
10170
+ }
10171
+ }
10172
+ function branchRenames(location) {
10173
+ if (!location.shared)
10174
+ return [];
10175
+ const files = [];
10176
+ const walk = (dir) => {
10177
+ let entries;
10178
+ try {
10179
+ entries = fs5.readdirSync(dir, { withFileTypes: true });
10180
+ } catch {
10181
+ return;
10182
+ }
10183
+ for (const entry of entries)
10184
+ if (entry.isDirectory())
10185
+ walk(path7.join(dir, entry.name));
10186
+ else
10187
+ files.push(path7.join(dir, entry.name));
10188
+ };
10189
+ walk(path7.join(path7.dirname(location.dir), "logs", "refs", "heads"));
10190
+ if (location.git)
10191
+ files.push(path7.join(location.git.gitDir, "logs", "HEAD"));
10192
+ const seen = new Set;
10193
+ const renames = [];
10194
+ for (const file of files) {
10195
+ let text = "";
10196
+ try {
10197
+ text = fs5.readFileSync(file, "utf8");
10198
+ } catch {
10199
+ continue;
10200
+ }
10201
+ for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
10202
+ const rename = { from: match[2], to: match[3], at: Number(match[1]) };
10203
+ const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
10204
+ if (seen.has(id))
10205
+ continue;
10206
+ seen.add(id);
10207
+ renames.push(rename);
10208
+ }
10209
+ }
10210
+ return renames.toSorted((left, right) => left.at - right.at);
10211
+ }
10212
+ function followRenames(key, boundAt, renames) {
10213
+ let current = key;
10214
+ let since = Math.floor(Date.parse(boundAt) / 1000);
10215
+ for (const rename of renames)
10216
+ if (rename.from === current && rename.at >= since) {
10217
+ current = rename.to;
10218
+ since = rename.at;
10219
+ }
10220
+ return current;
10221
+ }
10222
+ var NOT_A_REPO, gitRun = (cwd, args) => {
10223
+ const run = spawnSync2("git", args, {
10224
+ cwd,
10225
+ encoding: "utf8",
10226
+ timeout: 20000,
10227
+ killSignal: "SIGKILL",
10228
+ windowsHide: true,
10229
+ env: {
10230
+ ...process.env,
10231
+ GIT_TERMINAL_PROMPT: "0",
10232
+ GIT_OPTIONAL_LOCKS: "0",
10233
+ LC_ALL: "C",
10234
+ LANGUAGE: "C"
10235
+ }
10236
+ });
10237
+ const error = run.error;
10238
+ return {
10239
+ ok: run.status === 0,
10240
+ stdout: run.stdout ?? "",
10241
+ stderr: error ? String(error.message) : run.stderr ?? "",
10242
+ missing: error?.code === "ENOENT"
10243
+ };
10244
+ }, shortHash = (value) => createHash4("sha256").update(value).digest("hex").slice(0, 12), real = (file) => {
10245
+ try {
10246
+ return fs5.realpathSync.native(file);
10247
+ } catch {
10248
+ return file;
10249
+ }
10250
+ }, DISCOVERY_ENV, isGitDir = (dir) => {
10251
+ try {
10252
+ return fs5.statSync(path7.join(dir, "HEAD")).isFile() && fs5.statSync(path7.join(dir, "objects")).isDirectory() && fs5.statSync(path7.join(dir, "refs")).isDirectory();
10253
+ } catch {
10254
+ return false;
10255
+ }
10256
+ }, reftableRefs = (common) => {
10257
+ try {
10258
+ return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs5.readFileSync(path7.join(common, "config"), "utf8"));
10259
+ } catch {
10260
+ return false;
10261
+ }
10262
+ }, rebasingBranch = (gitDir) => {
10263
+ for (const state of ["rebase-merge", "rebase-apply"]) {
10264
+ try {
10265
+ const ref = fs5.readFileSync(path7.join(gitDir, state, "head-name"), "utf8").trim();
10266
+ const match = /^refs\/heads\/(.+)$/u.exec(ref);
10267
+ if (match)
10268
+ return match[1];
10269
+ } catch {}
10270
+ }
10271
+ return null;
10272
+ }, checkoutSlug = (root) => {
10273
+ let canonical = real(root);
10274
+ if (process.platform === "win32")
10275
+ canonical = canonical.toLowerCase();
10276
+ const base = path7.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
10277
+ return `${base}-${shortHash(canonical)}`;
10278
+ };
10279
+ var init_paths = __esm(() => {
10280
+ NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
10281
+ DISCOVERY_ENV = [
10282
+ "GIT_DIR",
10283
+ "GIT_COMMON_DIR",
10284
+ "GIT_WORK_TREE",
10285
+ "GIT_CEILING_DIRECTORIES",
10286
+ "GIT_DISCOVERY_ACROSS_FILESYSTEM"
10287
+ ];
10288
+ });
10289
+
10290
+ // packages/workit-core/src/core/store-lock.ts
10291
+ import { spawnSync as spawnSync3 } from "node:child_process";
10292
+ import fs6 from "node:fs";
10022
10293
  import { hostname } from "node:os";
10023
10294
  var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, RECLAIM_GUARD_TTL_MS = 30000, defaultLockTimeoutMs = 250, defaultLockTimeout = () => defaultLockTimeoutMs, parseMetadataLock = (raw) => {
10024
10295
  let value;
@@ -10037,9 +10308,6 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
10037
10308
  } catch {
10038
10309
  return null;
10039
10310
  }
10040
- }, sameMetadataLock = (left, right) => {
10041
- const parsed = metadataLockSchema.safeParse(left);
10042
- return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
10043
10311
  }, parseProcStatStart = (stat) => {
10044
10312
  const close = stat.lastIndexOf(")");
10045
10313
  if (close < 0)
@@ -10048,14 +10316,14 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
10048
10316
  }, processStartOf = (pid) => {
10049
10317
  if (process.platform === "linux") {
10050
10318
  try {
10051
- return parseProcStatStart(fs5.readFileSync(`/proc/${pid}/stat`, "utf8"));
10319
+ return parseProcStatStart(fs6.readFileSync(`/proc/${pid}/stat`, "utf8"));
10052
10320
  } catch {
10053
10321
  return null;
10054
10322
  }
10055
10323
  }
10056
10324
  if (process.platform === "darwin" || process.platform === "freebsd") {
10057
10325
  try {
10058
- const run = spawnSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
10326
+ const run = spawnSync3("ps", ["-o", "lstart=", "-p", String(pid)], {
10059
10327
  encoding: "utf8",
10060
10328
  timeout: 1000
10061
10329
  });
@@ -10068,7 +10336,7 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
10068
10336
  return null;
10069
10337
  }, readTrimmed = (file) => {
10070
10338
  try {
10071
- return fs5.readFileSync(file, "utf8").trim() || null;
10339
+ return fs6.readFileSync(file, "utf8").trim() || null;
10072
10340
  } catch {
10073
10341
  return null;
10074
10342
  }
@@ -10077,7 +10345,7 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
10077
10345
  return cachedLockHost;
10078
10346
  let pidns = null;
10079
10347
  try {
10080
- pidns = /\[(\d+)\]/.exec(fs5.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
10348
+ pidns = /\[(\d+)\]/.exec(fs6.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
10081
10349
  } catch {}
10082
10350
  const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
10083
10351
  cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
@@ -10114,7 +10382,7 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
10114
10382
  return { state: "live", reason: `held by running pid ${pid}` };
10115
10383
  }, ageOf = (file, nowMs) => {
10116
10384
  try {
10117
- return nowMs - fs5.lstatSync(file).mtimeMs;
10385
+ return nowMs - fs6.lstatSync(file).mtimeMs;
10118
10386
  } catch {
10119
10387
  return null;
10120
10388
  }
@@ -10124,7 +10392,7 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
10124
10392
  if (age === null || age <= RECLAIM_GUARD_TTL_MS)
10125
10393
  return false;
10126
10394
  try {
10127
- fs5.rmdirSync(guard);
10395
+ fs6.rmdirSync(guard);
10128
10396
  return true;
10129
10397
  } catch {
10130
10398
  return false;
@@ -10133,6 +10401,7 @@ var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, REC
10133
10401
  var init_store_lock = __esm(() => {
10134
10402
  init_zod();
10135
10403
  init_task_contract();
10404
+ init_paths();
10136
10405
  metadataLockSchema = object({
10137
10406
  pid: number2().int().nonnegative().safe(),
10138
10407
  processStart: string2().nullable(),
@@ -10143,129 +10412,622 @@ var init_store_lock = __esm(() => {
10143
10412
  FOREIGN_LOCK_TTL_MS = 10 * 60000;
10144
10413
  });
10145
10414
 
10415
+ // packages/workit-core/src/store/event-log.ts
10416
+ import { randomUUID as randomUUID2 } from "node:crypto";
10417
+ import fs7 from "node:fs";
10418
+ import path8 from "node:path";
10419
+ function readLog(file, from = 0, expectSeq = null) {
10420
+ let fd;
10421
+ try {
10422
+ fd = fs7.openSync(file, "r");
10423
+ } catch (error) {
10424
+ if (error.code === "ENOENT")
10425
+ return null;
10426
+ throw error;
10427
+ }
10428
+ let bytes;
10429
+ let size;
10430
+ try {
10431
+ size = fs7.fstatSync(fd).size;
10432
+ if (from > size)
10433
+ throw new LogDamage(`${file}: shorter than its snapshot`);
10434
+ bytes = Buffer.alloc(size - from);
10435
+ let read = 0;
10436
+ while (read < bytes.length) {
10437
+ const got = fs7.readSync(fd, bytes, read, bytes.length - read, from + read);
10438
+ if (got === 0)
10439
+ break;
10440
+ read += got;
10441
+ }
10442
+ bytes = bytes.subarray(0, read);
10443
+ size = from + read;
10444
+ } finally {
10445
+ fs7.closeSync(fd);
10446
+ }
10447
+ const events = [];
10448
+ let start = 0;
10449
+ let lastStart = from;
10450
+ let seq = expectSeq;
10451
+ for (;; ) {
10452
+ const newline = bytes.indexOf(10, start);
10453
+ if (newline < 0)
10454
+ break;
10455
+ const line = bytes.subarray(start, newline).toString("utf8");
10456
+ if (line.trim() !== "") {
10457
+ const event = parseEvent(line, file, from + start);
10458
+ if (seq !== null && event.seq !== seq)
10459
+ throw new LogDamage(`${file}: event seq ${event.seq} where ${seq} was expected`);
10460
+ seq = event.seq + 1;
10461
+ events.push(event);
10462
+ lastStart = from + start;
10463
+ }
10464
+ start = newline + 1;
10465
+ }
10466
+ return { events, end: from + start, lastStart, size };
10467
+ }
10468
+ function appendEvent(file, event, validEnd) {
10469
+ const line = Buffer.from(`${JSON.stringify(event)}
10470
+ `, "utf8");
10471
+ const created = !fs7.existsSync(file);
10472
+ if (!created && fs7.statSync(file).size !== validEnd) {
10473
+ const cut = fs7.openSync(file, "r+");
10474
+ try {
10475
+ fs7.ftruncateSync(cut, validEnd);
10476
+ fs7.fsyncSync(cut);
10477
+ } finally {
10478
+ fs7.closeSync(cut);
10479
+ }
10480
+ }
10481
+ const fd = fs7.openSync(file, "a", 384);
10482
+ try {
10483
+ let written = 0;
10484
+ while (written < line.length)
10485
+ written += fs7.writeSync(fd, line, written, line.length - written);
10486
+ fs7.fsyncSync(fd);
10487
+ } finally {
10488
+ fs7.closeSync(fd);
10489
+ }
10490
+ if (created)
10491
+ fsyncDirectory(path8.dirname(file));
10492
+ return validEnd;
10493
+ }
10494
+ function replaceFile(file, bytes, durable) {
10495
+ const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
10496
+ try {
10497
+ const fd = fs7.openSync(temporary, "wx", 384);
10498
+ try {
10499
+ fs7.writeSync(fd, bytes);
10500
+ if (durable)
10501
+ fs7.fsyncSync(fd);
10502
+ } finally {
10503
+ fs7.closeSync(fd);
10504
+ }
10505
+ renameWithRetry(temporary, file);
10506
+ if (durable)
10507
+ fsyncDirectory(path8.dirname(file));
10508
+ } catch (error) {
10509
+ try {
10510
+ fs7.unlinkSync(temporary);
10511
+ } catch {}
10512
+ throw error;
10513
+ }
10514
+ }
10515
+ var EVENT_VERSION = 1, LogDamage, isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), parseEvent = (line, file, offset) => {
10516
+ let value;
10517
+ try {
10518
+ value = JSON.parse(line);
10519
+ } catch {
10520
+ throw new LogDamage(`${file}: unreadable event at byte ${offset}`);
10521
+ }
10522
+ if (!isObject2(value) || typeof value.v !== "number")
10523
+ throw new LogDamage(`${file}: malformed event at byte ${offset}`);
10524
+ if (value.v > EVENT_VERSION)
10525
+ throw new LogDamage(`${file}: event format v${value.v} was written by a newer Workit; upgrade Workit to read this task`, true);
10526
+ if (typeof value.seq !== "number" || !Number.isInteger(value.seq) || typeof value.type !== "string" || typeof value.id !== "string" || typeof value.task !== "string" || !isObject2(value.data))
10527
+ throw new LogDamage(`${file}: malformed event at byte ${offset}`);
10528
+ return value;
10529
+ }, fsyncDirectory = (dir) => {
10530
+ try {
10531
+ const fd = fs7.openSync(dir, "r");
10532
+ try {
10533
+ fs7.fsyncSync(fd);
10534
+ } finally {
10535
+ fs7.closeSync(fd);
10536
+ }
10537
+ } catch (error) {
10538
+ if (process.platform !== "win32")
10539
+ throw error;
10540
+ }
10541
+ }, TRANSIENT_WINDOWS_CODES, isTransientWindowsError = (error) => {
10542
+ if (process.platform !== "win32")
10543
+ return false;
10544
+ const code = error?.code;
10545
+ return typeof code === "string" && TRANSIENT_WINDOWS_CODES.has(code);
10546
+ }, retryTransient = (run) => {
10547
+ if (process.platform !== "win32")
10548
+ return run();
10549
+ for (let attempt = 0;; attempt += 1) {
10550
+ try {
10551
+ return run();
10552
+ } catch (error) {
10553
+ if (attempt >= 20 || !isTransientWindowsError(error))
10554
+ throw error;
10555
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 * (attempt + 1));
10556
+ }
10557
+ }
10558
+ }, renameWithRetry = (from, to) => retryTransient(() => fs7.renameSync(from, to));
10559
+ var init_event_log = __esm(() => {
10560
+ LogDamage = class LogDamage extends Error {
10561
+ upgrade;
10562
+ constructor(message, upgrade = false) {
10563
+ super(message);
10564
+ this.upgrade = upgrade;
10565
+ }
10566
+ };
10567
+ TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
10568
+ });
10569
+
10570
+ // packages/workit-core/src/store/patch.ts
10571
+ function diff(before, after, path = []) {
10572
+ if (equal(before, after))
10573
+ return [];
10574
+ if (isPlainObject2(before) && isPlainObject2(after)) {
10575
+ const ops = [];
10576
+ for (const key of Object.keys(before))
10577
+ if (before[key] !== undefined && after[key] === undefined)
10578
+ ops.push({ op: "del", path: [...path, key] });
10579
+ for (const key of Object.keys(after)) {
10580
+ if (after[key] === undefined)
10581
+ continue;
10582
+ if (before[key] === undefined)
10583
+ ops.push({ op: "set", path: [...path, key], value: after[key] });
10584
+ else
10585
+ ops.push(...diff(before[key], after[key], [...path, key]));
10586
+ }
10587
+ return ops;
10588
+ }
10589
+ if (Array.isArray(before) && Array.isArray(after)) {
10590
+ const ops = [];
10591
+ if (after.length < before.length)
10592
+ ops.push({ op: "cut", path, length: after.length });
10593
+ const shared = Math.min(before.length, after.length);
10594
+ let changed = 0;
10595
+ for (let index = 0;index < shared; index += 1)
10596
+ if (!equal(before[index], after[index]))
10597
+ changed += 1;
10598
+ if (shared > 0 && changed > shared / 2 && changed > 4)
10599
+ return [{ op: "set", path, value: after }];
10600
+ for (let index = 0;index < shared; index += 1)
10601
+ ops.push(...diff(before[index], after[index], [...path, index]));
10602
+ if (after.length > before.length)
10603
+ ops.push({ op: "push", path, values: after.slice(before.length) });
10604
+ return ops;
10605
+ }
10606
+ return [{ op: "set", path, value: after }];
10607
+ }
10608
+ function apply(target, ops) {
10609
+ let root = target;
10610
+ for (const op of ops) {
10611
+ if (op.path.length === 0) {
10612
+ if (op.op === "set")
10613
+ root = clone2(op.value);
10614
+ else if (op.op === "push" && Array.isArray(root))
10615
+ root.push(...clone2(op.values));
10616
+ else if (op.op === "cut" && Array.isArray(root))
10617
+ root.length = op.length;
10618
+ else
10619
+ throw new Error(`patch: ${op.op} at the root is invalid`);
10620
+ continue;
10621
+ }
10622
+ let parent = root;
10623
+ for (const key of op.path.slice(0, -1)) {
10624
+ if (typeof parent !== "object" || parent === null)
10625
+ throw new Error(`patch: missing path ${op.path.join(".")}`);
10626
+ parent = parent[key];
10627
+ }
10628
+ if (typeof parent !== "object" || parent === null)
10629
+ throw new Error(`patch: missing path ${op.path.join(".")}`);
10630
+ const key = op.path.at(-1);
10631
+ const container = parent;
10632
+ if (op.op === "set")
10633
+ container[key] = clone2(op.value);
10634
+ else if (op.op === "del")
10635
+ delete container[key];
10636
+ else {
10637
+ const array = container[key];
10638
+ if (!Array.isArray(array))
10639
+ throw new Error(`patch: ${op.path.join(".")} is not an array`);
10640
+ if (op.op === "push")
10641
+ array.push(...clone2(op.values));
10642
+ else
10643
+ array.length = op.length;
10644
+ }
10645
+ }
10646
+ return root;
10647
+ }
10648
+ var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), equal = (left, right) => {
10649
+ if (left === right)
10650
+ return true;
10651
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null)
10652
+ return false;
10653
+ if (Array.isArray(left) !== Array.isArray(right))
10654
+ return false;
10655
+ if (Array.isArray(left)) {
10656
+ const other = right;
10657
+ return left.length === other.length && left.every((item, index) => equal(item, other[index]));
10658
+ }
10659
+ const leftKeys = Object.keys(left).filter((key) => left[key] !== undefined);
10660
+ const rightKeys = Object.keys(right).filter((key) => right[key] !== undefined);
10661
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => equal(left[key], right[key]));
10662
+ }, clone2 = (value) => structuredClone(value), isPath = (value) => Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isInteger(key) && key >= 0), isPatch = (value) => Array.isArray(value) && value.every((op) => {
10663
+ if (!isPlainObject2(op) || !isPath(op.path))
10664
+ return false;
10665
+ if (op.op === "set")
10666
+ return "value" in op;
10667
+ if (op.op === "del")
10668
+ return op.path.length > 0;
10669
+ if (op.op === "push")
10670
+ return Array.isArray(op.values);
10671
+ if (op.op === "cut")
10672
+ return typeof op.length === "number" && Number.isInteger(op.length) && op.length >= 0;
10673
+ return false;
10674
+ });
10675
+
10676
+ // packages/workit-core/src/store/reduce.ts
10677
+ import { createHash as createHash5 } from "node:crypto";
10678
+ function reduce(state, event, blobs) {
10679
+ const data = event.data;
10680
+ const known = KNOWN_TYPES.has(event.type);
10681
+ const carries = "record" in data || "ops" in data || "key" in data;
10682
+ if (!known && !carries) {
10683
+ if (event.critical === true)
10684
+ throw new LogDamage(`event ${event.type} was written by a newer Workit and must be understood; upgrade Workit to read this task`, true);
10685
+ return state;
10686
+ }
10687
+ let next = state;
10688
+ if ("record" in data) {
10689
+ if (!isObject3(data.record))
10690
+ throw new LogDamage(`event ${event.seq} carries a malformed record`);
10691
+ next = {
10692
+ record: decode2(data.record, blobs),
10693
+ key: null,
10694
+ legacy: null
10695
+ };
10696
+ } else if (next === null) {
10697
+ throw new LogDamage(`the log does not start with an opening event (found ${event.type})`);
10698
+ } else
10699
+ next = { ...next };
10700
+ if ("ops" in data) {
10701
+ if (!isPatch(data.ops))
10702
+ throw new LogDamage(`event ${event.seq} carries a malformed patch`);
10703
+ try {
10704
+ next.record = apply(next.record, decode2(data.ops, blobs));
10705
+ } catch (error) {
10706
+ throw new LogDamage(`event ${event.seq} does not apply: ${String(error)}`);
10707
+ }
10708
+ }
10709
+ if ("key" in data)
10710
+ next.key = isKey(data.key) ? data.key : null;
10711
+ if ("legacy" in data && isObject3(data.legacy))
10712
+ next.legacy = {
10713
+ path: String(data.legacy.path ?? ""),
10714
+ digest: String(data.legacy.digest ?? "")
10715
+ };
10716
+ return next;
10717
+ }
10718
+ function eventType(before, after) {
10719
+ const count = (value) => Array.isArray(value) ? value.length : 0;
10720
+ if (before.status !== after.status)
10721
+ return after.status === "closed" ? "task.closed" : after.status === "paused" ? "task.paused" : "task.resumed";
10722
+ if (count(after.evidence) > count(before.evidence)) {
10723
+ const last = after.evidence.at(-1);
10724
+ return last?.data?.observation ? "check.ran" : "evidence.recorded";
10725
+ }
10726
+ if (count(after.findings) > count(before.findings))
10727
+ return "finding.recorded";
10728
+ if (count(after.decisions) > count(before.decisions))
10729
+ return "decision.recorded";
10730
+ if (JSON.stringify(before.findings) !== JSON.stringify(after.findings))
10731
+ return "finding.updated";
10732
+ if (JSON.stringify(before.decisions) !== JSON.stringify(after.decisions))
10733
+ return "decision.updated";
10734
+ if (JSON.stringify(before.workers) !== JSON.stringify(after.workers))
10735
+ return "worker.updated";
10736
+ if (JSON.stringify(before.intent) !== JSON.stringify(after.intent))
10737
+ return "task.revised";
10738
+ if (JSON.stringify(before.assessments) !== JSON.stringify(after.assessments) || JSON.stringify(before.policy) !== JSON.stringify(after.policy))
10739
+ return "task.judged";
10740
+ if (JSON.stringify(before.progress) !== JSON.stringify(after.progress))
10741
+ return "task.noted";
10742
+ return "task.updated";
10743
+ }
10744
+ var OPENING_TYPES, KNOWN_TYPES, isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), isKey = (value) => isObject3(value) && typeof value.key === "string" && (value.kind === "branch" || value.kind === "detached" || value.kind === "dir") && (value.branch === null || typeof value.branch === "string"), BLOB_REF = "$blob", 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, blobDigest = (value) => createHash5("sha256").update(JSON.stringify(value)).digest("hex"), toBlob = (value, blobs) => {
10745
+ if (!isObject3(value))
10746
+ return value;
10747
+ const digest = blobDigest(value);
10748
+ blobs.write(digest, value);
10749
+ return { [BLOB_REF]: digest };
10750
+ }, encodeRecord = (record, blobs) => Array.isArray(record.candidates) ? { ...record, candidates: record.candidates.map((item) => toBlob(item, blobs)) } : record, encodeOps = (ops, blobs) => ops.map((op) => {
10751
+ if (op.path[0] !== "candidates")
10752
+ return op;
10753
+ if (op.op === "push" && op.path.length === 1)
10754
+ return { ...op, values: op.values.map((item) => toBlob(item, blobs)) };
10755
+ if (op.op === "set" && op.path.length === 1 && Array.isArray(op.value))
10756
+ return { ...op, value: op.value.map((item) => toBlob(item, blobs)) };
10757
+ if (op.op === "set" && op.path.length === 2)
10758
+ return { ...op, value: toBlob(op.value, blobs) };
10759
+ return op;
10760
+ }), decode2 = (value, blobs) => {
10761
+ const digest = blobRef(value);
10762
+ if (digest)
10763
+ return blobs.read(digest);
10764
+ if (Array.isArray(value))
10765
+ return value.map((item) => decode2(item, blobs));
10766
+ if (isObject3(value)) {
10767
+ const out = {};
10768
+ for (const [key, item] of Object.entries(value))
10769
+ out[key] = decode2(item, blobs);
10770
+ return out;
10771
+ }
10772
+ return value;
10773
+ };
10774
+ var init_reduce = __esm(() => {
10775
+ init_event_log();
10776
+ OPENING_TYPES = new Set([
10777
+ "task.opened",
10778
+ "task.imported",
10779
+ "migrated.from_v2",
10780
+ "task.checkpoint"
10781
+ ]);
10782
+ KNOWN_TYPES = new Set([
10783
+ ...OPENING_TYPES,
10784
+ "task.bound",
10785
+ "task.noted",
10786
+ "task.revised",
10787
+ "task.judged",
10788
+ "task.paused",
10789
+ "task.resumed",
10790
+ "task.closed",
10791
+ "task.updated",
10792
+ "check.ran",
10793
+ "evidence.recorded",
10794
+ "finding.recorded",
10795
+ "finding.updated",
10796
+ "decision.recorded",
10797
+ "decision.updated",
10798
+ "worker.updated"
10799
+ ]);
10800
+ });
10801
+
10802
+ // packages/workit-core/src/store/notes.ts
10803
+ var SLOT, reportMigration = (report) => {
10804
+ try {
10805
+ const reporter = globalThis[SLOT];
10806
+ if (typeof reporter === "function")
10807
+ reporter(report);
10808
+ } catch {}
10809
+ }, migrationReporterInstalled = () => typeof globalThis[SLOT] === "function";
10810
+ var init_notes = __esm(() => {
10811
+ SLOT = Symbol.for("workit.migrationReporter");
10812
+ });
10813
+
10146
10814
  // packages/workit-core/src/core/task-store.ts
10147
- import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
10148
10815
  import { AsyncLocalStorage } from "node:async_hooks";
10149
- import * as fs6 from "node:fs";
10150
- import path7 from "node:path";
10816
+ import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
10817
+ import * as fs8 from "node:fs";
10818
+ import path9 from "node:path";
10151
10819
 
10152
10820
  class TaskStore {
10153
10821
  root;
10822
+ origin;
10154
10823
  lockTimeoutMs;
10824
+ actor;
10825
+ migrateOnRead;
10826
+ located;
10827
+ migrationChecked = false;
10828
+ compactDue = new Set;
10155
10829
  constructor(root, options = {}) {
10156
- this.root = fs6.existsSync(root) ? fs6.realpathSync(root) : path7.resolve(root);
10830
+ const resolved = path9.resolve(root);
10831
+ this.origin = fs8.existsSync(resolved) ? fs8.realpathSync(resolved) : resolved;
10832
+ this.located = resolveStore(this.origin);
10833
+ this.root = checkoutRootOf(this.origin, this.located);
10157
10834
  this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
10835
+ this.actor = options.actor ?? envActor();
10836
+ this.migrateOnRead = options.migrateOnRead;
10837
+ }
10838
+ location() {
10839
+ if (this.located !== undefined && !(this.located instanceof Error) && !this.located.shared && fs8.existsSync(path9.join(this.root, ".git"))) {
10840
+ this.located = undefined;
10841
+ this.migrationChecked = false;
10842
+ }
10843
+ if (this.located === undefined)
10844
+ this.located = resolveStore(this.root);
10845
+ return this.located instanceof Error ? failure2("storage_error", this.located.message, { path: this.root }) : success(null, null, this.located);
10846
+ }
10847
+ currentKey() {
10848
+ const location = this.location();
10849
+ if (!location.ok)
10850
+ return location;
10851
+ return success(null, null, resolveTaskKey(this.root, location.data));
10158
10852
  }
10159
10853
  readTask(taskId) {
10160
10854
  if (!validId(taskId))
10161
10855
  return failure2("invalid_input", "task ID is invalid", { taskId });
10162
- const result = this.readRecord(path7.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
10163
- if (!result.exists)
10856
+ const ready = this.ready();
10857
+ if (!ready.ok)
10858
+ return ready;
10859
+ const loaded = this.load(taskId);
10860
+ if (!loaded.ok)
10861
+ return loaded;
10862
+ if (!loaded.data)
10164
10863
  return failure2("not_found", "task not found", { taskId });
10165
- if (result.result.ok && result.result.data.id !== taskId)
10166
- return failure2("recovery_required", "task filename and record ID differ", { taskId });
10167
- if (result.result.ok) {
10168
- const workspace = this.readWorkspace();
10169
- if (!workspace.ok)
10170
- return workspace;
10171
- if (!workspace.data || result.result.data.workspaceId !== workspace.data.id)
10172
- return failure2("recovery_required", "task workspace binding is invalid", { taskId });
10173
- }
10174
- return result.result;
10175
- }
10176
- listTasks() {
10177
- if (!fs6.existsSync(this.tasksDir))
10178
- return success(null, null, []);
10179
- let names;
10180
- try {
10181
- names = fs6.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
10182
- } catch (error) {
10183
- return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
10184
- path: this.tasksDir
10185
- });
10186
- }
10187
10864
  const workspace = this.readWorkspace();
10188
10865
  if (!workspace.ok)
10189
10866
  return workspace;
10190
- if (!workspace.data)
10191
- return failure2("recovery_required", "task workspace binding is invalid", {
10192
- path: this.workspacePath
10193
- });
10867
+ const bound = this.bindingError(loaded.data.record, workspace.data);
10868
+ return bound ?? success(null, null, loaded.data.record);
10869
+ }
10870
+ listTasks() {
10871
+ const listed = this.listTaskIndex();
10872
+ if (!listed.ok)
10873
+ return listed;
10194
10874
  const tasks = [];
10195
- for (const name of names.toSorted()) {
10196
- const item = this.readRecord(path7.join(this.tasksDir, name), taskRecordSchema);
10197
- if (!item.exists)
10198
- continue;
10199
- if (!item.result.ok)
10200
- return item.result;
10201
- if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
10202
- return failure2("recovery_required", "task filename and record ID differ", { path: name });
10203
- if (item.result.data.workspaceId !== workspace.data.id)
10204
- return failure2("recovery_required", "task workspace binding is invalid", { path: name });
10205
- tasks.push(item.result.data);
10875
+ for (const entry of listed.data.toSorted((left, right) => left.id.localeCompare(right.id))) {
10876
+ const loaded = this.load(entry.id);
10877
+ if (!loaded.ok)
10878
+ return loaded;
10879
+ if (loaded.data)
10880
+ tasks.push(loaded.data.record);
10206
10881
  }
10207
10882
  return success(null, null, tasks);
10208
10883
  }
10209
10884
  listTaskIndex() {
10210
- if (!fs6.existsSync(this.tasksDir))
10885
+ const all = this.listStoreIndex();
10886
+ if (!all.ok)
10887
+ return all;
10888
+ const workspace = this.readWorkspace();
10889
+ if (!workspace.ok)
10890
+ return workspace;
10891
+ if (!workspace.data)
10211
10892
  return success(null, null, []);
10893
+ const id = workspace.data.id;
10894
+ return success(null, null, all.data.filter((entry) => entry.workspaceId === id));
10895
+ }
10896
+ listStoreIndex() {
10897
+ const ready = this.ready();
10898
+ if (!ready.ok)
10899
+ return ready;
10900
+ const tasksDir = this.paths().tasks;
10212
10901
  let names;
10213
10902
  try {
10214
- names = fs6.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
10903
+ names = fs8.existsSync(tasksDir) ? fs8.readdirSync(tasksDir).filter((name) => validId(name)) : [];
10215
10904
  } catch (error) {
10216
10905
  return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
10217
- path: this.tasksDir
10906
+ path: tasksDir
10218
10907
  });
10219
10908
  }
10220
- const workspace = this.readWorkspace();
10221
- if (!workspace.ok)
10222
- return workspace;
10223
- if (!workspace.data)
10224
- return failure2("recovery_required", "task workspace binding is invalid", {
10225
- path: this.workspacePath
10226
- });
10227
- const stored = this.readIndex(workspace.data.id);
10909
+ const stored = this.readIndex();
10228
10910
  const entries = [];
10229
10911
  let changed = Object.keys(stored).length !== names.length;
10230
10912
  for (const name of names.toSorted()) {
10231
- const file = path7.join(this.tasksDir, name);
10232
- const signature = fileSignature(file);
10913
+ const signature = fileSignature(this.eventsPath(name));
10233
10914
  if (signature === null) {
10234
10915
  changed = true;
10235
10916
  continue;
10236
10917
  }
10237
- const cached = stored[name.slice(0, -5)];
10918
+ const cached = stored[name];
10238
10919
  if (cached && cached.file === signature && !racySignature(signature)) {
10239
10920
  entries.push(cached);
10240
10921
  continue;
10241
10922
  }
10242
- const item = this.readRecord(file, taskRecordSchema);
10243
- if (!item.exists)
10923
+ const loaded = this.load(name);
10924
+ if (!loaded.ok)
10925
+ return loaded;
10926
+ if (!loaded.data)
10244
10927
  continue;
10245
- if (!item.result.ok)
10246
- return item.result;
10247
- if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
10248
- return failure2("recovery_required", "task filename and record ID differ", { path: name });
10249
- if (item.result.data.workspaceId !== workspace.data.id)
10250
- return failure2("recovery_required", "task workspace binding is invalid", { path: name });
10251
- const entry = indexEntry(item.result.data, signature);
10928
+ const entry = indexEntry(loaded.data.record, loaded.data.key, loaded.data.legacy !== null, signature);
10252
10929
  if (!cached || canonicalJson(cached) !== canonicalJson(entry))
10253
10930
  changed = true;
10254
10931
  entries.push(entry);
10255
10932
  }
10256
10933
  if (changed)
10257
- this.writeIndex(workspace.data.id, entries);
10934
+ this.writeIndex(entries);
10258
10935
  return success(null, null, entries);
10259
10936
  }
10260
10937
  readWorkspace() {
10261
- const item = this.readRecord(this.workspacePath, workspaceRecordSchema);
10262
- if (!item.exists)
10263
- return success(null, null, null);
10264
- if (item.result.ok && item.result.data.root !== this.root && !sameDirectoryIdentity(item.result.data.root, this.root))
10265
- return failure2("recovery_required", "workspace root binding is invalid", {
10266
- path: this.workspacePath
10938
+ const ready = this.ready();
10939
+ if (!ready.ok)
10940
+ return ready;
10941
+ const file = this.paths().workspace;
10942
+ let bytes;
10943
+ try {
10944
+ bytes = retryTransient(() => fs8.readFileSync(file, "utf8"));
10945
+ } catch (error) {
10946
+ if (error.code === "ENOENT")
10947
+ return success(null, null, null);
10948
+ return failure2("recovery_required", `workspace record cannot be read: ${String(error)}`, {
10949
+ path: file
10267
10950
  });
10268
- return item.result;
10951
+ }
10952
+ const parsed = this.parseRecord(bytes, workspaceRecordSchema, "workspace");
10953
+ if (!parsed.ok)
10954
+ return parsed;
10955
+ if (parsed.data.root !== this.root && !sameDirectoryIdentity(parsed.data.root, this.root))
10956
+ return failure2("recovery_required", "workspace root binding is invalid", { path: file });
10957
+ return parsed;
10958
+ }
10959
+ implicitTask(input) {
10960
+ const ready = this.ready(input.create !== false);
10961
+ if (!ready.ok)
10962
+ return ready;
10963
+ const key = this.currentKey();
10964
+ if (!key.ok)
10965
+ return key;
10966
+ const found = this.openTaskForKey(key.data.key);
10967
+ if (!found.ok)
10968
+ return found;
10969
+ const workspace = this.readWorkspace();
10970
+ if (!workspace.ok)
10971
+ return workspace;
10972
+ if (found.data && workspace.data && found.data.workspaceId === workspace.data.id && (found.data.key === key.data.key || input.create === false)) {
10973
+ const task = this.readTask(found.data.id);
10974
+ return task.ok ? success(task.revision, null, { task: task.data, created: false, key: key.data }) : task;
10975
+ }
10976
+ if (input.create === false) {
10977
+ if (!found.data)
10978
+ return success(null, null, null);
10979
+ const loaded = this.load(found.data.id);
10980
+ if (!loaded.ok)
10981
+ return loaded;
10982
+ return loaded.data ? success(null, null, { task: loaded.data.record, created: false, key: key.data }) : success(null, null, null);
10983
+ }
10984
+ return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => this.implicitLocked(input, key.data)));
10985
+ }
10986
+ implicitLocked(input, key) {
10987
+ {
10988
+ const again = this.openTaskForKey(key.key);
10989
+ if (!again.ok)
10990
+ return again;
10991
+ if (again.data) {
10992
+ const moved = this.bindLocked(again.data.id, key, input.now);
10993
+ return moved.ok ? success(moved.data.revision, null, { task: moved.data, created: false, key }) : moved;
10994
+ }
10995
+ const label = key.kind === "branch" ? `branch ${key.branch}` : key.kind === "detached" ? "a detached HEAD" : path9.basename(this.root) || this.root;
10996
+ const intent = {
10997
+ objective: input.objective?.trim() || `Work on ${label}`,
10998
+ scope: { description: `the checkout (${label})`, paths: ["."], exclusions: [] },
10999
+ authorityRefs: []
11000
+ };
11001
+ const created = this.createLocked({ intent, provenance: input.provenance, expectedWorkspaceRevision: null, now: input.now }, false, key, false);
11002
+ return created.ok ? success(created.data.revision, null, { task: created.data, created: true, key }) : created;
11003
+ }
11004
+ }
11005
+ adoptTask(taskId) {
11006
+ if (!validId(taskId))
11007
+ return failure2("invalid_input", "task ID is invalid", { taskId });
11008
+ const ready = this.ready(true);
11009
+ if (!ready.ok)
11010
+ return ready;
11011
+ const key = this.currentKey();
11012
+ if (!key.ok)
11013
+ return key;
11014
+ return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => {
11015
+ const holder = this.openTaskForKey(key.data.key);
11016
+ if (!holder.ok)
11017
+ return holder;
11018
+ if (holder.data && holder.data.id !== taskId)
11019
+ 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 });
11020
+ return this.bindLocked(taskId, key.data);
11021
+ }));
11022
+ }
11023
+ keyReport() {
11024
+ const key = this.currentKey();
11025
+ if (!key.ok)
11026
+ return key;
11027
+ const bound = this.openTasksForKey(key.data.key);
11028
+ if (!bound.ok)
11029
+ return bound;
11030
+ return success(null, null, { key: key.data, bound: bound.data });
10269
11031
  }
10270
11032
  create(input, provenance, expectedWorkspaceRevision) {
10271
11033
  const value = "intent" in input ? input : {
@@ -10273,82 +11035,16 @@ class TaskStore {
10273
11035
  provenance,
10274
11036
  expectedWorkspaceRevision: expectedWorkspaceRevision ?? null
10275
11037
  };
10276
- return this.withLock(() => {
10277
- const current = this.readWorkspace();
10278
- if (!current.ok)
10279
- return current;
10280
- if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
10281
- return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
10282
- expectedWorkspaceRevision: value.expectedWorkspaceRevision,
10283
- actualWorkspaceRevision: current.data?.revision ?? null
10284
- });
10285
- }
10286
- if (!value.provenance)
10287
- return failure2("invalid_input", "provenance is required");
10288
- if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
10289
- return failure2("invalid_input", "task intent or provenance is invalid");
10290
- const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
10291
- if (current.data && !previousWorkspaceBytes)
10292
- return failure2("storage_error", "workspace snapshot disappeared during creation");
10293
- const workspace = current.data ? {
10294
- ...current.data,
10295
- revision: newRevision(),
10296
- root: this.root,
10297
- runtime: stampNew(current.data)
10298
- } : {
10299
- schemaVersion: SCHEMA_VERSION,
10300
- id: newId(),
10301
- revision: newRevision(),
10302
- root: this.root,
10303
- runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
10304
- writer: null
10305
- };
10306
- const timestamp = value.now ?? now();
10307
- const task = {
10308
- schemaVersion: SCHEMA_VERSION,
10309
- id: newId(),
10310
- workspaceId: workspace.id,
10311
- revision: newRevision(),
10312
- createdAt: timestamp,
10313
- updatedAt: timestamp,
10314
- origin: null,
10315
- intent: {
10316
- id: newId(),
10317
- recordedAt: timestamp,
10318
- provenance: value.provenance,
10319
- data: value.intent
10320
- },
10321
- constraints: [],
10322
- status: "active",
10323
- closure: null,
10324
- progress: { summary: "", nextAction: null, blockers: [] },
10325
- runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
10326
- assessments: [],
10327
- policy: null,
10328
- policyChanges: [],
10329
- candidates: [],
10330
- evidence: [],
10331
- decisions: [],
10332
- findings: [],
10333
- workers: []
10334
- };
10335
- const writtenWorkspace = this.replaceSnapshot(this.workspacePath, workspace, previousWorkspaceBytes);
10336
- if (!writtenWorkspace.ok)
10337
- return writtenWorkspace;
10338
- const writtenTask = this.replaceSnapshot(this.taskPath(task.id), task, null);
10339
- if (!writtenTask.ok) {
10340
- if (current.data && previousWorkspaceBytes) {
10341
- const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
10342
- if (!restored.ok)
10343
- return failure2("external_outcome_unknown", "task creation failed and workspace restoration is uncertain", { operation: "create", outcome: "unknown" });
10344
- }
10345
- return failure2("external_outcome_unknown", "workspace created but task write is uncertain", { operation: "create", outcome: "unknown" });
10346
- }
10347
- return success(task.revision, workspace.revision, task);
10348
- });
11038
+ const ready = this.ready(true);
11039
+ if (!ready.ok)
11040
+ return ready;
11041
+ return this.withLock(this.paths().checkoutLock, () => this.createLocked(value, true, null));
10349
11042
  }
10350
11043
  importTask(input) {
10351
- return this.withLock(() => {
11044
+ const ready = this.ready(true);
11045
+ if (!ready.ok)
11046
+ return ready;
11047
+ return this.withLock(this.paths().checkoutLock, () => {
10352
11048
  const current = this.readWorkspace();
10353
11049
  if (!current.ok)
10354
11050
  return current;
@@ -10380,23 +11076,8 @@ class TaskStore {
10380
11076
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
10381
11077
  actualWorkspaceRevision: current.data?.revision ?? null
10382
11078
  });
10383
- const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
10384
- if (current.data && !previousWorkspaceBytes)
10385
- return failure2("storage_error", "workspace snapshot disappeared during import");
10386
11079
  const timestamp = input.now ?? now();
10387
- const workspace = current.data ? {
10388
- ...current.data,
10389
- revision: newRevision(),
10390
- root: this.root,
10391
- runtime: stampNew(current.data)
10392
- } : {
10393
- schemaVersion: SCHEMA_VERSION,
10394
- id: input.workspaceId ?? newId(),
10395
- revision: newRevision(),
10396
- root: this.root,
10397
- runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
10398
- writer: null
10399
- };
11080
+ const workspace = this.nextWorkspace(current.data, input.workspaceId);
10400
11081
  const task = {
10401
11082
  ...input.task,
10402
11083
  workspaceId: workspace.id,
@@ -10411,62 +11092,46 @@ class TaskStore {
10411
11092
  const validTask = taskRecordSchema.safeParse(task);
10412
11093
  if (!validTask.success)
10413
11094
  return failure2("invalid_input", "imported task does not satisfy its schema");
10414
- const writtenWorkspace = this.replaceSnapshot(this.workspacePath, workspace, previousWorkspaceBytes);
10415
- if (!writtenWorkspace.ok)
10416
- return writtenWorkspace;
10417
- const writtenTask = this.replaceSnapshot(this.taskPath(task.id), validTask.data, null);
10418
- if (!writtenTask.ok) {
10419
- if (current.data && previousWorkspaceBytes) {
10420
- const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
10421
- if (!restored.ok)
10422
- return failure2("external_outcome_unknown", "import failed and workspace restoration is uncertain", { operation: "import", outcome: "unknown" });
10423
- }
10424
- return failure2("external_outcome_unknown", `workspace created but task import is uncertain: ${writtenTask.error}`, {
10425
- operation: "import",
10426
- outcome: "unknown",
10427
- path: writtenTask.details.path
10428
- });
10429
- }
10430
- return success(validTask.data.revision, workspace.revision, validTask.data);
11095
+ return this.writeNewTask(current.data, workspace, validTask.data, "task.imported", null, {
11096
+ operation: "import"
11097
+ });
10431
11098
  });
10432
11099
  }
10433
11100
  mutateTask(taskId, expected, update, timestamp) {
10434
- return this.withLock(() => {
10435
- const current = this.readTask(taskId);
10436
- if (!current.ok)
10437
- return current;
10438
- if (current.data.revision !== expected)
10439
- return this.conflict(expected, current.data.revision);
10440
- const previousBytes = this.snapshotBytes(this.taskPath(taskId));
10441
- if (!previousBytes)
10442
- return failure2("storage_error", "task snapshot disappeared during mutation");
11101
+ if (!validId(taskId))
11102
+ return failure2("invalid_input", "task ID is invalid", { taskId });
11103
+ const ready = this.ready(true);
11104
+ if (!ready.ok)
11105
+ return ready;
11106
+ const result = this.withLock(this.taskLockPath(taskId), () => {
11107
+ const loaded = this.loadBound(taskId);
11108
+ if (!loaded.ok)
11109
+ return loaded;
11110
+ if (loaded.data.record.revision !== expected)
11111
+ return this.conflict(expected, loaded.data.record.revision);
10443
11112
  const context = { now: timestamp ?? now(), revision: newRevision() };
10444
11113
  let changed;
10445
11114
  try {
10446
- changed = update(current.data, Object.freeze({ ...context }));
11115
+ changed = update(structuredClone(loaded.data.record), Object.freeze({ ...context }));
10447
11116
  } catch (error) {
10448
11117
  return failure2("storage_error", `task mutation failed: ${String(error)}`);
10449
11118
  }
10450
11119
  if (!changed.ok)
10451
11120
  return changed;
10452
- const record = {
10453
- ...changed.data,
10454
- id: taskId,
10455
- workspaceId: current.data.workspaceId,
10456
- createdAt: current.data.createdAt,
10457
- revision: context.revision,
10458
- updatedAt: context.now,
10459
- runtime: stampNew(current.data)
10460
- };
10461
- const valid = taskRecordSchema.safeParse(record);
10462
- if (!valid.success)
11121
+ const valid = this.nextTask(loaded.data.record, changed.data, context);
11122
+ if (!valid)
10463
11123
  return failure2("invalid_input", "task mutation produced an invalid record");
10464
- const written = this.replaceSnapshot(this.taskPath(taskId), valid.data, previousBytes);
10465
- return written.ok ? success(valid.data.revision, null, valid.data) : written;
11124
+ const written = this.appendPatch(loaded.data, valid);
11125
+ return written.ok ? success(valid.revision, null, valid) : written;
10466
11126
  });
11127
+ this.compactPending();
11128
+ return result;
10467
11129
  }
10468
11130
  mutateWorkspace(expected, update) {
10469
- return this.withLock(() => {
11131
+ const ready = this.ready(true);
11132
+ if (!ready.ok)
11133
+ return ready;
11134
+ return this.withLock(this.paths().checkoutLock, () => {
10470
11135
  const current = this.readWorkspace();
10471
11136
  if (!current.ok)
10472
11137
  return current;
@@ -10474,9 +11139,6 @@ class TaskStore {
10474
11139
  return failure2("not_found", "workspace not found");
10475
11140
  if (current.data.revision !== expected)
10476
11141
  return this.workspaceConflict(expected, current.data.revision);
10477
- const previousBytes = this.snapshotBytes(this.workspacePath);
10478
- if (!previousBytes)
10479
- return failure2("storage_error", "workspace snapshot disappeared during mutation");
10480
11142
  const context = { now: now(), revision: newRevision() };
10481
11143
  let changed;
10482
11144
  try {
@@ -10486,23 +11148,27 @@ class TaskStore {
10486
11148
  }
10487
11149
  if (!changed.ok)
10488
11150
  return changed;
10489
- const record = {
11151
+ const valid = workspaceRecordSchema.safeParse({
10490
11152
  ...changed.data,
10491
11153
  id: current.data.id,
10492
11154
  root: this.root,
10493
11155
  revision: context.revision,
10494
11156
  runtime: stampNew(current.data)
10495
- };
10496
- const valid = workspaceRecordSchema.safeParse(record);
11157
+ });
10497
11158
  if (!valid.success)
10498
11159
  return failure2("invalid_input", "workspace mutation produced an invalid record");
10499
- const written = this.replaceSnapshot(this.workspacePath, valid.data, previousBytes);
11160
+ const written = this.writeWorkspace(valid.data);
10500
11161
  return written.ok ? success(valid.data.revision, valid.data.revision, valid.data) : written;
10501
11162
  });
10502
11163
  }
10503
11164
  mutateTaskAndWorkspace(input) {
10504
- return this.withLock(() => {
10505
- const task = this.readTask(input.taskId);
11165
+ if (!validId(input.taskId))
11166
+ return failure2("invalid_input", "task ID is invalid", { taskId: input.taskId });
11167
+ const ready = this.ready(true);
11168
+ if (!ready.ok)
11169
+ return ready;
11170
+ const result = this.withLock(this.paths().checkoutLock, () => this.withLock(this.taskLockPath(input.taskId), () => {
11171
+ const task = this.loadBound(input.taskId);
10506
11172
  if (!task.ok)
10507
11173
  return task;
10508
11174
  const workspace = this.readWorkspace();
@@ -10513,14 +11179,10 @@ class TaskStore {
10513
11179
  const expected = input.expectedRevision;
10514
11180
  if (!expected)
10515
11181
  return failure2("invalid_input", "task revision is required");
10516
- if (task.data.revision !== expected)
10517
- return this.conflict(expected, task.data.revision);
11182
+ if (task.data.record.revision !== expected)
11183
+ return this.conflict(expected, task.data.record.revision);
10518
11184
  if (workspace.data.revision !== input.expectedWorkspaceRevision)
10519
11185
  return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
10520
- const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
10521
- const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
10522
- if (!previousWorkspaceBytes || !previousTaskBytes)
10523
- return failure2("storage_error", "snapshot disappeared during coupled mutation");
10524
11186
  const workspaceContext = { now: input.now ?? now(), revision: newRevision() };
10525
11187
  let changedWorkspace;
10526
11188
  try {
@@ -10539,286 +11201,69 @@ class TaskStore {
10539
11201
  });
10540
11202
  if (!nextWorkspace.success)
10541
11203
  return failure2("invalid_input", "workspace mutation produced an invalid record");
10542
- const reserved = this.replaceSnapshot(this.workspacePath, nextWorkspace.data, previousWorkspaceBytes);
11204
+ const reserved = this.writeWorkspace(nextWorkspace.data);
10543
11205
  if (!reserved.ok)
10544
11206
  return reserved;
11207
+ const uncertain = (message) => {
11208
+ this.markUncertain(nextWorkspace.data);
11209
+ return failure2("external_outcome_unknown", message, {
11210
+ operation: "coupled_mutation",
11211
+ outcome: "unknown"
11212
+ });
11213
+ };
10545
11214
  const taskContext = { now: input.now ?? now(), revision: newRevision() };
10546
11215
  let changedTask;
10547
11216
  try {
10548
- changedTask = input.task(task.data, Object.freeze({ ...taskContext }));
11217
+ changedTask = input.task(structuredClone(task.data.record), Object.freeze({ ...taskContext }));
10549
11218
  } catch (error) {
10550
- this.markUncertain(nextWorkspace.data);
10551
- return failure2("external_outcome_unknown", `workspace reserved but task mutation threw: ${String(error)}`, { operation: "coupled_mutation", outcome: "unknown" });
10552
- }
10553
- if (!changedTask.ok) {
10554
- this.markUncertain(nextWorkspace.data);
10555
- return failure2("external_outcome_unknown", "workspace reserved but task update is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
10556
- }
10557
- const nextTask = taskRecordSchema.safeParse({
10558
- ...changedTask.data,
10559
- id: input.taskId,
10560
- workspaceId: task.data.workspaceId,
10561
- createdAt: task.data.createdAt,
10562
- revision: taskContext.revision,
10563
- updatedAt: taskContext.now,
10564
- runtime: stampNew(task.data)
10565
- });
10566
- if (!nextTask.success) {
10567
- this.markUncertain(nextWorkspace.data);
10568
- return failure2("external_outcome_unknown", "workspace reserved but task update is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
10569
- }
10570
- const written = this.replaceSnapshot(this.taskPath(input.taskId), nextTask.data, previousTaskBytes);
10571
- if (!written.ok) {
10572
- this.markUncertain(nextWorkspace.data);
10573
- return failure2("external_outcome_unknown", "workspace reserved but task replacement is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
11219
+ return uncertain(`workspace reserved but task mutation threw: ${String(error)}`);
10574
11220
  }
10575
- return success(nextTask.data.revision, nextWorkspace.data.revision, {
10576
- task: nextTask.data,
11221
+ if (!changedTask.ok)
11222
+ return uncertain("workspace reserved but task update is uncertain");
11223
+ const nextTask = this.nextTask(task.data.record, changedTask.data, taskContext);
11224
+ if (!nextTask)
11225
+ return uncertain("workspace reserved but task update is uncertain");
11226
+ const written = this.appendPatch(task.data, nextTask);
11227
+ if (!written.ok)
11228
+ return uncertain("workspace reserved but task append is uncertain");
11229
+ return success(nextTask.revision, nextWorkspace.data.revision, {
11230
+ task: nextTask,
10577
11231
  workspace: nextWorkspace.data
10578
11232
  });
10579
- });
10580
- }
10581
- recoveryCandidates() {
10582
- if (!fs6.existsSync(this.recoveryDir))
10583
- return success(null, null, []);
10584
- try {
10585
- const candidates = [];
10586
- for (const name of fs6.readdirSync(this.recoveryDir)) {
10587
- const match = RECOVERY_NAME.exec(name);
10588
- if (match)
10589
- candidates.push({
10590
- target: match[1],
10591
- path: path7.join(this.recoveryDir, name),
10592
- digest: match[3]
10593
- });
10594
- }
10595
- return success(null, null, candidates);
10596
- } catch (error) {
10597
- return failure2("storage_error", `unable to list recovery: ${String(error)}`, {
10598
- path: this.recoveryDir
10599
- });
10600
- }
10601
- }
10602
- recoverTask(taskId, input) {
10603
- return this.recover("task", taskId, input);
10604
- }
10605
- recoverWorkspace(input) {
10606
- return this.recover("workspace", null, input);
10607
- }
10608
- recover(target, taskId, input) {
10609
- if (taskId !== null && !validId(taskId))
10610
- return failure2("invalid_input", "task ID is invalid", { taskId });
10611
- if (!input.reason || !Array.isArray(input.authorityRefs) || typeof input.processEvidence !== "function" || !validDigest(input.expectedBytes) || !validDigest(input.snapshotDigest))
10612
- return failure2("invalid_input", "recovery authority is invalid");
10613
- if (input.authorityRefs.some((ref) => !refSchema.safeParse(ref).success))
10614
- return failure2("invalid_input", "recovery references are invalid");
10615
- try {
10616
- const workspace = this.readWorkspace();
10617
- if (!workspace.ok && target === "task")
10618
- return workspace;
10619
- const workspaceValue = workspace.ok ? workspace.data : null;
10620
- if (workspaceValue && workspaceValue.revision !== input.expectedWorkspaceRevision)
10621
- return this.conflict(input.expectedWorkspaceRevision, workspaceValue.revision);
10622
- const file = target === "workspace" ? this.workspacePath : this.taskPath(taskId);
10623
- const currentBytes = this.snapshotBytes(file);
10624
- if (!currentBytes)
10625
- return failure2("not_found", "snapshot not found");
10626
- if (digestBytes(currentBytes) !== input.expectedBytes)
10627
- return failure2("revision_conflict", "snapshot bytes do not match expected bytes");
10628
- let selectedBytes = currentBytes;
10629
- if (digestBytes(currentBytes) !== input.snapshotDigest) {
10630
- const candidate = this.findRecovery(target, taskId, input.snapshotDigest, workspaceValue);
10631
- if (!candidate)
10632
- return failure2("recovery_required", "validated recovery snapshot not found");
10633
- selectedBytes = candidate;
10634
- }
10635
- const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
10636
- if (!selectedRecord.ok)
10637
- return selectedRecord;
10638
- if (target === "workspace") {
10639
- const parsed = selectedRecord;
10640
- if (!parsed.ok || parsed.data.root !== this.root)
10641
- return failure2("recovery_required", "workspace recovery binding is invalid");
10642
- } else {
10643
- const parsed = selectedRecord;
10644
- if (!parsed.ok || parsed.data.id !== taskId || !workspaceValue || parsed.data.workspaceId !== workspaceValue.id)
10645
- return failure2("recovery_required", "task recovery binding is invalid");
10646
- }
10647
- const lock = this.readLockSnapshot();
10648
- if (!lock.ok)
10649
- return lock;
10650
- const writer = target === "workspace" ? selectedRecord.data.writer : workspaceValue?.writer ?? null;
10651
- const evidence = this.processEvidence(input, lock.data?.data ?? null, writer);
10652
- if (!evidence.ok)
10653
- return evidence;
10654
- if (lock.data && (evidence.data.pid !== lock.data.data.pid || evidence.data.processStart !== lock.data.data.processStart))
10655
- return failure2("recovery_required", "process evidence does not match metadata lock");
10656
- if (writer && (evidence.data.state !== "accounted_for" || evidence.data.ownerDigest !== sha256(canonicalJson(writer))))
10657
- return failure2("recovery_required", "workspace writer is not accounted for");
10658
- const recoveryGate = { reclaimed: false };
10659
- const result = this.withLock((handle) => {
10660
- if (lock.data && !recoveryGate.reclaimed)
10661
- return failure2("recovery_required", "metadata lock changed during recovery", {
10662
- path: this.lockPath
10663
- });
10664
- if (!handle.verifyStillHeld())
10665
- return failure2("recovery_required", "metadata lock was compromised", {
10666
- path: this.lockPath
10667
- });
10668
- const reacquiredBytes = this.snapshotBytes(file);
10669
- if (!reacquiredBytes)
10670
- return failure2("recovery_required", "snapshot disappeared during recovery");
10671
- if (digestBytes(reacquiredBytes) !== input.expectedBytes)
10672
- return failure2("revision_conflict", "snapshot changed during recovery");
10673
- if (target === "task") {
10674
- const currentWorkspace = this.readWorkspace();
10675
- if (!currentWorkspace.ok || !currentWorkspace.data)
10676
- return failure2("recovery_required", "workspace changed during recovery");
10677
- if (currentWorkspace.data.revision !== input.expectedWorkspaceRevision)
10678
- return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
10679
- }
10680
- if (target === "workspace") {
10681
- const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
10682
- if (!parsed.ok)
10683
- return parsed;
10684
- const value = { ...parsed.data, revision: newRevision(), writer: null };
10685
- const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
10686
- return replaced.ok ? success(value.revision, value.revision, value) : replaced;
10687
- }
10688
- const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
10689
- if (!parsed.ok)
10690
- return parsed;
10691
- const value = {
10692
- ...parsed.data,
10693
- revision: newRevision(),
10694
- updatedAt: now(),
10695
- status: parsed.data.status === "active" ? "paused" : parsed.data.status
10696
- };
10697
- const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
10698
- return replaced.ok ? success(value.revision, null, value) : replaced;
10699
- }, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
10700
- return result;
10701
- } catch (error) {
10702
- return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
10703
- }
10704
- }
10705
- processEvidence(input, lock, writer) {
10706
- const lockCopy = lock ? this.deepFreeze(structuredClone(lock)) : null;
10707
- const writerCopy = writer ? this.deepFreeze(structuredClone(writer)) : null;
10708
- const lockDigest = lockCopy ? sha256(canonicalJson(lockCopy)) : null;
10709
- const writerDigest = writerCopy ? sha256(canonicalJson(writerCopy)) : null;
10710
- try {
10711
- const result = input.processEvidence(lockCopy, writerCopy);
10712
- if (lockCopy && sha256(canonicalJson(lockCopy)) !== lockDigest)
10713
- return failure2("recovery_required", "process evidence mutated lock identity");
10714
- if (writerCopy && sha256(canonicalJson(writerCopy)) !== writerDigest)
10715
- return failure2("recovery_required", "process evidence mutated writer identity");
10716
- if (!result.ok)
10717
- return result;
10718
- const parsed = processEvidenceSchema.safeParse(result.data);
10719
- return parsed.success ? success(null, null, parsed.data) : failure2("recovery_required", "process evidence is invalid");
10720
- } catch (error) {
10721
- return failure2("recovery_required", `process evidence failed: ${String(error)}`);
10722
- }
10723
- }
10724
- deepFreeze(value) {
10725
- if (value && typeof value === "object") {
10726
- for (const child of Object.values(value))
10727
- this.deepFreeze(child);
10728
- Object.freeze(value);
10729
- }
10730
- return value;
10731
- }
10732
- readLockSnapshot() {
10733
- try {
10734
- const stat = fs6.lstatSync(this.lockPath);
10735
- if (!stat.isFile() || stat.isSymbolicLink())
10736
- return failure2("recovery_required", "metadata lock is not a regular file", {
10737
- path: this.lockPath
10738
- });
10739
- const raw = fs6.readFileSync(this.lockPath, "utf8");
10740
- return success(null, null, { raw, data: parseMetadataLock(raw) });
10741
- } catch (error) {
10742
- if (error?.code === "ENOENT")
10743
- return success(null, null, null);
10744
- return failure2("recovery_required", `metadata lock is invalid: ${String(error)}`, {
10745
- path: this.lockPath
10746
- });
10747
- }
10748
- }
10749
- metadataLockOptions() {
10750
- const inProcess = heldInProcess.has(this.root);
10751
- return {
10752
- lockPath: this.lockPath,
10753
- staleMs: Number.MAX_SAFE_INTEGER,
10754
- timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
10755
- retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
10756
- staleRecovery: "remove-if-unchanged",
10757
- shouldReclaim: ({ payload, nowMs }) => {
10758
- let ageMs = null;
10759
- try {
10760
- ageMs = nowMs - fs6.lstatSync(this.lockPath).mtimeMs;
10761
- } catch {}
10762
- return classifyLockOwner(payload, ageMs).state === "stale";
10763
- },
10764
- shouldRemoveStaleLock: () => true,
10765
- parsePayload: parseMetadataLockOrNull,
10766
- payload: () => ({
10767
- pid: process.pid,
10768
- processStart: processStartOf(process.pid),
10769
- host: localLockHost(),
10770
- nonce: randomUUID2()
10771
- })
10772
- };
10773
- }
10774
- acquireMetadataLock(options) {
10775
- clearAbandonedReclaimGuard(this.lockPath);
10776
- const deadline = Date.now() + (options.timeoutMs ?? 0);
10777
- while (true) {
10778
- try {
10779
- return acquireFileLockSync(this.workspacePath, {
10780
- ...options,
10781
- timeoutMs: Math.max(0, deadline - Date.now())
10782
- });
10783
- } catch (error) {
10784
- const code = error?.code;
10785
- if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
10786
- throw error;
10787
- if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
10788
- throw error;
10789
- if (code !== "file_lock_stale")
10790
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
10791
- }
10792
- }
10793
- }
10794
- externalActionLockOptions() {
10795
- const options = this.metadataLockOptions();
10796
- return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
11233
+ }));
11234
+ this.compactPending();
11235
+ return result;
10797
11236
  }
10798
11237
  async withExternalActionLock(operation, reentrant = false) {
10799
11238
  const activeRoots = externalActionLockRoots.getStore();
10800
11239
  if (activeRoots && [...activeRoots].some((root) => root === this.root || sameDirectoryIdentity(root, this.root)))
10801
11240
  return operation();
11241
+ const ready = this.ready(true);
11242
+ if (!ready.ok)
11243
+ return ready;
11244
+ const lockPath = this.paths().checkoutLock;
10802
11245
  try {
10803
- this.initializeMutationStorage();
11246
+ this.initializeStorage();
10804
11247
  } catch (error) {
10805
11248
  return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
10806
- path: this.workitDir
11249
+ path: this.paths().dir
10807
11250
  });
10808
11251
  }
10809
11252
  let handle;
10810
11253
  try {
10811
- handle = this.acquireMetadataLock(this.externalActionLockOptions());
11254
+ const options = this.lockOptions(lockPath);
11255
+ handle = this.acquire(lockPath, {
11256
+ ...options,
11257
+ payload: () => ({ ...options.payload(), externalAction: true })
11258
+ });
10812
11259
  } catch (error) {
10813
- return this.lockFailure(error);
11260
+ return this.lockFailure(lockPath, error);
10814
11261
  }
10815
- holdRoot(this.root);
11262
+ hold(lockPath);
10816
11263
  let result;
10817
11264
  try {
10818
11265
  if (!handle.verifyStillHeld())
10819
- result = failure2("recovery_required", "metadata lock was compromised", {
10820
- path: this.lockPath
10821
- });
11266
+ result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
10822
11267
  else {
10823
11268
  try {
10824
11269
  const run = () => operation();
@@ -10832,529 +11277,1147 @@ class TaskStore {
10832
11277
  if (!handle.verifyStillHeld())
10833
11278
  result = failure2("external_outcome_unknown", "metadata lock was compromised", {
10834
11279
  outcome: "unknown",
10835
- path: this.lockPath
11280
+ path: lockPath
10836
11281
  });
10837
11282
  } catch {
10838
11283
  result = failure2("external_outcome_unknown", "metadata lock could not be verified", {
10839
11284
  outcome: "unknown",
10840
- path: this.lockPath
11285
+ path: lockPath
10841
11286
  });
10842
11287
  }
10843
11288
  }
10844
11289
  } catch {
10845
11290
  result = failure2("external_outcome_unknown", "external action lock operation failed", {
10846
11291
  outcome: "unknown",
10847
- path: this.lockPath
11292
+ path: lockPath
10848
11293
  });
10849
11294
  }
10850
- dropRoot(this.root);
11295
+ drop(lockPath);
10851
11296
  try {
10852
11297
  retryTransient(() => handle.release());
10853
11298
  } catch (error) {
10854
- const released = this.lockFailure(error);
11299
+ const released = this.lockFailure(lockPath, error);
10855
11300
  const releaseError = released.ok ? "metadata lock release failed" : released.error;
10856
11301
  return result.ok ? failure2("external_outcome_unknown", "external action metadata lock release is uncertain", {
10857
11302
  outcome: "unknown",
10858
- path: this.lockPath
11303
+ path: lockPath
10859
11304
  }) : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
10860
11305
  }
10861
11306
  return result;
10862
11307
  }
10863
- recoveryLockOptions(observed, evidence, gate) {
10864
- const options = this.metadataLockOptions();
10865
- options.timeoutMs = 0;
10866
- options.retry = { retries: 0 };
10867
- options.parsePayload = parseMetadataLock;
10868
- options.staleRecovery = "remove-if-unchanged";
10869
- options.shouldReclaim = ({ payload }) => Boolean(observed && sameMetadataLock(payload, observed.data) && (evidence.state === "stopped" || evidence.state === "accounted_for"));
10870
- options.shouldRemoveStaleLock = ({ raw, payload }) => {
10871
- if (!observed || raw !== observed.raw || !sameMetadataLock(payload, observed.data))
10872
- return false;
10873
- gate.reclaimed = true;
10874
- return true;
11308
+ collectGarbage(options = {}) {
11309
+ const dryRun = options.dryRun === true;
11310
+ const compactAbove = options.compactAbove ?? COMPACT_ABOVE;
11311
+ const report = {
11312
+ dryRun,
11313
+ compacted: { tasks: [], eventsFolded: 0, bytesBefore: 0, bytesAfter: 0 },
11314
+ blobs: { removed: 0, removedBytes: 0, kept: 0 },
11315
+ temporary: { removed: 0 },
11316
+ legacyRecovery: null,
11317
+ retried: [],
11318
+ failed: []
11319
+ };
11320
+ const ready = this.ready(!dryRun);
11321
+ if (!ready.ok)
11322
+ return ready;
11323
+ const dirs = this.paths();
11324
+ const recovery = path9.join(this.root, ".workit", "recovery");
11325
+ if (fs8.existsSync(recovery)) {
11326
+ let files = 0;
11327
+ let bytes = 0;
11328
+ try {
11329
+ for (const name of fs8.readdirSync(recovery)) {
11330
+ const stat = fs8.lstatSync(path9.join(recovery, name));
11331
+ if (!stat.isFile())
11332
+ continue;
11333
+ files += 1;
11334
+ bytes += stat.size;
11335
+ }
11336
+ } catch {}
11337
+ const remove = options.pruneRecovery === true && !dryRun;
11338
+ if (remove)
11339
+ fs8.rmSync(recovery, { recursive: true, force: true });
11340
+ report.legacyRecovery = { path: recovery, files, bytes, removed: remove };
11341
+ }
11342
+ if (!fs8.existsSync(dirs.tasks))
11343
+ return success(null, null, report);
11344
+ const names = fs8.readdirSync(dirs.tasks).filter((name) => validId(name));
11345
+ for (const name of names) {
11346
+ const file = this.eventsPath(name);
11347
+ let size = 0;
11348
+ try {
11349
+ size = fs8.statSync(file).size;
11350
+ } catch {
11351
+ continue;
11352
+ }
11353
+ const lines = this.countEvents(file);
11354
+ if (lines > compactAbove) {
11355
+ if (dryRun) {
11356
+ report.compacted.tasks.push(name);
11357
+ report.compacted.eventsFolded += lines - COMPACT_KEEP;
11358
+ report.compacted.bytesBefore += size;
11359
+ } else {
11360
+ const compacted = this.compact(name);
11361
+ if (!compacted.ok)
11362
+ (compacted.code === "busy" ? report.retried : report.failed).push(name);
11363
+ else {
11364
+ report.compacted.tasks.push(name);
11365
+ report.compacted.eventsFolded += compacted.data.folded;
11366
+ report.compacted.bytesBefore += size;
11367
+ report.compacted.bytesAfter += compacted.data.bytes;
11368
+ }
11369
+ }
11370
+ }
11371
+ }
11372
+ const nowMs = Date.now();
11373
+ const sweep = () => {
11374
+ const references = new Set;
11375
+ for (const name of names)
11376
+ try {
11377
+ for (const match of fs8.readFileSync(this.eventsPath(name), "utf8").matchAll(/"\$blob":"([0-9a-f]{64})"/g))
11378
+ references.add(match[1]);
11379
+ } catch {}
11380
+ if (fs8.existsSync(dirs.blobs))
11381
+ for (const name of fs8.readdirSync(dirs.blobs)) {
11382
+ const digest = name.replace(/\.json$/, "");
11383
+ const file = path9.join(dirs.blobs, name);
11384
+ let stat;
11385
+ try {
11386
+ stat = fs8.lstatSync(file);
11387
+ } catch {
11388
+ continue;
11389
+ }
11390
+ if (references.has(digest) || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS) {
11391
+ report.blobs.kept += 1;
11392
+ continue;
11393
+ }
11394
+ report.blobs.removed += 1;
11395
+ report.blobs.removedBytes += stat.size;
11396
+ if (!dryRun)
11397
+ fs8.rmSync(file, { force: true });
11398
+ }
11399
+ return success(null, null, null);
10875
11400
  };
10876
- return options;
11401
+ const swept = dryRun ? sweep() : this.withLock(path9.join(dirs.dir, "gc.lock"), sweep);
11402
+ if (!swept.ok)
11403
+ return swept;
11404
+ const temporaryDirs = [
11405
+ dirs.dir,
11406
+ dirs.tasks,
11407
+ dirs.blobs,
11408
+ dirs.checkout,
11409
+ ...names.map((name) => path9.join(dirs.tasks, name))
11410
+ ];
11411
+ for (const directory of temporaryDirs) {
11412
+ if (!fs8.existsSync(directory))
11413
+ continue;
11414
+ for (const name of fs8.readdirSync(directory)) {
11415
+ if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
11416
+ continue;
11417
+ const file = path9.join(directory, name);
11418
+ try {
11419
+ const stat = fs8.lstatSync(file);
11420
+ if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
11421
+ continue;
11422
+ } catch {
11423
+ continue;
11424
+ }
11425
+ report.temporary.removed += 1;
11426
+ if (!dryRun)
11427
+ fs8.rmSync(file, { force: true });
11428
+ }
11429
+ }
11430
+ return success(null, null, report);
10877
11431
  }
10878
- markUncertain(workspace) {
10879
- if (!workspace.writer || workspace.writer.state === "uncertain")
10880
- return;
10881
- const value = {
10882
- ...workspace,
10883
- writer: { ...workspace.writer, state: "uncertain" },
10884
- revision: newRevision(),
10885
- runtime: stampNew(workspace)
11432
+ paths() {
11433
+ const location = this.location();
11434
+ const dir = location.ok ? location.data.dir : path9.join(this.root, ".workit");
11435
+ const checkout = path9.join(dir, "checkouts", checkoutSlug(this.root));
11436
+ return {
11437
+ dir,
11438
+ shared: location.ok ? location.data.shared : false,
11439
+ marker: path9.join(dir, "store.json"),
11440
+ checkout,
11441
+ workspace: path9.join(checkout, "workspace.json"),
11442
+ checkoutLock: path9.join(checkout, "metadata.lock"),
11443
+ tasks: path9.join(dir, "tasks"),
11444
+ index: path9.join(dir, "task-index.json"),
11445
+ blobs: path9.join(dir, "blobs", "candidates"),
11446
+ legacy: path9.join(dir, "legacy", checkoutSlug(this.root), "v2"),
11447
+ v2: path9.join(this.root, ".workit")
10886
11448
  };
10887
- this.replaceSnapshot(this.workspacePath, value, workspace);
10888
11449
  }
10889
- withLock(operation, options = this.metadataLockOptions(), contention = "busy") {
11450
+ eventsPath(taskId) {
11451
+ return path9.join(this.paths().tasks, taskId, "events.jsonl");
11452
+ }
11453
+ snapshotPath(taskId) {
11454
+ return path9.join(this.paths().tasks, taskId, "snapshot.json");
11455
+ }
11456
+ taskLockPath(taskId) {
11457
+ return path9.join(this.paths().tasks, taskId, "lock");
11458
+ }
11459
+ ready(write = false) {
11460
+ const location = this.location();
11461
+ if (!location.ok)
11462
+ return location;
11463
+ if (this.migrationChecked)
11464
+ return success(null, null, null);
11465
+ const dirs = this.paths();
10890
11466
  try {
10891
- this.initializeMutationStorage();
10892
- } catch (error) {
10893
- return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
10894
- path: this.workitDir
10895
- });
11467
+ const marker = JSON.parse(fs8.readFileSync(dirs.marker, "utf8"));
11468
+ if (typeof marker.version === "number" && marker.version > STORE_FORMAT)
11469
+ return failure2("recovery_required", `the workit store at ${dirs.dir} has format ${marker.version}; upgrade Workit to use it`, { path: dirs.marker });
11470
+ } catch {}
11471
+ this.migrationChecked = true;
11472
+ const settled = this.settleMigrations(write);
11473
+ if (!settled.ok)
11474
+ this.migrationChecked = false;
11475
+ return settled;
11476
+ }
11477
+ settleMigrations(write) {
11478
+ const moved = this.adoptLocalStore();
11479
+ if (!moved.ok)
11480
+ return moved;
11481
+ const pending = this.pendingMigrations();
11482
+ if (pending.length > 0 && !write && !(this.migrateOnRead ?? migrationReporterInstalled()))
11483
+ 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" });
11484
+ for (const v2 of this.v2Dirs()) {
11485
+ const migrated = this.migrateV2(v2);
11486
+ if (!migrated.ok)
11487
+ return migrated;
10896
11488
  }
10897
- let handle;
10898
- let result = failure2("storage_error", "metadata lock operation did not produce a result");
11489
+ return success(null, null, null);
11490
+ }
11491
+ blobStore() {
11492
+ const dir = this.paths().blobs;
11493
+ return {
11494
+ read: (digest) => {
11495
+ try {
11496
+ return JSON.parse(fs8.readFileSync(path9.join(dir, `${digest}.json`), "utf8"));
11497
+ } catch (error) {
11498
+ throw new LogDamage(`stored candidate ${digest} is missing or unreadable: ${String(error)}`);
11499
+ }
11500
+ },
11501
+ write: (digest, value) => {
11502
+ const file = path9.join(dir, `${digest}.json`);
11503
+ if (fs8.existsSync(file)) {
11504
+ const stamp = new Date;
11505
+ try {
11506
+ fs8.utimesSync(file, stamp, stamp);
11507
+ return;
11508
+ } catch {}
11509
+ }
11510
+ fs8.mkdirSync(dir, { recursive: true });
11511
+ replaceFile(file, JSON.stringify(value), true);
11512
+ }
11513
+ };
11514
+ }
11515
+ load(taskId) {
10899
11516
  try {
10900
- handle = this.acquireMetadataLock(options);
11517
+ const loaded = this.loadOrThrow(taskId);
11518
+ if (!loaded)
11519
+ return success(null, null, null);
11520
+ const parsed = this.parseTask(loaded.state.record, taskId);
11521
+ if (!parsed.ok)
11522
+ return parsed;
11523
+ return success(null, null, {
11524
+ record: parsed.data,
11525
+ key: loaded.state.key,
11526
+ legacy: loaded.state.legacy,
11527
+ seq: loaded.seq,
11528
+ lastId: loaded.lastId,
11529
+ lastStart: loaded.lastStart,
11530
+ end: loaded.end,
11531
+ size: loaded.size,
11532
+ snapshotSeq: loaded.snapshotSeq
11533
+ });
10901
11534
  } catch (error) {
10902
- result = this.lockFailure(error, contention);
11535
+ if (error instanceof LogDamage)
11536
+ return failure2("recovery_required", error.upgrade ? error.message : `task ${taskId} log is damaged: ${error.message}`, { taskId, path: this.eventsPath(taskId) });
11537
+ return failure2("recovery_required", `task ${taskId} cannot be read: ${String(error)}`, {
11538
+ taskId
11539
+ });
10903
11540
  }
10904
- if (handle)
10905
- holdRoot(this.root);
10906
- if (handle) {
11541
+ }
11542
+ loadOrThrow(taskId) {
11543
+ const blobs = this.blobStore();
11544
+ const file = this.eventsPath(taskId);
11545
+ const snapshot = this.readSnapshot(taskId);
11546
+ if (snapshot) {
10907
11547
  try {
10908
- if (!handle.verifyStillHeld())
10909
- result = failure2("recovery_required", "metadata lock was compromised", {
10910
- path: this.lockPath
10911
- });
10912
- else {
10913
- try {
10914
- result = operation(handle);
10915
- } catch (error) {
10916
- result = failure2("storage_error", `mutation failed: ${String(error)}`);
10917
- }
11548
+ const tail = readLog(file, snapshot.lastStart, snapshot.seq);
11549
+ const first = tail?.events[0];
11550
+ if (tail && first && first.id === snapshot.lastId) {
11551
+ let state = {
11552
+ record: snapshot.record,
11553
+ key: snapshot.key,
11554
+ legacy: snapshot.legacy
11555
+ };
11556
+ for (const event of tail.events.slice(1))
11557
+ state = reduce(state, event, blobs);
11558
+ return this.loadedFrom(state, tail, snapshot.seq);
10918
11559
  }
10919
11560
  } catch (error) {
10920
- result = this.lockFailure(error);
11561
+ if (error instanceof LogDamage && error.upgrade)
11562
+ throw error;
10921
11563
  }
10922
11564
  }
10923
- if (handle) {
10924
- dropRoot(this.root);
10925
- try {
10926
- retryTransient(() => handle.release());
10927
- } catch (error) {
10928
- const releaseFailure = this.lockFailure(error);
10929
- const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
10930
- result = result.ok ? releaseFailure : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
10931
- }
11565
+ const log = readLog(file, 0, null);
11566
+ if (!log || log.events.length === 0) {
11567
+ if (log && log.size > 0)
11568
+ throw new LogDamage("the log holds no complete event");
11569
+ return null;
10932
11570
  }
10933
- return result;
11571
+ if (!OPENING_TYPES.has(log.events[0].type))
11572
+ throw new LogDamage(`the log starts with ${log.events[0].type}, not an opening event`);
11573
+ let state = null;
11574
+ for (const event of log.events)
11575
+ state = reduce(state, event, blobs);
11576
+ return this.loadedFrom(state, log, 0);
11577
+ }
11578
+ loadedFrom(state, log, snapshotSeq) {
11579
+ const last = log.events.at(-1);
11580
+ return {
11581
+ state,
11582
+ seq: last.seq,
11583
+ lastId: last.id,
11584
+ lastStart: log.lastStart,
11585
+ end: log.end,
11586
+ size: log.size,
11587
+ snapshotSeq
11588
+ };
10934
11589
  }
10935
- lockDirWritable() {
10936
- const probe = `${this.lockPath}.${process.pid}.${randomUUID2()}.probe`;
11590
+ readSnapshot(taskId) {
10937
11591
  try {
10938
- fs6.closeSync(fs6.openSync(probe, "wx", 384));
11592
+ const value = JSON.parse(fs8.readFileSync(this.snapshotPath(taskId), "utf8"));
11593
+ 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))
11594
+ return null;
11595
+ return value;
10939
11596
  } catch {
10940
- return false;
11597
+ return null;
10941
11598
  }
11599
+ }
11600
+ writeSnapshot(taskId, loaded) {
11601
+ const snapshot = {
11602
+ v: 1,
11603
+ task: taskId,
11604
+ seq: loaded.seq,
11605
+ lastId: loaded.lastId,
11606
+ lastStart: loaded.lastStart,
11607
+ end: loaded.end,
11608
+ key: loaded.key,
11609
+ legacy: loaded.legacy,
11610
+ record: loaded.record
11611
+ };
10942
11612
  try {
10943
- fs6.rmSync(probe, { force: true });
11613
+ replaceFile(this.snapshotPath(taskId), JSON.stringify(snapshot), false);
10944
11614
  } catch {}
10945
- return true;
10946
11615
  }
10947
- lockHolderPresent() {
11616
+ loadBound(taskId) {
11617
+ const loaded = this.load(taskId);
11618
+ if (!loaded.ok)
11619
+ return loaded;
11620
+ if (!loaded.data)
11621
+ return failure2("not_found", "task not found", { taskId });
11622
+ const workspace = this.readWorkspace();
11623
+ if (!workspace.ok)
11624
+ return workspace;
11625
+ const bound = this.bindingError(loaded.data.record, workspace.data);
11626
+ return bound ?? success(null, null, loaded.data);
11627
+ }
11628
+ bindingError(task, workspace) {
11629
+ if (workspace && task.workspaceId === workspace.id)
11630
+ return null;
11631
+ 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 });
11632
+ }
11633
+ nextTask(current, changed, context) {
11634
+ const valid = taskRecordSchema.safeParse({
11635
+ ...changed,
11636
+ id: current.id,
11637
+ workspaceId: current.workspaceId,
11638
+ createdAt: current.createdAt,
11639
+ revision: context.revision,
11640
+ updatedAt: context.now,
11641
+ runtime: stampNew(current)
11642
+ });
11643
+ return valid.success ? valid.data : null;
11644
+ }
11645
+ event(taskId, seq, type, data) {
11646
+ return {
11647
+ v: EVENT_VERSION,
11648
+ seq,
11649
+ at: new Date().toISOString(),
11650
+ id: newId(),
11651
+ task: taskId,
11652
+ actor: this.actor,
11653
+ type,
11654
+ data
11655
+ };
11656
+ }
11657
+ appendPatch(loaded, next, extra = {}) {
11658
+ const taskId = next.id;
10948
11659
  try {
10949
- fs6.lstatSync(this.lockPath);
10950
- return true;
11660
+ const blobs = this.blobStore();
11661
+ const before = loaded.record;
11662
+ const after = next;
11663
+ const ops = encodeOps(diff(before, after), blobs);
11664
+ const { type, ...data } = extra;
11665
+ if (data.key)
11666
+ data.key = { ...data.key, boundAt: data.key.boundAt ?? new Date().toISOString() };
11667
+ const event = this.event(taskId, loaded.seq + 1, type ?? ("key" in extra ? "task.bound" : eventType(before, after)), { ops, ...data });
11668
+ const start = appendEvent(this.eventsPath(taskId), event, loaded.end);
11669
+ const end = start + Buffer.byteLength(`${JSON.stringify(event)}
11670
+ `);
11671
+ const key = "key" in data ? data.key ?? null : loaded.key;
11672
+ const state = {
11673
+ record: next,
11674
+ key,
11675
+ legacy: extra.legacy ?? loaded.legacy,
11676
+ seq: event.seq,
11677
+ lastId: event.id,
11678
+ lastStart: start,
11679
+ end
11680
+ };
11681
+ if (event.seq - loaded.snapshotSeq >= SNAPSHOT_EVERY || next.status !== loaded.record.status || loaded.snapshotSeq === 0)
11682
+ this.writeSnapshot(taskId, state);
11683
+ if (end > AUTO_COMPACT_BYTES)
11684
+ this.compactDue.add(taskId);
11685
+ this.indexTask(next, key, (extra.legacy ?? loaded.legacy) !== null);
11686
+ return success(null, null, null);
10951
11687
  } catch (error) {
10952
- return error?.code !== "ENOENT";
11688
+ return failure2("storage_error", `task append failed: ${String(error)}`, {
11689
+ path: this.eventsPath(taskId)
11690
+ });
10953
11691
  }
10954
11692
  }
10955
- lockFailure(error, contention = "busy") {
10956
- const value = error;
10957
- const code = typeof value?.code === "string" ? value.code : "";
10958
- if (isTransientWindowsError(error) && !this.lockHolderPresent() && !this.lockDirWritable())
10959
- return failure2("storage_error", `cannot create the workspace metadata lock (${code}); no other Workit call holds it`, {
10960
- path: this.lockPath,
10961
- guidance: `Check permissions and the read-only attribute on ${path7.dirname(this.lockPath)}.`
11693
+ openTask(record, type, key, legacy = null) {
11694
+ const dir = path9.join(this.paths().tasks, record.id);
11695
+ fs8.mkdirSync(dir, { recursive: true });
11696
+ const blobs = this.blobStore();
11697
+ const bound = key ? { ...key, boundAt: key.boundAt ?? new Date().toISOString() } : null;
11698
+ const event = this.event(record.id, 1, type, {
11699
+ record: encodeRecord(record, blobs),
11700
+ key: bound,
11701
+ ...legacy ? { legacy } : {}
11702
+ });
11703
+ const file = this.eventsPath(record.id);
11704
+ const existing = readLog(file, 0, null);
11705
+ if (existing && existing.events.length > 0)
11706
+ throw new Error(`task ${record.id} already has a log`);
11707
+ appendEvent(file, event, 0);
11708
+ this.indexTask(record, bound, legacy !== null);
11709
+ }
11710
+ compactPending() {
11711
+ for (const taskId of this.compactDue) {
11712
+ this.compactDue.delete(taskId);
11713
+ this.compact(taskId);
11714
+ }
11715
+ }
11716
+ compact(taskId) {
11717
+ try {
11718
+ const file = this.eventsPath(taskId);
11719
+ const log = readLog(file, 0, null);
11720
+ if (!log || log.events.length <= COMPACT_KEEP + 1)
11721
+ return success(null, null, { folded: 0, bytes: log?.size ?? 0 });
11722
+ const blobs = this.blobStore();
11723
+ const cut = log.events.length - COMPACT_KEEP;
11724
+ let state = null;
11725
+ for (const event of log.events.slice(0, cut))
11726
+ state = reduce(state, event, blobs);
11727
+ const folded = log.events[cut - 1];
11728
+ const checkpoint = {
11729
+ ...this.event(taskId, folded.seq, "task.checkpoint", {
11730
+ record: encodeRecord(state.record, blobs),
11731
+ key: state.key,
11732
+ ...state.legacy ? { legacy: state.legacy } : {},
11733
+ folded: { from: log.events[0].seq, to: folded.seq }
11734
+ })
11735
+ };
11736
+ const prefix = `${[checkpoint, ...log.events.slice(cut)].map((event) => JSON.stringify(event)).join(`
11737
+ `)}
11738
+ `;
11739
+ const last = log.events.at(-1);
11740
+ return this.withLock(this.taskLockPath(taskId), () => {
11741
+ let latest = null;
11742
+ try {
11743
+ latest = readLog(file, log.lastStart, last.seq);
11744
+ } catch {}
11745
+ if (!latest || latest.events[0]?.id !== last.id)
11746
+ return failure2("busy", "the task log changed during compaction; retry", { taskId });
11747
+ const appended = fs8.readFileSync(file).subarray(log.end, latest.end);
11748
+ const bytes = Buffer.concat([Buffer.from(prefix, "utf8"), appended]);
11749
+ replaceFile(file, bytes.toString("utf8"), true);
11750
+ try {
11751
+ fs8.rmSync(this.snapshotPath(taskId), { force: true });
11752
+ } catch {}
11753
+ const reloaded = this.load(taskId);
11754
+ if (reloaded.ok && reloaded.data)
11755
+ this.writeSnapshot(taskId, reloaded.data);
11756
+ return success(null, null, { folded: cut, bytes: bytes.length });
10962
11757
  });
10963
- if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
10964
- let lock = null;
10965
- try {
10966
- lock = parseMetadataLockOrNull(fs6.readFileSync(this.lockPath, "utf8"));
10967
- } catch {}
10968
- if (lock?.externalAction)
10969
- return failure2("writer_conflict", "workspace is reserved by a managed external action", {
10970
- outcome: "not_started",
10971
- path: this.lockPath
10972
- });
10973
- if (contention === "busy")
10974
- return failure2("busy", `workspace metadata lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
10975
- outcome: "not_started",
10976
- path: this.lockPath,
10977
- guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
10978
- });
11758
+ } catch (error) {
11759
+ return failure2("storage_error", `compaction failed: ${String(error)}`, { taskId });
10979
11760
  }
10980
- 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));
10981
- return failure2(recovery ? "recovery_required" : "storage_error", `metadata lock operation failed: ${String(error)}`, { path: this.lockPath });
10982
11761
  }
10983
- initializeMutationStorage() {
10984
- retryTransient(() => fs6.mkdirSync(this.tasksDir, { recursive: true }));
10985
- retryTransient(() => fs6.mkdirSync(this.recoveryDir, { recursive: true }));
10986
- let current = null;
11762
+ countEvents(file) {
10987
11763
  try {
10988
- current = retryTransient(() => fs6.readFileSync(this.gitignorePath, "utf8"));
10989
- } catch {}
10990
- if (current !== `*
10991
- `)
10992
- retryTransient(() => fs6.writeFileSync(this.gitignorePath, `*
10993
- `));
11764
+ const bytes = fs8.readFileSync(file);
11765
+ let count = 0;
11766
+ for (let index = bytes.indexOf(10);index >= 0; index = bytes.indexOf(10, index + 1))
11767
+ count += 1;
11768
+ return count;
11769
+ } catch {
11770
+ return 0;
11771
+ }
11772
+ }
11773
+ openTasksForKey(key) {
11774
+ const all = this.listStoreIndex();
11775
+ if (!all.ok)
11776
+ return all;
11777
+ const location = this.location();
11778
+ const renames = location.ok ? branchRenames(location.data) : [];
11779
+ 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));
11780
+ }
11781
+ openTaskForKey(key) {
11782
+ const bound = this.openTasksForKey(key);
11783
+ return bound.ok ? success(null, null, bound.data[0] ?? null) : bound;
11784
+ }
11785
+ withKeyLock(key, operation) {
11786
+ const digest = createHash6("sha256").update(key).digest("hex").slice(0, 32);
11787
+ return this.withLock(path9.join(this.paths().dir, "keys", `${digest}.lock`), operation);
11788
+ }
11789
+ bindLocked(taskId, key, timestamp) {
11790
+ const current = this.readWorkspace();
11791
+ if (!current.ok)
11792
+ return current;
11793
+ let workspace = current.data;
11794
+ if (!workspace) {
11795
+ workspace = this.nextWorkspace(null);
11796
+ const written = this.writeWorkspace(workspace);
11797
+ if (!written.ok)
11798
+ return written;
11799
+ }
11800
+ const target = workspace;
11801
+ return this.withLock(this.taskLockPath(taskId), () => {
11802
+ const loaded = this.load(taskId);
11803
+ if (!loaded.ok)
11804
+ return loaded;
11805
+ if (!loaded.data)
11806
+ return failure2("not_found", "task not found", { taskId });
11807
+ if (loaded.data.record.status === "closed")
11808
+ return failure2("invalid_transition", "a closed task cannot be adopted", { taskId });
11809
+ if (loaded.data.record.workspaceId === target.id && loaded.data.key?.key === key.key)
11810
+ return success(loaded.data.record.revision, null, loaded.data.record);
11811
+ const next = taskRecordSchema.safeParse({
11812
+ ...loaded.data.record,
11813
+ workspaceId: target.id,
11814
+ revision: newRevision(),
11815
+ updatedAt: timestamp ?? now(),
11816
+ runtime: stampNew(loaded.data.record)
11817
+ });
11818
+ if (!next.success)
11819
+ return failure2("invalid_input", "rebound task is invalid");
11820
+ const written = this.appendPatch(loaded.data, next.data, { key });
11821
+ return written.ok ? success(next.data.revision, null, next.data) : written;
11822
+ });
11823
+ }
11824
+ unbindLocked(taskId) {
11825
+ return this.withLock(this.taskLockPath(taskId), () => {
11826
+ const loaded = this.load(taskId);
11827
+ if (!loaded.ok)
11828
+ return loaded;
11829
+ if (!loaded.data || loaded.data.key === null)
11830
+ return success(null, null, null);
11831
+ return this.appendPatch(loaded.data, loaded.data.record, { key: null });
11832
+ });
11833
+ }
11834
+ createLocked(value, bindCurrent, key, checkRevision = true) {
11835
+ const current = this.readWorkspace();
11836
+ if (!current.ok)
11837
+ return current;
11838
+ if (checkRevision && (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null))
11839
+ return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
11840
+ expectedWorkspaceRevision: value.expectedWorkspaceRevision,
11841
+ actualWorkspaceRevision: current.data?.revision ?? null
11842
+ });
11843
+ if (!value.provenance)
11844
+ return failure2("invalid_input", "provenance is required");
11845
+ if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
11846
+ return failure2("invalid_input", "task intent or provenance is invalid");
11847
+ if (bindCurrent) {
11848
+ const currentKey = this.currentKey();
11849
+ if (currentKey.ok)
11850
+ return this.withKeyLock(currentKey.data.key, () => {
11851
+ const holder = this.openTaskForKey(currentKey.data.key);
11852
+ if (!holder.ok)
11853
+ return holder;
11854
+ if (holder.data) {
11855
+ const released = this.unbindLocked(holder.data.id);
11856
+ if (!released.ok)
11857
+ return released;
11858
+ }
11859
+ return this.newTask(value, current.data, currentKey.data);
11860
+ });
11861
+ }
11862
+ return this.newTask(value, current.data, key);
11863
+ }
11864
+ newTask(value, previous, binding) {
11865
+ const current = { data: previous };
11866
+ const workspace = this.nextWorkspace(current.data);
11867
+ const timestamp = value.now ?? now();
11868
+ const task = {
11869
+ schemaVersion: SCHEMA_VERSION,
11870
+ id: newId(),
11871
+ workspaceId: workspace.id,
11872
+ revision: newRevision(),
11873
+ createdAt: timestamp,
11874
+ updatedAt: timestamp,
11875
+ origin: null,
11876
+ intent: {
11877
+ id: newId(),
11878
+ recordedAt: timestamp,
11879
+ provenance: value.provenance,
11880
+ data: value.intent
11881
+ },
11882
+ constraints: [],
11883
+ status: "active",
11884
+ closure: null,
11885
+ progress: { summary: "", nextAction: null, blockers: [] },
11886
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
11887
+ assessments: [],
11888
+ policy: null,
11889
+ policyChanges: [],
11890
+ candidates: [],
11891
+ evidence: [],
11892
+ decisions: [],
11893
+ findings: [],
11894
+ workers: []
11895
+ };
11896
+ return this.writeNewTask(current.data, workspace, task, "task.opened", binding, {
11897
+ operation: "create"
11898
+ });
10994
11899
  }
10995
- replaceSnapshot(file, value, previous) {
10996
- let temporary;
11900
+ nextWorkspace(current, id) {
11901
+ return current ? { ...current, revision: newRevision(), root: this.root, runtime: stampNew(current) } : {
11902
+ schemaVersion: SCHEMA_VERSION,
11903
+ id: id ?? newId(),
11904
+ revision: newRevision(),
11905
+ root: this.root,
11906
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
11907
+ writer: null
11908
+ };
11909
+ }
11910
+ writeNewTask(previous, workspace, task, type, key, details) {
11911
+ const written = this.writeWorkspace(workspace);
11912
+ if (!written.ok)
11913
+ return written;
10997
11914
  try {
10998
- if (Buffer.isBuffer(previous))
10999
- this.saveRecovery(file, previous);
11000
- else if (previous !== null && typeof previous === "object")
11001
- this.saveRecovery(file, jsonBytes(previous));
11002
- else if (typeof previous === "string")
11003
- this.saveRecovery(file, previous);
11004
- temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
11005
- const fd = fs6.openSync(temporary, "wx", 384);
11006
- try {
11007
- fs6.writeSync(fd, jsonBytes(value));
11008
- fs6.fsyncSync(fd);
11009
- } finally {
11010
- fs6.closeSync(fd);
11915
+ this.openTask(task, type, key);
11916
+ } catch (error) {
11917
+ if (previous) {
11918
+ const restored = this.writeWorkspace(previous);
11919
+ if (!restored.ok)
11920
+ return failure2("external_outcome_unknown", `task ${details.operation} failed and workspace restoration is uncertain`, { ...details, outcome: "unknown" });
11011
11921
  }
11012
- retryTransient(() => fs6.renameSync(temporary, file));
11013
- temporary = undefined;
11014
- this.fsyncDirectory(path7.dirname(file));
11015
- if (path7.dirname(file) === this.tasksDir)
11016
- this.indexTaskWrite(file, value);
11017
- return success(null, null, value);
11922
+ return failure2("external_outcome_unknown", `workspace written but the task log is uncertain: ${String(error)}`, { ...details, outcome: "unknown" });
11923
+ }
11924
+ return success(task.revision, workspace.revision, task);
11925
+ }
11926
+ writeWorkspace(record) {
11927
+ const file = this.paths().workspace;
11928
+ try {
11929
+ fs8.mkdirSync(path9.dirname(file), { recursive: true });
11930
+ replaceFile(file, `${canonicalJson(record)}
11931
+ `, true);
11932
+ return success(null, null, null);
11018
11933
  } catch (error) {
11019
- return failure2("storage_error", `snapshot replacement failed: ${String(error)}`, {
11020
- path: file
11021
- });
11022
- } finally {
11023
- if (temporary)
11024
- try {
11025
- fs6.unlinkSync(temporary);
11026
- } catch {}
11934
+ return failure2("storage_error", `workspace write failed: ${String(error)}`, { path: file });
11027
11935
  }
11028
11936
  }
11029
- readIndex(workspaceId) {
11937
+ markUncertain(workspace) {
11938
+ if (!workspace.writer || workspace.writer.state === "uncertain")
11939
+ return;
11940
+ this.writeWorkspace({
11941
+ ...workspace,
11942
+ writer: { ...workspace.writer, state: "uncertain" },
11943
+ revision: newRevision(),
11944
+ runtime: stampNew(workspace)
11945
+ });
11946
+ }
11947
+ readIndex() {
11030
11948
  try {
11031
- const value = JSON.parse(fs6.readFileSync(this.indexPath, "utf8"));
11032
- if (!isObject2(value) || value.version !== INDEX_VERSION || value.workspaceId !== workspaceId || !isObject2(value.tasks))
11949
+ const value = JSON.parse(fs8.readFileSync(this.paths().index, "utf8"));
11950
+ if (!isObject4(value) || value.version !== INDEX_VERSION || !isObject4(value.tasks))
11033
11951
  return {};
11034
11952
  const tasks = {};
11035
11953
  for (const [id, entry] of Object.entries(value.tasks))
11036
- 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))
11954
+ 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))
11037
11955
  tasks[id] = entry;
11038
11956
  return tasks;
11039
11957
  } catch {
11040
11958
  return {};
11041
11959
  }
11042
11960
  }
11043
- writeIndex(workspaceId, entries) {
11044
- const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
11961
+ writeIndex(entries) {
11045
11962
  try {
11046
- fs6.writeFileSync(temporary, JSON.stringify({
11963
+ fs8.mkdirSync(this.paths().tasks, { recursive: true });
11964
+ replaceFile(this.paths().index, JSON.stringify({
11047
11965
  version: INDEX_VERSION,
11048
- workspaceId,
11049
11966
  tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
11050
- }), { mode: 384, flag: "wx" });
11051
- retryTransient(() => fs6.renameSync(temporary, this.indexPath));
11967
+ }), false);
11968
+ } catch {}
11969
+ }
11970
+ indexTask(task, key, legacy) {
11971
+ try {
11972
+ const signature = fileSignature(this.eventsPath(task.id));
11973
+ if (signature === null)
11974
+ return;
11975
+ const tasks = this.readIndex();
11976
+ tasks[task.id] = indexEntry(task, key, legacy, signature);
11977
+ this.writeIndex(Object.values(tasks));
11978
+ } catch {}
11979
+ }
11980
+ parseTask(record, taskId) {
11981
+ const parsed = this.parseValue(record, taskRecordSchema, "task");
11982
+ if (!parsed.ok)
11983
+ return { ...parsed, details: { ...parsed.details, taskId } };
11984
+ if (parsed.data.id !== taskId)
11985
+ return failure2("recovery_required", "task log and record ID differ", { taskId });
11986
+ return parsed;
11987
+ }
11988
+ parseRecord(bytes, schema, label) {
11989
+ let value;
11990
+ try {
11991
+ value = JSON.parse(bytes);
11052
11992
  } catch {
11993
+ return failure2("recovery_required", `${label} record JSON is corrupt`);
11994
+ }
11995
+ return this.parseValue(value, schema, label);
11996
+ }
11997
+ parseValue(value, schema, label) {
11998
+ if (isObject4(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
11999
+ return failure2("unsupported_version", `unsupported ${label} schema version`);
12000
+ const parsed = parseStoredRecord(schema, value);
12001
+ if (parsed.success)
12002
+ return success(null, null, parsed.data);
12003
+ const writerVersion = isObject4(value) && isObject4(value.runtime) ? value.runtime.updatedWith : null;
12004
+ if (parsed.critical.length > 0)
12005
+ return failure2("recovery_required", `${label} record requires fields this Workit cannot read (${parsed.critical.join(", ")}); upgrade Workit before mutating this checkout`);
12006
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
12007
+ return failure2("recovery_required", `${label} record was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
12008
+ return failure2("recovery_required", `${label} record does not satisfy its schema`);
12009
+ }
12010
+ conflict(expected, actual) {
12011
+ return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", { expectedRevision: expected, actualRevision: actual });
12012
+ }
12013
+ workspaceConflict(expected, actual) {
12014
+ return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
12015
+ }
12016
+ v2Dirs() {
12017
+ return [...new Set([this.root, this.origin])].map((dir) => path9.join(dir, ".workit"));
12018
+ }
12019
+ pendingMigrations() {
12020
+ return this.v2Dirs().filter((dir) => {
12021
+ const raw = this.readV2Workspace(path9.join(dir, "workspace.json"));
12022
+ return raw !== null && raw !== TOMBSTONE;
12023
+ });
12024
+ }
12025
+ migrateV2(v2) {
12026
+ const dirs = this.paths();
12027
+ const v2Workspace = path9.join(v2, "workspace.json");
12028
+ const v2Tasks = path9.join(v2, "tasks");
12029
+ const legacy = path9.join(dirs.dir, "legacy", checkoutSlug(path9.dirname(v2)), "v2");
12030
+ const raw = this.readV2Workspace(v2Workspace);
12031
+ if (raw === null || raw === TOMBSTONE)
12032
+ return success(null, null, null);
12033
+ return this.withLock(dirs.checkoutLock, () => this.withLock(path9.join(v2, "metadata.lock"), () => {
12034
+ const workspace = this.readV2Workspace(v2Workspace);
12035
+ if (workspace === null || workspace === TOMBSTONE)
12036
+ return success(null, null, null);
12037
+ const parsedWorkspace = this.parseRecord(workspace.bytes, workspaceRecordSchema, "2.x workspace");
12038
+ if (!parsedWorkspace.ok)
12039
+ return failure2("recovery_required", `cannot migrate the 2.x store at ${v2}: ${parsedWorkspace.error}; move it aside to start fresh`, { path: v2Workspace });
12040
+ let files = [];
11053
12041
  try {
11054
- fs6.unlinkSync(temporary);
12042
+ files = fs8.readdirSync(v2Tasks).filter((name) => name.endsWith(".json") && validId(name.slice(0, -5))).filter((name) => !this.isStub(path9.join(v2Tasks, name)));
11055
12043
  } catch {}
11056
- }
12044
+ const current = this.readWorkspace();
12045
+ if (!current.ok)
12046
+ return current;
12047
+ let workspaceMoved = false;
12048
+ let target = current.data;
12049
+ if (!target) {
12050
+ target = { ...parsedWorkspace.data, root: this.root };
12051
+ const written = this.writeWorkspace(target);
12052
+ if (!written.ok)
12053
+ return written;
12054
+ workspaceMoved = true;
12055
+ }
12056
+ let migrated = 0;
12057
+ let skipped = 0;
12058
+ const done = new Map;
12059
+ try {
12060
+ fs8.mkdirSync(path9.join(legacy, "tasks"), { recursive: true });
12061
+ for (const name of ["workspace.json", "index.json"])
12062
+ if (fs8.existsSync(path9.join(v2, name)))
12063
+ fs8.copyFileSync(path9.join(v2, name), path9.join(legacy, name));
12064
+ for (const name of files) {
12065
+ const id = name.slice(0, -5);
12066
+ const file = path9.join(v2Tasks, name);
12067
+ const bytes = fs8.readFileSync(file, "utf8");
12068
+ const digest = sha256(bytes);
12069
+ fs8.writeFileSync(path9.join(legacy, "tasks", name), bytes);
12070
+ const parsed = this.parseRecord(bytes, taskRecordSchema, "2.x task");
12071
+ if (!parsed.ok || parsed.data.id !== id)
12072
+ 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 });
12073
+ const record = { ...parsed.data, workspaceId: target.id };
12074
+ const origin = { path: path9.join(legacy, "tasks", name), digest };
12075
+ if (!fs8.existsSync(this.eventsPath(id))) {
12076
+ this.openTask(record, "migrated.from_v2", null, origin);
12077
+ migrated += 1;
12078
+ } else {
12079
+ const resynced = this.withLock(this.taskLockPath(id), () => {
12080
+ const loaded = this.load(id);
12081
+ if (!loaded.ok)
12082
+ return loaded;
12083
+ if (!loaded.data)
12084
+ return failure2("not_found", "task not found", { taskId: id });
12085
+ if (loaded.data.legacy?.digest === digest)
12086
+ return success(null, null, false);
12087
+ const appended = this.appendPatch(loaded.data, record, {
12088
+ type: "migrated.from_v2",
12089
+ legacy: origin
12090
+ });
12091
+ return appended.ok ? success(null, null, true) : appended;
12092
+ });
12093
+ if (!resynced.ok)
12094
+ return resynced;
12095
+ if (resynced.data)
12096
+ migrated += 1;
12097
+ else
12098
+ skipped += 1;
12099
+ }
12100
+ done.set(name, digest);
12101
+ }
12102
+ } catch (error) {
12103
+ return failure2("storage_error", `cannot migrate the 2.x store at ${v2}: ${String(error)}`, {
12104
+ path: v2
12105
+ });
12106
+ }
12107
+ try {
12108
+ let complete = true;
12109
+ for (const [name, digest] of done) {
12110
+ const file = path9.join(v2Tasks, name);
12111
+ if (sha256(fs8.readFileSync(file, "utf8")) !== digest) {
12112
+ complete = false;
12113
+ continue;
12114
+ }
12115
+ replaceFile(file, this.stubFor(name.slice(0, -5)), false);
12116
+ }
12117
+ if (!complete)
12118
+ return failure2("busy", `the 2.x store at ${v2} changed during migration; retry`, {
12119
+ path: v2
12120
+ });
12121
+ fs8.rmSync(path9.join(v2, "index.json"), { force: true });
12122
+ this.writeTombstone(v2, target.id);
12123
+ } catch (error) {
12124
+ return failure2("storage_error", `cannot finish the 2.x migration: ${String(error)}`, {
12125
+ path: v2
12126
+ });
12127
+ }
12128
+ reportMigration({
12129
+ from: v2,
12130
+ to: dirs.dir,
12131
+ backup: legacy,
12132
+ tasks: migrated,
12133
+ skipped,
12134
+ workspace: workspaceMoved
12135
+ });
12136
+ return success(null, null, null);
12137
+ }));
11057
12138
  }
11058
- indexTaskWrite(file, task) {
12139
+ adoptLocalStore() {
12140
+ const dirs = this.paths();
12141
+ const marker = path9.join(dirs.v2, "store.json");
12142
+ if (!dirs.shared || !fs8.existsSync(marker))
12143
+ return success(null, null, null);
12144
+ return this.withLock(dirs.checkoutLock, () => {
12145
+ if (!fs8.existsSync(marker))
12146
+ return success(null, null, null);
12147
+ let tasks = 0;
12148
+ try {
12149
+ const merge = (from, to) => {
12150
+ if (!fs8.existsSync(to)) {
12151
+ fs8.mkdirSync(path9.dirname(to), { recursive: true });
12152
+ try {
12153
+ fs8.renameSync(from, to);
12154
+ } catch {
12155
+ fs8.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
12156
+ fs8.rmSync(from, { recursive: true, force: true });
12157
+ }
12158
+ return;
12159
+ }
12160
+ if (fs8.lstatSync(from).isDirectory() && fs8.lstatSync(to).isDirectory())
12161
+ for (const name of fs8.readdirSync(from))
12162
+ merge(path9.join(from, name), path9.join(to, name));
12163
+ };
12164
+ for (const sub of ["tasks", "checkouts", "blobs", "legacy"]) {
12165
+ const from = path9.join(dirs.v2, sub);
12166
+ if (!fs8.existsSync(from))
12167
+ continue;
12168
+ if (sub === "tasks") {
12169
+ for (const name of fs8.readdirSync(from))
12170
+ if (validId(name) && !fs8.existsSync(path9.join(dirs.dir, sub, name)))
12171
+ tasks += 1;
12172
+ }
12173
+ merge(from, path9.join(dirs.dir, sub));
12174
+ }
12175
+ const ledger = path9.join(dirs.v2, "ledger");
12176
+ if (fs8.existsSync(ledger) && !fs8.existsSync(path9.join(dirs.dir, "ledger")))
12177
+ merge(ledger, path9.join(dirs.dir, "ledger"));
12178
+ fs8.rmSync(path9.join(dirs.v2, "task-index.json"), { force: true });
12179
+ fs8.renameSync(marker, path9.join(dirs.v2, "store.moved.json"));
12180
+ } catch (error) {
12181
+ return failure2("storage_error", `cannot move ${dirs.v2} into ${dirs.dir}: ${String(error)}`, {
12182
+ path: dirs.v2
12183
+ });
12184
+ }
12185
+ reportMigration({
12186
+ from: dirs.v2,
12187
+ to: dirs.dir,
12188
+ backup: dirs.v2,
12189
+ tasks,
12190
+ skipped: 0,
12191
+ workspace: false
12192
+ });
12193
+ return success(null, null, null);
12194
+ });
12195
+ }
12196
+ readV2Workspace(file) {
12197
+ let bytes;
11059
12198
  try {
11060
- const signature = fileSignature(file);
11061
- if (signature === null)
11062
- return;
11063
- const tasks = this.readIndex(task.workspaceId);
11064
- tasks[task.id] = indexEntry(task, signature);
11065
- this.writeIndex(task.workspaceId, Object.values(tasks));
12199
+ bytes = fs8.readFileSync(file, "utf8");
12200
+ } catch {
12201
+ return null;
12202
+ }
12203
+ try {
12204
+ const value = JSON.parse(bytes);
12205
+ if (isObject4(value) && isObject4(value.store) && value.store.format === "workit-store")
12206
+ return TOMBSTONE;
11066
12207
  } catch {}
12208
+ return { bytes };
12209
+ }
12210
+ writeTombstone(v2, workspaceId) {
12211
+ const dirs = this.paths();
12212
+ const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
12213
+ const marker = {
12214
+ schemaVersion: SCHEMA_VERSION,
12215
+ id: workspaceId ?? newId(),
12216
+ revision: newRevision(),
12217
+ root: path9.dirname(v2),
12218
+ runtime: { createdWith: version, updatedWith: version },
12219
+ writer: null,
12220
+ store: {
12221
+ format: "workit-store",
12222
+ version: STORE_FORMAT,
12223
+ path: dirs.dir,
12224
+ note: `workit ${STORE_MIN_RUNTIME}+ keeps task state in ${dirs.dir}; upgrade Workit`
12225
+ },
12226
+ critical: ["store"]
12227
+ };
12228
+ fs8.mkdirSync(v2, { recursive: true });
12229
+ const ignore = path9.join(v2, ".gitignore");
12230
+ if (!fs8.existsSync(ignore))
12231
+ fs8.writeFileSync(ignore, `*
12232
+ `);
12233
+ replaceFile(path9.join(v2, "workspace.json"), `${JSON.stringify(marker, null, 2)}
12234
+ `, true);
12235
+ }
12236
+ stubFor(taskId) {
12237
+ const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
12238
+ return `${JSON.stringify({
12239
+ schemaVersion: SCHEMA_VERSION,
12240
+ id: taskId,
12241
+ runtime: { createdWith: version, updatedWith: version },
12242
+ store: { format: "workit-store", version: STORE_FORMAT, path: this.paths().dir },
12243
+ critical: ["store"]
12244
+ })}
12245
+ `;
11067
12246
  }
11068
- saveRecovery(file, bytes) {
11069
- const target = path7.basename(file) === "workspace.json" ? "workspace" : "task";
11070
- const id = target === "task" ? path7.basename(file, ".json") : "workspace";
11071
- const destination = path7.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
11072
- if (fs6.existsSync(destination)) {
11073
- if (digestBytes(retryTransient(() => fs6.readFileSync(destination))) !== digestBytes(bytes))
11074
- throw new Error("recovery copy already exists with different bytes");
11075
- const stamp = new Date;
11076
- retryTransient(() => fs6.utimesSync(destination, stamp, stamp));
11077
- this.pruneRecovery(`${target}.${id}.`, destination);
11078
- return;
11079
- }
11080
- let temporary;
12247
+ isStub(file) {
11081
12248
  try {
11082
- temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
11083
- const fd = fs6.openSync(temporary, "wx", 384);
11084
- try {
11085
- fs6.writeSync(fd, bytes);
11086
- fs6.fsyncSync(fd);
11087
- } finally {
11088
- fs6.closeSync(fd);
11089
- }
11090
- retryTransient(() => fs6.renameSync(temporary, destination));
11091
- temporary = undefined;
11092
- this.fsyncDirectory(this.recoveryDir);
11093
- this.pruneRecovery(`${target}.${id}.`, destination);
11094
- } finally {
11095
- if (temporary)
11096
- try {
11097
- fs6.unlinkSync(temporary);
11098
- } catch {}
12249
+ const value = JSON.parse(fs8.readFileSync(file, "utf8"));
12250
+ return isObject4(value) && isObject4(value.store) && value.store.format === "workit-store";
12251
+ } catch {
12252
+ return false;
11099
12253
  }
11100
12254
  }
11101
- pruneRecovery(prefix, keep) {
12255
+ writeTombstoneIfMissing() {
11102
12256
  try {
11103
- const copies = this.recoveryCopies(prefix);
11104
- const surplus = this.surplusCopies(copies, path7.basename(keep));
11105
- for (const copy of surplus)
11106
- fs6.rmSync(copy.path, { force: true });
12257
+ const v2 = path9.join(this.root, ".workit");
12258
+ if (fs8.existsSync(path9.join(v2, "workspace.json")))
12259
+ return;
12260
+ let id = null;
12261
+ try {
12262
+ id = JSON.parse(fs8.readFileSync(this.paths().workspace, "utf8")).id ?? null;
12263
+ } catch {}
12264
+ this.writeTombstone(v2, id);
11107
12265
  } catch {}
11108
12266
  }
11109
- recoveryCopies(prefix = "") {
11110
- if (!fs6.existsSync(this.recoveryDir))
11111
- return [];
11112
- const copies = [];
11113
- for (const name of fs6.readdirSync(this.recoveryDir)) {
11114
- if (!name.startsWith(prefix))
11115
- continue;
11116
- const match = RECOVERY_NAME.exec(name);
11117
- if (!match)
11118
- continue;
11119
- const file = path7.join(this.recoveryDir, name);
12267
+ initializeStorage() {
12268
+ const dirs = this.paths();
12269
+ retryTransient(() => fs8.mkdirSync(dirs.checkout, { recursive: true }));
12270
+ retryTransient(() => fs8.mkdirSync(dirs.tasks, { recursive: true }));
12271
+ if (!fs8.existsSync(dirs.marker))
12272
+ replaceFile(dirs.marker, `${JSON.stringify({ format: "workit-store", version: STORE_FORMAT, createdWith: runtimeVersion() })}
12273
+ `, false);
12274
+ if (!dirs.shared) {
12275
+ const ignore = path9.join(dirs.dir, ".gitignore");
12276
+ let current = null;
11120
12277
  try {
11121
- const stat = fs6.lstatSync(file, { bigint: true });
11122
- if (!stat.isFile())
11123
- continue;
11124
- copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
12278
+ current = fs8.readFileSync(ignore, "utf8");
11125
12279
  } catch {}
12280
+ if (current !== `*
12281
+ `)
12282
+ retryTransient(() => fs8.writeFileSync(ignore, `*
12283
+ `));
11126
12284
  }
11127
- return copies;
11128
- }
11129
- surplusCopies(copies, keep) {
11130
- 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);
11131
- return ordered.slice(RECOVERY_COPIES_PER_RECORD);
12285
+ this.writeTombstoneIfMissing();
11132
12286
  }
11133
- collectGarbage(options = {}) {
11134
- const dryRun = options.dryRun === true;
11135
- const report = {
11136
- dryRun,
11137
- recovery: { removed: 0, removedBytes: 0, kept: 0 },
11138
- temporary: { removed: 0 },
11139
- candidates: { removed: 0, tasks: [], skippedActive: [], skippedClosed: [], failed: [] }
12287
+ lockOptions(lockPath) {
12288
+ const inProcess = heldInProcess.has(lockPath);
12289
+ return {
12290
+ lockPath,
12291
+ staleMs: Number.MAX_SAFE_INTEGER,
12292
+ timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
12293
+ retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
12294
+ staleRecovery: "remove-if-unchanged",
12295
+ shouldReclaim: ({ payload, nowMs }) => {
12296
+ let ageMs = null;
12297
+ try {
12298
+ ageMs = nowMs - fs8.lstatSync(lockPath).mtimeMs;
12299
+ } catch {}
12300
+ return classifyLockOwner(payload, ageMs).state === "stale";
12301
+ },
12302
+ shouldRemoveStaleLock: () => true,
12303
+ parsePayload: parseMetadataLockOrNull,
12304
+ payload: () => ({
12305
+ pid: process.pid,
12306
+ processStart: processStartOf(process.pid),
12307
+ host: localLockHost(),
12308
+ nonce: randomUUID3()
12309
+ })
11140
12310
  };
11141
- if (!fs6.existsSync(this.workitDir))
11142
- return success(null, null, report);
11143
- const sweep = () => {
12311
+ }
12312
+ acquire(lockPath, options) {
12313
+ clearAbandonedReclaimGuard(lockPath);
12314
+ const deadline = Date.now() + (options.timeoutMs ?? 0);
12315
+ while (true) {
11144
12316
  try {
11145
- const groups = new Map;
11146
- for (const copy of this.recoveryCopies())
11147
- groups.set(copy.group, [...groups.get(copy.group) ?? [], copy]);
11148
- for (const copies of groups.values()) {
11149
- const surplus = this.surplusCopies(copies);
11150
- report.recovery.kept += copies.length - surplus.length;
11151
- for (const copy of surplus) {
11152
- report.recovery.removed += 1;
11153
- try {
11154
- report.recovery.removedBytes += fs6.statSync(copy.path).size;
11155
- } catch {}
11156
- if (!dryRun)
11157
- fs6.rmSync(copy.path, { force: true });
11158
- }
11159
- }
11160
- const nowMs = Date.now();
11161
- for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
11162
- if (!fs6.existsSync(directory))
11163
- continue;
11164
- for (const name of fs6.readdirSync(directory)) {
11165
- if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
11166
- continue;
11167
- const file = path7.join(directory, name);
11168
- const stat = fs6.lstatSync(file);
11169
- if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
11170
- continue;
11171
- report.temporary.removed += 1;
11172
- if (!dryRun)
11173
- fs6.rmSync(file, { force: true });
11174
- }
11175
- }
11176
- return success(null, null, null);
11177
- } catch (error) {
11178
- return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
11179
- path: this.recoveryDir
12317
+ return acquireFileLockSync(lockPath, {
12318
+ ...options,
12319
+ timeoutMs: Math.max(0, deadline - Date.now())
11180
12320
  });
12321
+ } catch (error) {
12322
+ const code = error?.code;
12323
+ if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
12324
+ throw error;
12325
+ if (code !== "file_lock_stale" && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
12326
+ throw error;
12327
+ if (code !== "file_lock_stale")
12328
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
11181
12329
  }
11182
- };
11183
- const pruned = dryRun ? sweep() : this.withLock(sweep);
11184
- if (!pruned.ok)
11185
- return pruned;
11186
- const tasks = this.listTasks();
11187
- if (!tasks.ok)
11188
- return tasks;
11189
- for (const task of tasks.data) {
11190
- const deduped = dedupeCandidates(task.candidates);
11191
- const removed = task.candidates.length - deduped.length;
11192
- if (removed === 0)
11193
- continue;
11194
- if (task.status === "active") {
11195
- report.candidates.skippedActive.push(task.id);
11196
- continue;
11197
- }
11198
- if (task.status === "closed") {
11199
- report.candidates.skippedClosed.push(task.id);
11200
- continue;
11201
- }
11202
- if (!dryRun) {
11203
- const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
11204
- ...current,
11205
- candidates: dedupeCandidates(current.candidates)
11206
- }));
11207
- if (!written.ok) {
11208
- report.candidates.failed.push(task.id);
11209
- continue;
11210
- }
11211
- }
11212
- report.candidates.removed += removed;
11213
- report.candidates.tasks.push(task.id);
11214
12330
  }
11215
- return success(null, null, report);
11216
12331
  }
11217
- findRecovery(target, taskId, digest, workspace) {
11218
- if (!fs6.existsSync(this.recoveryDir))
11219
- return null;
11220
- const workspaceId = workspace?.id ?? this.trustedWorkspaceId();
11221
- if (target === "workspace" && !workspaceId)
11222
- return null;
11223
- const candidates = this.recoveryCandidates();
11224
- if (!candidates.ok)
11225
- return null;
11226
- for (const candidate of candidates.data) {
11227
- if (candidate.target !== target || candidate.digest !== digest)
11228
- continue;
11229
- try {
11230
- if (!fs6.lstatSync(candidate.path).isFile())
11231
- continue;
11232
- const bytes = fs6.readFileSync(candidate.path);
11233
- if (digestBytes(bytes) !== digest)
11234
- continue;
11235
- if (target === "workspace") {
11236
- const parsed = this.parseBytes(bytes, workspaceRecordSchema);
11237
- if (parsed.ok && parsed.data.id === workspaceId && parsed.data.root === this.root)
11238
- return bytes;
11239
- } else {
11240
- const parsed = this.parseBytes(bytes, taskRecordSchema);
11241
- if (parsed.ok && parsed.data.id === taskId && parsed.data.workspaceId === workspace?.id)
11242
- return bytes;
11243
- }
11244
- } catch {}
12332
+ withLock(lockPath, operation) {
12333
+ try {
12334
+ this.initializeStorage();
12335
+ fs8.mkdirSync(path9.dirname(lockPath), { recursive: true });
12336
+ } catch (error) {
12337
+ return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
12338
+ path: this.paths().dir
12339
+ });
11245
12340
  }
11246
- return null;
11247
- }
11248
- fsyncDirectory(directory) {
12341
+ let handle;
11249
12342
  try {
11250
- const fd = fs6.openSync(directory, "r");
11251
- try {
11252
- fs6.fsyncSync(fd);
11253
- } finally {
11254
- fs6.closeSync(fd);
11255
- }
12343
+ handle = this.acquire(lockPath, this.lockOptions(lockPath));
11256
12344
  } catch (error) {
11257
- if (process.platform !== "win32")
11258
- throw error;
12345
+ return this.lockFailure(lockPath, error);
11259
12346
  }
11260
- }
11261
- trustedWorkspaceId() {
11262
- if (!fs6.existsSync(this.tasksDir))
11263
- return null;
11264
- const ids = new Set;
12347
+ hold(lockPath);
12348
+ let result;
11265
12349
  try {
11266
- for (const name of fs6.readdirSync(this.tasksDir)) {
11267
- if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
11268
- return null;
11269
- const parsed = this.parseBytes(fs6.readFileSync(path7.join(this.tasksDir, name)), taskRecordSchema);
11270
- if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
11271
- return null;
11272
- ids.add(parsed.data.workspaceId);
12350
+ if (!handle.verifyStillHeld())
12351
+ result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
12352
+ else {
12353
+ try {
12354
+ result = operation();
12355
+ } catch (error) {
12356
+ result = failure2("storage_error", `mutation failed: ${String(error)}`);
12357
+ }
11273
12358
  }
11274
- } catch {
11275
- return null;
12359
+ } catch (error) {
12360
+ result = this.lockFailure(lockPath, error);
12361
+ }
12362
+ drop(lockPath);
12363
+ try {
12364
+ retryTransient(() => handle.release());
12365
+ } catch (error) {
12366
+ const released = this.lockFailure(lockPath, error);
12367
+ const releaseError = released.ok ? "metadata lock release failed" : released.error;
12368
+ result = result.ok ? released : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
11276
12369
  }
11277
- return ids.size === 1 ? [...ids][0] : null;
12370
+ return result;
11278
12371
  }
11279
- snapshotBytes(file) {
12372
+ lockDirWritable(lockPath) {
12373
+ const probe = `${lockPath}.${process.pid}.${randomUUID3()}.probe`;
11280
12374
  try {
11281
- return retryTransient(() => fs6.readFileSync(file));
12375
+ fs8.closeSync(fs8.openSync(probe, "wx", 384));
11282
12376
  } catch {
11283
- return null;
12377
+ return false;
11284
12378
  }
12379
+ try {
12380
+ fs8.rmSync(probe, { force: true });
12381
+ } catch {}
12382
+ return true;
11285
12383
  }
11286
- readRecord(file, schema) {
12384
+ lockHolderPresent(lockPath) {
11287
12385
  try {
11288
- const bytes = retryTransient(() => fs6.readFileSync(file, "utf8"));
11289
- return { exists: true, result: this.parseBytes(bytes, schema) };
12386
+ fs8.lstatSync(lockPath);
12387
+ return true;
11290
12388
  } catch (error) {
11291
- if (error?.code === "ENOENT")
11292
- return { exists: false, result: success(null, null, null) };
11293
- return {
11294
- exists: true,
11295
- result: failure2("recovery_required", `snapshot cannot be read: ${String(error)}`, {
11296
- path: file
11297
- })
11298
- };
12389
+ return error?.code !== "ENOENT";
11299
12390
  }
11300
12391
  }
11301
- parseBytes(bytes, schema, mode = "read") {
11302
- let value;
11303
- try {
11304
- value = JSON.parse(typeof bytes === "string" ? bytes : bytes.toString("utf8"));
11305
- } catch {
11306
- return failure2("recovery_required", "snapshot JSON is corrupt");
12392
+ lockFailure(lockPath, error) {
12393
+ const value = error;
12394
+ const code = typeof value?.code === "string" ? value.code : "";
12395
+ if (isTransientWindowsError(error) && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
12396
+ return failure2("storage_error", `cannot create the workit lock (${code}); no other Workit call holds it`, {
12397
+ path: lockPath,
12398
+ guidance: `Check permissions and the read-only attribute on ${path9.dirname(lockPath)}.`
12399
+ });
12400
+ if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
12401
+ let lock = null;
12402
+ try {
12403
+ lock = parseMetadataLockOrNull(fs8.readFileSync(lockPath, "utf8"));
12404
+ } catch {}
12405
+ if (lock?.externalAction)
12406
+ return failure2("writer_conflict", "workspace is reserved by a managed external action", {
12407
+ outcome: "not_started",
12408
+ path: lockPath
12409
+ });
12410
+ return failure2("busy", `workit lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
12411
+ outcome: "not_started",
12412
+ path: lockPath,
12413
+ guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
12414
+ });
11307
12415
  }
11308
- if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
11309
- return failure2("unsupported_version", "unsupported snapshot schema version");
11310
- const parsed = parseStoredRecord(schema, value);
11311
- if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
11312
- return success(null, null, parsed.data);
11313
- const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
11314
- const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
11315
- if (parsed.success)
11316
- return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
11317
- if (parsed.critical.length > 0)
11318
- return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
11319
- if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
11320
- return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
11321
- return failure2("recovery_required", "snapshot does not satisfy its schema");
11322
- }
11323
- conflict(expected, actual) {
11324
- return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
11325
- expectedRevision: expected,
11326
- actualRevision: actual
11327
- });
11328
- }
11329
- workspaceConflict(expected, actual) {
11330
- return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
11331
- }
11332
- taskPath(taskId) {
11333
- return path7.join(this.tasksDir, `${taskId}.json`);
11334
- }
11335
- get workitDir() {
11336
- return path7.join(this.root, ".workit");
11337
- }
11338
- get tasksDir() {
11339
- return path7.join(this.workitDir, "tasks");
11340
- }
11341
- get recoveryDir() {
11342
- return path7.join(this.workitDir, "recovery");
11343
- }
11344
- get workspacePath() {
11345
- return path7.join(this.workitDir, "workspace.json");
11346
- }
11347
- get indexPath() {
11348
- return path7.join(this.workitDir, "index.json");
11349
- }
11350
- get lockPath() {
11351
- return path7.join(this.workitDir, "metadata.lock");
11352
- }
11353
- get gitignorePath() {
11354
- return path7.join(this.workitDir, ".gitignore");
12416
+ const recovery = code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
12417
+ return failure2(recovery ? "recovery_required" : "storage_error", `lock operation failed: ${String(error)}`, { path: lockPath });
11355
12418
  }
11356
12419
  }
11357
- var RECOVERY_COPIES_PER_RECORD = 3, STALE_TEMPORARY_MS, RECOVERY_NAME, processEvidenceSchema, INDEX_VERSION = 1, hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null, indexEntry = (task, file) => {
12420
+ var MIGRATION_PENDING = "workit migration pending", STORE_FORMAT = 3, STORE_MIN_RUNTIME = "3.0.0", SNAPSHOT_EVERY = 32, COMPACT_ABOVE = 200, COMPACT_KEEP = 50, AUTO_COMPACT_BYTES, TOMBSTONE, STALE_TEMPORARY_MS, INDEX_VERSION = 3, isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), hostSession = (value) => isObject4(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null, indexEntry = (task, key, legacy, file) => {
11358
12421
  const sessions = [];
11359
12422
  const intent = hostSession(task.intent.provenance.session);
11360
12423
  if (intent)
@@ -11374,11 +12437,16 @@ var RECOVERY_COPIES_PER_RECORD = 3, STALE_TEMPORARY_MS, RECOVERY_NAME, processEv
11374
12437
  source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
11375
12438
  progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
11376
12439
  sessions,
12440
+ workspaceId: task.workspaceId,
12441
+ key: key?.key ?? null,
12442
+ branch: key?.branch ?? null,
12443
+ boundAt: key?.boundAt ?? null,
12444
+ legacy,
11377
12445
  file
11378
12446
  };
11379
12447
  }, fileSignature = (file) => {
11380
12448
  try {
11381
- const stat = fs6.statSync(file, { bigint: true });
12449
+ const stat = fs8.statSync(file, { bigint: true });
11382
12450
  return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
11383
12451
  } catch {
11384
12452
  return null;
@@ -11387,10 +12455,10 @@ var RECOVERY_COPIES_PER_RECORD = 3, STALE_TEMPORARY_MS, RECOVERY_NAME, processEv
11387
12455
  const [mtime = 0n, ctime = 0n] = signature.split(":").slice(3, 5).map(BigInt);
11388
12456
  const changed = mtime > ctime ? mtime : ctime;
11389
12457
  return BigInt(Date.now()) * 1000000n - changed < RACY_WINDOW_NS;
11390
- }, now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"), cachedRuntimeVersion = null, runtimeVersion = () => {
12458
+ }, now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"), oldestFirst = (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id), cachedRuntimeVersion = null, runtimeVersion = () => {
11391
12459
  if (cachedRuntimeVersion === null) {
11392
12460
  try {
11393
- const pkg = JSON.parse(fs6.readFileSync(path7.join(packageRoot(), "package.json"), "utf8"));
12461
+ const pkg = JSON.parse(fs8.readFileSync(path9.join(packageRoot(), "package.json"), "utf8"));
11394
12462
  cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
11395
12463
  } catch {
11396
12464
  cachedRuntimeVersion = "0.0.0";
@@ -11410,69 +12478,52 @@ var RECOVERY_COPIES_PER_RECORD = 3, STALE_TEMPORARY_MS, RECOVERY_NAME, processEv
11410
12478
  return left > right;
11411
12479
  }
11412
12480
  return false;
11413
- }, jsonBytes = (value) => `${canonicalJson(value)}
11414
- `, digestBytes = (value) => createHash4("sha256").update(value).digest("hex"), isObject2 = (value) => typeof value === "object" && value !== null, 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), validDigest = (value) => /^[0-9a-f]{64}$/.test(value), sameDirectoryIdentity = (left, right) => {
11415
- if (!path7.isAbsolute(left) || !path7.isAbsolute(right))
12481
+ }, 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), sameDirectoryIdentity = (left, right) => {
12482
+ if (!path9.isAbsolute(left) || !path9.isAbsolute(right))
11416
12483
  return false;
11417
- const normalizedLeft = path7.resolve(left);
11418
- const normalizedRight = path7.resolve(right);
12484
+ const normalizedLeft = path9.resolve(left);
12485
+ const normalizedRight = path9.resolve(right);
11419
12486
  try {
11420
- const a = fs6.statSync(normalizedLeft, { bigint: true });
11421
- const b = fs6.statSync(normalizedRight, { bigint: true });
12487
+ const a = fs8.statSync(normalizedLeft, { bigint: true });
12488
+ const b = fs8.statSync(normalizedRight, { bigint: true });
11422
12489
  if (!a.isDirectory() || !b.isDirectory())
11423
12490
  return false;
11424
12491
  if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
11425
12492
  return true;
11426
- const canonicalLeft = fs6.realpathSync(normalizedLeft);
11427
- const canonicalRight = fs6.realpathSync(normalizedRight);
12493
+ const canonicalLeft = fs8.realpathSync(normalizedLeft);
12494
+ const canonicalRight = fs8.realpathSync(normalizedRight);
11428
12495
  if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
11429
12496
  return true;
11430
12497
  return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
11431
12498
  } catch {
11432
12499
  return false;
11433
12500
  }
11434
- }, TRANSIENT_WINDOWS_CODES, isTransientWindowsError = (error) => {
11435
- if (process.platform !== "win32")
11436
- return false;
11437
- const code = error?.code;
11438
- return typeof code === "string" && TRANSIENT_WINDOWS_CODES.has(code);
11439
- }, retryTransient = (run) => {
11440
- if (process.platform !== "win32")
11441
- return run();
11442
- for (let attempt = 0;; attempt += 1) {
11443
- try {
11444
- return run();
11445
- } catch (error) {
11446
- if (attempt >= 20 || !isTransientWindowsError(error))
11447
- throw error;
11448
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 * (attempt + 1));
11449
- }
11450
- }
11451
- }, externalActionLockRoots, heldInProcess, holdRoot = (root) => heldInProcess.set(root, (heldInProcess.get(root) ?? 0) + 1), dropRoot = (root) => {
11452
- const count = (heldInProcess.get(root) ?? 1) - 1;
12501
+ }, externalActionLockRoots, heldInProcess, hold = (lock) => heldInProcess.set(lock, (heldInProcess.get(lock) ?? 0) + 1), drop = (lock) => {
12502
+ const count = (heldInProcess.get(lock) ?? 1) - 1;
11453
12503
  if (count > 0)
11454
- heldInProcess.set(root, count);
12504
+ heldInProcess.set(lock, count);
11455
12505
  else
11456
- heldInProcess.delete(root);
11457
- }, dedupeCandidates = (candidates) => {
11458
- const last = new Map(candidates.map((candidate, index) => [candidate.id, index]));
11459
- return candidates.filter((candidate, index) => last.get(candidate.id) === index);
12506
+ heldInProcess.delete(lock);
12507
+ }, envActor = () => {
12508
+ const value = (key) => process.env[key]?.trim() || null;
12509
+ return {
12510
+ host: value("WORKIT_HOST") ?? "runtime",
12511
+ session: value("WORKIT_SESSION_ID"),
12512
+ agentId: value("WORKIT_AGENT_ID")
12513
+ };
11460
12514
  };
11461
12515
  var init_task_store = __esm(() => {
11462
- init_zod();
11463
- init_package_root();
11464
12516
  init_file_lock();
12517
+ init_package_root();
11465
12518
  init_task_contract();
11466
12519
  init_store_lock();
12520
+ init_event_log();
12521
+ init_paths();
12522
+ init_reduce();
12523
+ init_notes();
12524
+ AUTO_COMPACT_BYTES = 2 * 1024 * 1024;
12525
+ TOMBSTONE = Symbol("tombstone");
11467
12526
  STALE_TEMPORARY_MS = 60 * 60000;
11468
- RECOVERY_NAME = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/;
11469
- processEvidenceSchema = object({
11470
- state: _enum(["stopped", "accounted_for"]),
11471
- pid: number2().int().nonnegative(),
11472
- processStart: string2().nullable(),
11473
- ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
11474
- }).strict();
11475
- TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
11476
12527
  externalActionLockRoots = new AsyncLocalStorage;
11477
12528
  heldInProcess = new Map;
11478
12529
  });
@@ -11534,13 +12585,13 @@ var init_workers = __esm(() => {
11534
12585
  });
11535
12586
 
11536
12587
  // packages/workit-core/src/core/auto-approval.ts
11537
- import { createHash as createHash5 } from "node:crypto";
12588
+ import { createHash as createHash7 } from "node:crypto";
11538
12589
  import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
11539
- import path8 from "node:path";
12590
+ import path10 from "node:path";
11540
12591
  var AUTO_CLASSES, OPERATION_CLASS, operationAutoClass = (operation) => typeof operation === "string" ? OPERATION_CLASS[operation] ?? null : null, digestOf = (configDirPath) => {
11541
12592
  try {
11542
12593
  const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
11543
- return createHash5("sha256").update(raw).digest("hex");
12594
+ return createHash7("sha256").update(raw).digest("hex");
11544
12595
  } catch {
11545
12596
  return null;
11546
12597
  }
@@ -11580,7 +12631,7 @@ var AUTO_CLASSES, OPERATION_CLASS, operationAutoClass = (operation) => typeof op
11580
12631
  const cwd = parsed?.payload?.cwd;
11581
12632
  if (cwd === undefined)
11582
12633
  return root;
11583
- if (typeof cwd !== "string" || !path8.isAbsolute(cwd))
12634
+ if (typeof cwd !== "string" || !path10.isAbsolute(cwd))
11584
12635
  return null;
11585
12636
  return realpathSync3(cwd);
11586
12637
  } catch {
@@ -11672,9 +12723,9 @@ var init_auto_approval = __esm(() => {
11672
12723
  });
11673
12724
 
11674
12725
  // packages/workit-core/src/core/authority.ts
11675
- import { createHash as createHash6 } from "node:crypto";
11676
- import * as fs7 from "node:fs";
11677
- import path9 from "node:path";
12726
+ import { createHash as createHash8 } from "node:crypto";
12727
+ import * as fs9 from "node:fs";
12728
+ import path11 from "node:path";
11678
12729
  function applicableDecision(task, purpose, binding, checkoutRoot) {
11679
12730
  if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
11680
12731
  return [];
@@ -12076,10 +13127,10 @@ var verifiedAuthorities, trustedAuthority = (kind, provenance, expected, binding
12076
13127
  const record = verifiedAuthorities.get(authority);
12077
13128
  verifiedAuthorities.delete(authority);
12078
13129
  return record?.provenance ?? null;
12079
- }, authorityMatches = (authority, expected) => authority.kind === "action" && authority.taskId === expected.taskId && authority.workspaceId === expected.workspaceId && authority.decisionId === expected.decisionId && sameRef(authority.actionRef, expected.actionRef) && authority.taskRevision === expected.taskRevision && authority.workspaceRevision === expected.workspaceRevision && authority.outcome === expected.outcome, authorityDecisionMatches = (authority, decision) => authority.purpose === decision.purpose && authority.response === decision.response && authority.digest === decision.digest && canonicalJson(authority.binding) === canonicalJson(decision.binding) && canonicalJson(authority.requirementIds) === canonicalJson(decision.requirementIds), provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session), digestBytes2 = (bytes) => createHash6("sha256").update(bytes).digest("hex"), verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
13130
+ }, authorityMatches = (authority, expected) => authority.kind === "action" && authority.taskId === expected.taskId && authority.workspaceId === expected.workspaceId && authority.decisionId === expected.decisionId && sameRef(authority.actionRef, expected.actionRef) && authority.taskRevision === expected.taskRevision && authority.workspaceRevision === expected.workspaceRevision && authority.outcome === expected.outcome, authorityDecisionMatches = (authority, decision) => authority.purpose === decision.purpose && authority.response === decision.response && authority.digest === decision.digest && canonicalJson(authority.binding) === canonicalJson(decision.binding) && canonicalJson(authority.requirementIds) === canonicalJson(decision.requirementIds), provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session), digestBytes = (bytes) => createHash8("sha256").update(bytes).digest("hex"), verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
12080
13131
  let root;
12081
13132
  try {
12082
- root = fs7.realpathSync(checkoutRoot);
13133
+ root = fs9.realpathSync(checkoutRoot);
12083
13134
  } catch {
12084
13135
  return failure2("permission_denied", "checkout root is unavailable");
12085
13136
  }
@@ -12088,23 +13139,23 @@ var verifiedAuthorities, trustedAuthority = (kind, provenance, expected, binding
12088
13139
  continue;
12089
13140
  if (!reference.digest)
12090
13141
  return failure2("invalid_input", "document references require a byte digest");
12091
- const target = path9.resolve(root, reference.path);
12092
- if (target !== root && !target.startsWith(`${root}${path9.sep}`))
13142
+ const target = path11.resolve(root, reference.path);
13143
+ if (target !== root && !target.startsWith(`${root}${path11.sep}`))
12093
13144
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
12094
13145
  fields: [
12095
13146
  { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
12096
13147
  ]
12097
13148
  });
12098
13149
  try {
12099
- const relative = path9.relative(root, target);
13150
+ const relative = path11.relative(root, target);
12100
13151
  let current = root;
12101
- for (const segment of relative.split(path9.sep)) {
13152
+ for (const segment of relative.split(path11.sep)) {
12102
13153
  if (!segment)
12103
13154
  continue;
12104
- current = path9.join(current, segment);
12105
- const stat = fs7.lstatSync(current);
12106
- const real = fs7.realpathSync(current);
12107
- if (real !== root && !real.startsWith(`${root}${path9.sep}`))
13155
+ current = path11.join(current, segment);
13156
+ const stat = fs9.lstatSync(current);
13157
+ const real = fs9.realpathSync(current);
13158
+ if (real !== root && !real.startsWith(`${root}${path11.sep}`))
12108
13159
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
12109
13160
  fields: [
12110
13161
  {
@@ -12120,7 +13171,7 @@ var verifiedAuthorities, trustedAuthority = (kind, provenance, expected, binding
12120
13171
  if (current !== target && !stat.isDirectory())
12121
13172
  return failure2("invalid_input", "document reference ancestor is not a directory");
12122
13173
  }
12123
- if (digestBytes2(fs7.readFileSync(target)) !== reference.digest)
13174
+ if (digestBytes(fs9.readFileSync(target)) !== reference.digest)
12124
13175
  return failure2("permission_denied", "approved document bytes have changed");
12125
13176
  } catch {
12126
13177
  return failure2("invalid_input", "approved document is unavailable");
@@ -12532,9 +13583,9 @@ var init_policy_resolver = __esm(() => {
12532
13583
  });
12533
13584
 
12534
13585
  // packages/workit-core/src/check-config.ts
12535
- import { spawnSync as spawnSync3 } from "node:child_process";
12536
- import fs8 from "node:fs";
12537
- import path10 from "node:path";
13586
+ import { spawnSync as spawnSync4 } from "node:child_process";
13587
+ import fs10 from "node:fs";
13588
+ import path12 from "node:path";
12538
13589
  function splitCommand(command) {
12539
13590
  const argv = [];
12540
13591
  let current = "";
@@ -12572,12 +13623,12 @@ function splitCommand(command) {
12572
13623
  return argv.length ? argv : new Error("empty command");
12573
13624
  }
12574
13625
  function checkRoot(cwd) {
12575
- return path10.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
13626
+ return path12.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
12576
13627
  }
12577
13628
  function detectedChecks(root) {
12578
13629
  const read = (file) => {
12579
13630
  try {
12580
- return fs8.readFileSync(path10.join(root, file), "utf8");
13631
+ return fs10.readFileSync(path12.join(root, file), "utf8");
12581
13632
  } catch {
12582
13633
  return null;
12583
13634
  }
@@ -12615,7 +13666,7 @@ function detectedChecks(root) {
12615
13666
  }
12616
13667
  function loadCheckConfig(cwd) {
12617
13668
  const root = checkRoot(cwd);
12618
- const file = path10.join(root, CHECKS_FILE);
13669
+ const file = path12.join(root, CHECKS_FILE);
12619
13670
  const none = (error = null) => ({
12620
13671
  source: error ? CHECKS_FILE : "none",
12621
13672
  root,
@@ -12625,7 +13676,7 @@ function loadCheckConfig(cwd) {
12625
13676
  });
12626
13677
  let text = null;
12627
13678
  try {
12628
- text = fs8.readFileSync(file, "utf8");
13679
+ text = fs10.readFileSync(file, "utf8");
12629
13680
  } catch (error) {
12630
13681
  if (error.code !== "ENOENT")
12631
13682
  return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
@@ -12686,10 +13737,10 @@ function workitAvailable(env = process.env) {
12686
13737
  return workitOnPath;
12687
13738
  const windows = process.platform === "win32";
12688
13739
  const exts = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
12689
- const dirs = (env.PATH ?? env.Path ?? "").split(path10.delimiter).filter(Boolean);
13740
+ const dirs = (env.PATH ?? env.Path ?? "").split(path12.delimiter).filter(Boolean);
12690
13741
  workitOnPath = dirs.some((dir) => exts.some((ext) => {
12691
13742
  try {
12692
- return fs8.statSync(path10.join(dir, `workit${ext}`)).isFile();
13743
+ return fs10.statSync(path12.join(dir, `workit${ext}`)).isFile();
12693
13744
  } catch {
12694
13745
  return false;
12695
13746
  }
@@ -12700,7 +13751,7 @@ function checkCommand(args, version, onPath = workitAvailable()) {
12700
13751
  return onPath ? `workit check ${args}` : `npx -y @brainervirus/workit-cli@${version} check ${args}`;
12701
13752
  }
12702
13753
  var SCRIPT_CHECKS, CHECKS_FILE = "workit.checks.json", isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), SHELL_META, git = (cwd, args) => {
12703
- const run = spawnSync3("git", args, {
13754
+ const run = spawnSync4("git", args, {
12704
13755
  cwd,
12705
13756
  encoding: "utf8",
12706
13757
  timeout: 20000,
@@ -12710,7 +13761,7 @@ var SCRIPT_CHECKS, CHECKS_FILE = "workit.checks.json", isRecord2 = (value) => ty
12710
13761
  const value = run.status === 0 ? (run.stdout ?? "").trim() : "";
12711
13762
  return value || null;
12712
13763
  }, packageManager = (root) => {
12713
- const has = (file) => fs8.existsSync(path10.join(root, file));
13764
+ const has = (file) => fs10.existsSync(path12.join(root, file));
12714
13765
  if (has("bun.lock") || has("bun.lockb"))
12715
13766
  return "bun";
12716
13767
  if (has("pnpm-lock.yaml"))
@@ -12733,11 +13784,11 @@ var init_check_config = __esm(() => {
12733
13784
  });
12734
13785
 
12735
13786
  // packages/workit-core/src/git/rev.ts
12736
- import { spawnSync as spawnSync4 } from "node:child_process";
12737
- import { createHash as createHash7 } from "node:crypto";
12738
- import fs9 from "node:fs";
13787
+ import { spawnSync as spawnSync5 } from "node:child_process";
13788
+ import { createHash as createHash9 } from "node:crypto";
13789
+ import fs11 from "node:fs";
12739
13790
  import os2 from "node:os";
12740
- import path11 from "node:path";
13791
+ import path13 from "node:path";
12741
13792
  function headSha(cwd) {
12742
13793
  return line(git2(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
12743
13794
  }
@@ -12748,10 +13799,10 @@ function worktreeTree(cwd, options = {}) {
12748
13799
  return null;
12749
13800
  const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
12750
13801
  const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
12751
- const scratch = fs9.mkdtempSync(path11.join(os2.tmpdir(), "workit-tree-"));
13802
+ const scratch = fs11.mkdtempSync(path13.join(os2.tmpdir(), "workit-tree-"));
12752
13803
  try {
12753
- const tempIndex = path11.join(scratch, "index");
12754
- if (fs9.existsSync(indexPath))
13804
+ const tempIndex = path13.join(scratch, "index");
13805
+ if (fs11.existsSync(indexPath))
12755
13806
  seedIndex(indexPath, tempIndex);
12756
13807
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
12757
13808
  const untracked = git2(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
@@ -12763,7 +13814,7 @@ function worktreeTree(cwd, options = {}) {
12763
13814
  const skipped = [];
12764
13815
  for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
12765
13816
  try {
12766
- const stat = fs9.statSync(path11.join(top, file));
13817
+ const stat = fs11.statSync(path13.join(top, file));
12767
13818
  if (stat.isFile() && stat.size > cap)
12768
13819
  skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
12769
13820
  } catch {}
@@ -12780,10 +13831,10 @@ function worktreeTree(cwd, options = {}) {
12780
13831
  if (!tree)
12781
13832
  return null;
12782
13833
  const base = line(git2(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
12783
- const key = skipped.length ? `sha256:${createHash7("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
13834
+ const key = skipped.length ? `sha256:${createHash9("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
12784
13835
  return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
12785
13836
  } finally {
12786
- fs9.rmSync(scratch, { recursive: true, force: true });
13837
+ fs11.rmSync(scratch, { recursive: true, force: true });
12787
13838
  }
12788
13839
  }
12789
13840
  function worktreeSignal(cwd, options = {}) {
@@ -12803,16 +13854,16 @@ function worktreeSignal(cwd, options = {}) {
12803
13854
  });
12804
13855
  if (!status.ok)
12805
13856
  return null;
12806
- const hash = createHash7("sha256");
13857
+ const hash = createHash9("sha256");
12807
13858
  hash.update(`${headSha(top) ?? "unborn"}\x00`);
12808
13859
  for (const entry of status.stdout.split("\x00")) {
12809
13860
  if (entry.length < 4)
12810
13861
  continue;
12811
13862
  const file = entry.slice(3);
12812
- const absolute = path11.join(top, file);
13863
+ const absolute = path13.join(top, file);
12813
13864
  let stat = "-";
12814
13865
  try {
12815
- const info = fs9.lstatSync(absolute, { bigint: true });
13866
+ const info = fs11.lstatSync(absolute, { bigint: true });
12816
13867
  stat = `${info.mode.toString(8)}:${info.size}:${info.mtimeNs}`;
12817
13868
  if (info.isDirectory())
12818
13869
  stat += `:${headSha(absolute) ?? "-"}`;
@@ -12822,7 +13873,7 @@ function worktreeSignal(cwd, options = {}) {
12822
13873
  return `sig:${hash.digest("hex")}`;
12823
13874
  }
12824
13875
  var MAX_BUFFER, GIT_TIMEOUTS, git2 = (cwd, args, options = {}) => {
12825
- const result = spawnSync4("git", args, {
13876
+ const result = spawnSync5("git", args, {
12826
13877
  cwd,
12827
13878
  encoding: "utf8",
12828
13879
  env: options.env ?? process.env,
@@ -12845,10 +13896,10 @@ var MAX_BUFFER, GIT_TIMEOUTS, git2 = (cwd, args, options = {}) => {
12845
13896
  const value = run.stdout.trim();
12846
13897
  return value ? value : null;
12847
13898
  }, emptyTree = (cwd) => line(git2(cwd, ["mktree"], { input: "" })), MAX_UNTRACKED_BYTES, seedIndex = (from, to) => {
12848
- fs9.copyFileSync(from, to);
12849
- const { atimeNs, mtimeNs } = fs9.statSync(from, { bigint: true });
13899
+ fs11.copyFileSync(from, to);
13900
+ const { atimeNs, mtimeNs } = fs11.statSync(from, { bigint: true });
12850
13901
  const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
12851
- fs9.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
13902
+ fs11.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
12852
13903
  };
12853
13904
  var init_rev = __esm(() => {
12854
13905
  MAX_BUFFER = 256 * 1024 * 1024;
@@ -12857,14 +13908,14 @@ var init_rev = __esm(() => {
12857
13908
  });
12858
13909
 
12859
13910
  // packages/workit-core/src/core/task-evaluation.ts
12860
- import { createHash as createHash8 } from "node:crypto";
12861
- import * as fs10 from "node:fs";
12862
- import { spawnSync as spawnSync5 } from "node:child_process";
12863
- import path12 from "node:path";
13911
+ import { createHash as createHash10 } from "node:crypto";
13912
+ import * as fs12 from "node:fs";
13913
+ import { spawnSync as spawnSync6 } from "node:child_process";
13914
+ import path14 from "node:path";
12864
13915
  function captureCandidate(root, scope, environment = []) {
12865
13916
  let checkout;
12866
13917
  try {
12867
- checkout = fs10.realpathSync(root);
13918
+ checkout = fs12.realpathSync(root);
12868
13919
  } catch {
12869
13920
  return failure2("invalid_input", "candidate root does not exist");
12870
13921
  }
@@ -12882,11 +13933,11 @@ function captureCandidate(root, scope, environment = []) {
12882
13933
  if (item === ".git" || item === ".workit")
12883
13934
  continue;
12884
13935
  names.add(item);
12885
- if (!fs10.existsSync(target))
13936
+ if (!fs12.existsSync(target))
12886
13937
  continue;
12887
13938
  let stat;
12888
13939
  try {
12889
- stat = fs10.lstatSync(target);
13940
+ stat = fs12.lstatSync(target);
12890
13941
  } catch {
12891
13942
  uncertain = true;
12892
13943
  continue;
@@ -12902,17 +13953,17 @@ function captureCandidate(root, scope, environment = []) {
12902
13953
  for (const item of [...names].toSorted()) {
12903
13954
  if (!scopeMatches(item, normalizedScope))
12904
13955
  continue;
12905
- const target = path12.join(checkout, item);
13956
+ const target = path14.join(checkout, item);
12906
13957
  try {
12907
- const stat = fs10.lstatSync(target);
13958
+ const stat = fs12.lstatSync(target);
12908
13959
  if (stat.isSymbolicLink()) {
12909
- const link = fs10.readlinkSync(target);
12910
- files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
13960
+ const link = fs12.readlinkSync(target);
13961
+ files.push({ path: item, kind: "symlink", digest: digestBytes2(link), executable: null });
12911
13962
  } else if (stat.isFile()) {
12912
13963
  files.push({
12913
13964
  path: item,
12914
13965
  kind: "file",
12915
- digest: digestBytes3(fs10.readFileSync(target)),
13966
+ digest: digestBytes2(fs12.readFileSync(target)),
12916
13967
  executable: (stat.mode & 73) !== 0
12917
13968
  });
12918
13969
  } else if (!stat.isDirectory()) {
@@ -12937,7 +13988,7 @@ function captureCandidate(root, scope, environment = []) {
12937
13988
  const values = supplied.toSorted((left, right) => compareCodeUnits3(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
12938
13989
  if (values.some(({ value }) => value === null))
12939
13990
  completeness = "uncertain";
12940
- const headResult = spawnSync5("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
13991
+ const headResult = spawnSync6("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
12941
13992
  if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
12942
13993
  completeness = "uncertain";
12943
13994
  const head = headResult.status === 0 ? headResult.stdout.trim() : null;
@@ -13247,12 +14298,12 @@ function evaluateClosure(requestedOutcome, view) {
13247
14298
  requirementIds: evaluations.map((item) => item.requirementId)
13248
14299
  });
13249
14300
  }
13250
- var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex"), inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path12.sep}`), relative = (root, candidate) => path12.relative(root, candidate).split(path12.sep).join("/") || ".", canonicalPath = (value) => {
14301
+ var digestBytes2 = (value) => createHash10("sha256").update(value).digest("hex"), inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path14.sep}`), relative = (root, candidate) => path14.relative(root, candidate).split(path14.sep).join("/") || ".", canonicalPath = (value) => {
13251
14302
  if (typeof value !== "string" || !value)
13252
14303
  return null;
13253
14304
  if (value.includes("\\") || value.split("/").includes(".."))
13254
14305
  return null;
13255
- const normalized = path12.posix.normalize(value);
14306
+ const normalized = path14.posix.normalize(value);
13256
14307
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
13257
14308
  return null;
13258
14309
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -13282,7 +14333,7 @@ var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex"),
13282
14333
  }
13283
14334
  return left.length - right.length;
13284
14335
  }, gitPaths = (root) => {
13285
- const result = spawnSync5("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
14336
+ const result = spawnSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
13286
14337
  cwd: root,
13287
14338
  encoding: "buffer"
13288
14339
  });
@@ -13294,20 +14345,20 @@ var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex"),
13294
14345
  git: !/not a git repository/i.test(stderr)
13295
14346
  };
13296
14347
  }
13297
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path12.sep).join("/")) : [];
13298
- const stagedDeleted = spawnSync5("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
14348
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")) : [];
14349
+ const stagedDeleted = spawnSync6("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
13299
14350
  if (stagedDeleted.status !== 0) {
13300
14351
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
13301
14352
  if (!/not a git repository/i.test(stderr))
13302
14353
  return { paths, uncertain: true, git: true };
13303
14354
  } else if (stagedDeleted.stdout) {
13304
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path12.sep).join("/")));
14355
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")));
13305
14356
  }
13306
14357
  return { paths: [...new Set(paths)], uncertain: false, git: true };
13307
14358
  }, walk = (root, directory, output) => {
13308
14359
  let names;
13309
14360
  try {
13310
- names = fs10.readdirSync(directory);
14361
+ names = fs12.readdirSync(directory);
13311
14362
  } catch {
13312
14363
  return true;
13313
14364
  }
@@ -13315,10 +14366,10 @@ var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex"),
13315
14366
  for (const name of names) {
13316
14367
  if (name === ".git" || name === ".workit")
13317
14368
  continue;
13318
- const target = path12.join(directory, name);
14369
+ const target = path14.join(directory, name);
13319
14370
  let stat;
13320
14371
  try {
13321
- stat = fs10.lstatSync(target);
14372
+ stat = fs12.lstatSync(target);
13322
14373
  } catch {
13323
14374
  uncertain = true;
13324
14375
  continue;
@@ -13332,14 +14383,14 @@ var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex"),
13332
14383
  }, scopeRoots = (root, scope) => {
13333
14384
  const roots = [];
13334
14385
  for (const item of scope.paths.length ? scope.paths : ["."]) {
13335
- const target = path12.resolve(root, item);
14386
+ const target = path14.resolve(root, item);
13336
14387
  if (!inside(root, target))
13337
14388
  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` }] });
13338
14389
  let ancestor = target;
13339
- while (!fs10.existsSync(ancestor) && ancestor !== root)
13340
- ancestor = path12.dirname(ancestor);
14390
+ while (!fs12.existsSync(ancestor) && ancestor !== root)
14391
+ ancestor = path14.dirname(ancestor);
13341
14392
  try {
13342
- if (!inside(root, fs10.realpathSync(ancestor)))
14393
+ if (!inside(root, fs12.realpathSync(ancestor)))
13343
14394
  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` }] });
13344
14395
  } catch {
13345
14396
  return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
@@ -13459,104 +14510,76 @@ var METHODS, methodMatches = (definition, requirement) => definition.dimensions?
13459
14510
  return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
13460
14511
  return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
13461
14512
  }, invariantBootstrap = () => `
13462
- Workit is optional coordination and repository-policy tooling around the host,
13463
- not a permission system or a mandatory workflow. Native host allow/ask/deny,
13464
- sandbox, plan/read-only mode, and organization rules remain authoritative.
13465
- Ordinary investigation, questions, non-Git work, and routine reversible edits
13466
- need zero Workit task, assessment, or writer calls. Use one compact tracked
13467
- record only when handoff, dependent steps, concurrent actors, or meaningful
13468
- decisions make continuity useful; similar titles alone never merge tasks.
14513
+ Workit is optional coordination and proof tooling around the host: you decide
14514
+ what and whether, the \`workit\` CLI does how and records what it observed.
14515
+ Native host allow/ask/deny, sandbox and org rules stay authoritative. Never
14516
+ switch paths to evade a denial, never claim enforcement a host cannot provide,
14517
+ and never fabricate a receipt, approval or verdict. A question answer is not
14518
+ host permission, and imported decisions or handoffs never grant authority.
13469
14519
 
13470
- For a routine user-authorized branch or commit, prefer native host Git/shell
13471
- tools from the outset when managed coordination or outcome reconciliation is
13472
- not needed. Inspect the actual target checkout and its configured conventions;
13473
- use Workit's read-only context/policy tools when needed. Do not start a task,
13474
- acquire a writer, or mint a decision receipt solely for a native Git action.
13475
- Native permissions still apply. Never switch execution paths to evade a denial
13476
- or repeat an uncertain managed effect. A local-commit endpoint does not imply
13477
- PR readiness, a fresh review, or post-commit assessment/closure paperwork.
14520
+ Autonomy contract:
14521
+ - Continue to the requested endpoint (commit, push, PR, green CI, verified,
14522
+ merged) without asking "continue?", running checks and repairs on the way.
14523
+ Stop only for a new consequential choice, a host denial, a conflicting edit
14524
+ or an unresolved blocker; report open gaps and uncertain workers.
14525
+ - No endpoint named: stop at a local commit on a policy-compliant branch and
14526
+ state the next command. Push and open a PR only when the request implies
14527
+ delivery (fix, implement, ship) and \`workit grant show\` reports
14528
+ \`defaultEndpoint\` \`pr\`; otherwise the endpoint is \`commit\`. PRs open,
14529
+ CI green and verified is the most autonomy allows, never the default target.
14530
+ Merge and release need a workspace grant; a blocked verb names its unblock.
14531
+ - Ask only for a product or preference choice, or for authority you lack, with
14532
+ your recommended answer. Facts are yours: read, run or prototype.
14533
+ - Label claims measured (you ran it this session and saw the result),
14534
+ inferred, or guess. Prose is a note: only a configured check the CLI ran
14535
+ (\`workit check <name>\`) satisfies a gate; an ad-hoc
14536
+ \`workit check -- <cmd>\` never does.
14537
+ - Author is not verifier: never record a passing verdict on work your session
14538
+ wrote. The lead starts each verifier with its own session
14539
+ (\`WORKIT_SESSION_ID=<lead>-v<n>\`; Claude Code subagents get one from the
14540
+ hook), and the verifier records \`workit ledger verdict\`.
14541
+ - A local-commit endpoint does not imply PR readiness, and a local commit alone
14542
+ is not evidence of a requested remote push. Before reporting delivery,
14543
+ reconcile every requested item and observe it (\`workit verify-delivery\`).
13478
14544
 
13479
- For tracked work, inspect its current state and use the shared operations for
13480
- task, policy, evidence, finding, decision, worker, writer, and state changes.
13481
- Start a record once for an explicit tracked objective; assess or reassess only
13482
- when policy selection or changed evidence/constraints requires it. Omitted
13483
- expectedRevision and expectedWorkspaceRevision use current values; explicit
13484
- values are still concurrency-checked, so never copy revisions between calls.
13485
- A busy result means another live Workit call holds the checkout lock: retry the
13486
- same call; it is not a recovery condition. A lock left by a dead process is
13487
- reclaimed on the next write, and \`workit doctor --fix-lock\` clears it on demand.
13488
- An omitted revision absorbs a concurrent write: Workit re-reads, re-checks
13489
- policy, and reapplies the call, returning busy under persistent contention. A
13490
- revision_conflict means a revision you passed is stale: re-read the record
13491
- before deciding whether to retry.
13492
- A solo edit does not need writer acquisition; use it when concurrent checkout
13493
- writers need coordination. Record only observed facts and checks. Close-time
13494
- testing and verification gates accept only a configured check the CLI ran:
13495
- \`workit check <name>\` (\`npx -y @brainervirus/workit-cli check <name>\` when
13496
- \`workit\` is not on PATH); a recorded check result is a note and an ad-hoc
13497
- \`workit check -- <cmd>\` never satisfies a gate. Evidence can become stale when its bound candidate or tree changes; reconcile findings against the
13498
- current candidate before recording completion.
14545
+ CLI first: if a step has one right answer, use the \`workit\` verb instead of
14546
+ hand-running git, gh or glab: check, git, pr, ci, stack, verify-delivery,
14547
+ ledger, handoff, test-audit (\`workit help <verb>\`; without \`workit\` on
14548
+ PATH, \`npx -y @brainervirus/workit-cli\`). \`busy\` is retryable
14549
+ (\`workit doctor --fix-lock\` clears a dead lock); \`blocked\` names its unblock.
14550
+ The task families (task, policy, evidence, finding, decision, worker, writer,
14551
+ state) are optional continuity for tracked work; each branch has one implicit
14552
+ task that the first recording creates, and a solo edit needs no task or writer.
14553
+ Omit revisions; a revision_conflict means one you passed is stale, so re-read.
14554
+ Across repositories, bind each item to its checkout, branch and endpoint,
14555
+ resolve competing targets before a mutation, and reconcile an uncertain
14556
+ external effect before retrying it.
13499
14557
 
13500
- Workit validates domain policy against the actual action target, configured
13501
- account, branch and commit conventions, protected refs, and current repository
13502
- state. The task directory is coordination state, not a boundary on which
13503
- repository may be changed. Use an action-time cwd target where a managed
13504
- Git/hosting action supports it; a non-Git directory remains valid for OS work.
13505
- GitHub/GitLab use the active gh/glab CLI identity; YouTrack uses its own token.
13506
- Preserve uncertain external outcomes and reconcile repository/provider state
13507
- before retrying. Internal reservations prevent duplicate or ambiguous effects;
13508
- they are not permission tickets for every edit.
14558
+ Mid-task input: answer a quick question in place, fold an adjustment in, and
14559
+ never silently drop or resume an objective (workit-continue).
13509
14560
 
13510
- On OpenCode V1 and V2, use native host tools for external mutations. Workit
13511
- provides read-only workit_context; it has no managed external-action executor.
13512
- Do not create proposals or request Workit approvals merely to invoke native
13513
- tools. Old uncertain managed effects still require evidence before any retry.
13514
-
13515
- Use host-native authorization through the host's supported path. A native
13516
- question receipt records an actual question interaction; it is not automatic
13517
- host permission. Record a meaningful user choice once with provenance when
13518
- future retrieval helps. Never fabricate a receipt, re-ask only to mint one, or
13519
- let imported decisions grant authority. Never turn a host deny into allow or
13520
- claim enforcement/evidence a host cannot provide. A precise request with settled
13521
- constraints does not need an interview. Present genuine unresolved options with
13522
- evidence and a recommendation, then continue toward the requested delivery
13523
- endpoint: investigation, implementation, branch/commit/push, PR/MR-ready,
13524
- merge, or release. Run
13525
- applicable checks and safe repairs without repeated continuation questions;
13526
- stop for missing host authority, a new consequential choice, a conflicting edit,
13527
- or an unresolved blocker. A tracked record may close when actual evidence
13528
- supports its outcome; no final human closure ceremony is required.
13529
-
13530
- A completed native subagent run stops its bound worker by itself; if a worker
13531
- strands in cancelling with its run verifiably over, repeat worker.cancel to
13532
- confirm the stop. Preserve unresolved requirements, gaps, and uncertain workers.
13533
- When context changes, distinguish a quick question, same-task adjustment, and
13534
- separate request. Answer a quick question without pausing/resuming task state;
13535
- park a concise checkpoint only when substantial work needs to continue later.
13536
- For work spanning repositories, bind each unfinished item to its actual checkout,
13537
- branch, requested deliverables, and delivery endpoint. Preserve held items with
13538
- an explicit resume condition. Resolve competing plausible targets before a
13539
- mutation; ask only when available context cannot settle that consequential choice.
13540
- Before reporting delivery, reconcile every requested item and observe the actual
13541
- target result; a local commit alone is not evidence of a requested remote push.
13542
-
13543
- Skill routing: slash aliases /wk-* load on demand. Use workit-steer for a
13544
- substantial interruption or change of direction, workit-deslop when relevant
13545
- to a PR-ready endpoint, workit-green-run for failing CI, workit-blast-radius
13546
- when impact is uncertain, workit-challenge for genuinely open consequential
13547
- choices, workit-bdd to turn acceptance criteria into Given/When/Then tests, and
13548
- workit-test-audit to check tests for tautologies. Load workit-plan when dependencies or handoff need durable next actions.
13549
- Load the skill; never act from memory of it.
14561
+ Skills: load the skill, never act from memory of it (slash aliases /wk-<name>).
14562
+ - brainstorm, plan, spec, grill, should we: workit-shape
14563
+ - implement, build, add a feature: workit-implement
14564
+ - review, blast radius: workit-review
14565
+ - bug, broken, flaky, regression: workit-debug
14566
+ - ship, babysit, CI, merge: workit-ship
14567
+ - resume, pick up, handoff, interruption: workit-continue
14568
+ - BDD, TDD, acceptance criteria, Given/When/Then: workit-bdd
14569
+ - test audit, tautology, weak tests: workit-test-audit
14570
+ - deslop, slop, dead code: workit-deslop
14571
+ - fan out, parallelize, parallel agents, swarm: workit-fanout
14572
+ - verify the app, smoke test, prove it works: workit-verify-app
13550
14573
  `.trim();
13551
14574
  var init_methods = __esm(() => {
13552
14575
  METHODS = {
13553
- "workit-challenge": { dimensions: ["challenge", "decisions"] },
13554
- "workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
14576
+ "workit-shape": { dimensions: ["challenge", "decisions", "artifacts", "continuity"] },
14577
+ "workit-bdd": { dimensions: ["testing"] },
13555
14578
  "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
13556
- "workit-plan": { dimensions: ["artifacts", "continuity"] },
13557
- "workit-implement": { dimensions: ["delegation"] },
14579
+ "workit-implement": { ruleIds: ["mechanical-existing-checks"] },
14580
+ "workit-fanout": { dimensions: ["delegation"] },
13558
14581
  "workit-debug": {},
13559
- "workit-handoff": {},
14582
+ "workit-continue": {},
13560
14583
  "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
13561
14584
  };
13562
14585
  });
@@ -13690,7 +14713,9 @@ class WorkitCore {
13690
14713
  }
13691
14714
  contextRootError() {
13692
14715
  try {
13693
- if (realpathSync6(this.context.root) !== this.store.root)
14716
+ realpathSync6(this.context.root);
14717
+ const root = checkoutRootOf(this.context.root);
14718
+ if (root !== this.store.root && !sameDirectoryIdentity(root, this.store.root))
13694
14719
  return failure2("invalid_input", "operation context root does not match the task store root");
13695
14720
  } catch {
13696
14721
  return failure2("invalid_input", "operation context root cannot be resolved");
@@ -13712,6 +14737,30 @@ class WorkitCore {
13712
14737
  retryOmittedRevisions(request, run) {
13713
14738
  return retryFilledRevisions(filledRevisions(request), run);
13714
14739
  }
14740
+ implicitTask(family, request) {
14741
+ if (typeof request !== "object" || request === null || Array.isArray(request))
14742
+ return success(null, null, request);
14743
+ const value = request;
14744
+ if (value.taskId !== undefined || typeof value.action !== "string")
14745
+ return success(null, null, request);
14746
+ const action = `${family}.${value.action}`;
14747
+ if (!TASK_SCOPED_ACTIONS.has(action))
14748
+ return success(null, null, request);
14749
+ const root = this.contextRootError();
14750
+ if (!root.ok)
14751
+ return root;
14752
+ const create = CREATING_ACTIONS.has(action) && (this.context.workerId ?? null) === null;
14753
+ const found = this.store.implicitTask({
14754
+ provenance: provenance(this.context),
14755
+ create,
14756
+ now: trustedNow(this.context)
14757
+ });
14758
+ if (!found.ok)
14759
+ return found;
14760
+ if (!found.data)
14761
+ 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');
14762
+ return success(null, null, { ...request, taskId: found.data.task.id });
14763
+ }
13715
14764
  helperEntry(task, requireSession = false, requireActive = false) {
13716
14765
  const workerId = this.context.workerId ?? null;
13717
14766
  if (workerId === null)
@@ -13743,7 +14792,11 @@ class WorkitCore {
13743
14792
  return null;
13744
14793
  }
13745
14794
  state(request) {
13746
- return request?.action === "import" ? this.retryOmittedRevisions(request, () => this.stateOnce(request)) : this.stateOnce(request);
14795
+ const resolved = this.implicitTask("state", request);
14796
+ if (!resolved.ok)
14797
+ return resolved;
14798
+ const value = resolved.data;
14799
+ return value?.action === "import" ? this.retryOmittedRevisions(value, () => this.stateOnce(value)) : this.stateOnce(value);
13747
14800
  }
13748
14801
  stateOnce(request) {
13749
14802
  const root = this.contextRootError();
@@ -13810,44 +14863,14 @@ class WorkitCore {
13810
14863
  return imported;
13811
14864
  return this.summary(imported.data);
13812
14865
  }
13813
- if (!this.context.nativeRecovery)
13814
- return failure2("permission_denied", "native recovery authority is unavailable");
13815
- const processEvidence = (lock, writer) => this.context.nativeRecovery({
13816
- lock,
13817
- writer,
13818
- reason: input.reason,
13819
- authorityRefs: input.authorityRefs
13820
- });
13821
- if (input.target === "workspace")
13822
- return this.store.recoverWorkspace({
13823
- expectedBytes: input.expectedBytes,
13824
- snapshotDigest: input.snapshotDigest,
13825
- reason: input.reason,
13826
- authorityRefs: input.authorityRefs,
13827
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
13828
- processEvidence
13829
- });
13830
- const workspace = this.store.readWorkspace();
13831
- if (!workspace.ok)
13832
- return workspace;
13833
- if (!workspace.data)
13834
- return failure2("not_found", "workspace not found");
13835
- this.fillRevisions(input, undefined, workspace.data);
13836
- if (input.target === "workspace" && input.taskId !== undefined)
13837
- return failure2("invalid_input", "workspace recovery takes no task ID");
13838
- if (input.target === "task" && input.taskId === undefined)
13839
- return failure2("invalid_input", "task recovery requires a task ID");
13840
- return this.store.recoverTask(input.taskId, {
13841
- expectedBytes: input.expectedBytes,
13842
- snapshotDigest: input.snapshotDigest,
13843
- reason: input.reason,
13844
- authorityRefs: input.authorityRefs,
13845
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
13846
- processEvidence
13847
- });
14866
+ return failure2("invalid_input", "unknown state action");
13848
14867
  }
13849
14868
  task(request) {
13850
- return this.retryOmittedRevisions(request, () => this.taskOnce(request));
14869
+ const resolved = this.implicitTask("task", request);
14870
+ if (!resolved.ok)
14871
+ return resolved;
14872
+ const value = resolved.data;
14873
+ return this.retryOmittedRevisions(value, () => this.taskOnce(value));
13851
14874
  }
13852
14875
  taskOnce(request) {
13853
14876
  const root = this.contextRootError();
@@ -13949,7 +14972,11 @@ class WorkitCore {
13949
14972
  }
13950
14973
  }
13951
14974
  policy(request) {
13952
- return this.retryOmittedRevisions(request, () => this.policyOnce(request));
14975
+ const resolved = this.implicitTask("policy", request);
14976
+ if (!resolved.ok)
14977
+ return resolved;
14978
+ const value = resolved.data;
14979
+ return this.retryOmittedRevisions(value, () => this.policyOnce(value));
13953
14980
  }
13954
14981
  policyOnce(request) {
13955
14982
  const root = this.contextRootError();
@@ -13998,7 +15025,11 @@ class WorkitCore {
13998
15025
  return success(changed.data.revision, null, changed.data.policy);
13999
15026
  }
14000
15027
  evidence(request) {
14001
- return this.retryOmittedRevisions(request, () => this.evidenceOnce(request));
15028
+ const resolved = this.implicitTask("evidence", request);
15029
+ if (!resolved.ok)
15030
+ return resolved;
15031
+ const value = resolved.data;
15032
+ return this.retryOmittedRevisions(value, () => this.evidenceOnce(value));
14002
15033
  }
14003
15034
  evidenceOnce(request) {
14004
15035
  const root = this.contextRootError();
@@ -14139,13 +15170,16 @@ class WorkitCore {
14139
15170
  return success(changed.data.revision, null, changed.data.evidence.at(-1));
14140
15171
  }
14141
15172
  decision(request) {
14142
- return this.recordDecision(request);
15173
+ const resolved = this.implicitTask("decision", request);
15174
+ return resolved.ok ? this.recordDecision(resolved.data) : resolved;
14143
15175
  }
14144
15176
  observeDecision(request, observation) {
14145
- return this.recordDecision(request, observation, true);
15177
+ const resolved = this.implicitTask("decision", request);
15178
+ return resolved.ok ? this.recordDecision(resolved.data, observation, true) : resolved;
14146
15179
  }
14147
15180
  observeStandingDecision(request) {
14148
- return this.recordDecision(request, undefined, true, true);
15181
+ const resolved = this.implicitTask("decision", request);
15182
+ return resolved.ok ? this.recordDecision(resolved.data, undefined, true, true) : resolved;
14149
15183
  }
14150
15184
  commitTask(taskId, expected, filled, recheck, update) {
14151
15185
  let revision = expected;
@@ -14307,7 +15341,11 @@ class WorkitCore {
14307
15341
  return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
14308
15342
  }
14309
15343
  finding(request) {
14310
- return this.retryOmittedRevisions(request, () => this.findingOnce(request));
15344
+ const resolved = this.implicitTask("finding", request);
15345
+ if (!resolved.ok)
15346
+ return resolved;
15347
+ const value = resolved.data;
15348
+ return this.retryOmittedRevisions(value, () => this.findingOnce(value));
14311
15349
  }
14312
15350
  findingOnce(request) {
14313
15351
  const root = this.contextRootError();
@@ -14433,7 +15471,11 @@ class WorkitCore {
14433
15471
  return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
14434
15472
  }
14435
15473
  worker(request) {
14436
- return this.retryOmittedRevisions(request, () => this.workerOnce(request));
15474
+ const resolved = this.implicitTask("worker", request);
15475
+ if (!resolved.ok)
15476
+ return resolved;
15477
+ const value = resolved.data;
15478
+ return this.retryOmittedRevisions(value, () => this.workerOnce(value));
14437
15479
  }
14438
15480
  workerOnce(request) {
14439
15481
  const root = this.contextRootError();
@@ -14783,7 +15825,11 @@ class WorkitCore {
14783
15825
  return applied;
14784
15826
  }
14785
15827
  writer(request) {
14786
- return this.retryOmittedRevisions(request, () => this.writerOnce(request));
15828
+ const resolved = this.implicitTask("writer", request);
15829
+ if (!resolved.ok)
15830
+ return resolved;
15831
+ const value = resolved.data;
15832
+ return this.retryOmittedRevisions(value, () => this.writerOnce(value));
14787
15833
  }
14788
15834
  writerOnce(request) {
14789
15835
  const root = this.contextRootError();
@@ -15547,7 +16593,7 @@ var provenance = (context, kind = context.provenanceKind ?? "host_observed") =>
15547
16593
  return changed;
15548
16594
  const updated = changed.data.task.workers.find((candidate) => candidate.id === input.workerId);
15549
16595
  return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
15550
- }, refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] })), REVISION_RETRY_ATTEMPTS = 8, retryPause, filledRevisions = (request) => {
16596
+ }, refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] })), TASK_SCOPED_ACTIONS, CREATING_ACTIONS, REVISION_RETRY_ATTEMPTS = 8, retryPause, filledRevisions = (request) => {
15551
16597
  const value = typeof request === "object" && request !== null ? request : {};
15552
16598
  return {
15553
16599
  task: value.expectedRevision === undefined,
@@ -15776,12 +16822,46 @@ var init_task_engine = __esm(() => {
15776
16822
  init_authority();
15777
16823
  init_policy_resolver();
15778
16824
  init_auto_approval();
16825
+ init_task_store();
16826
+ init_paths();
15779
16827
  init_store_lock();
15780
16828
  init_task_context();
15781
16829
  init_workers();
15782
16830
  init_task_evaluation();
15783
16831
  verifiedWorkers = new WeakMap;
15784
16832
  dispatchReservations = new WeakMap;
16833
+ TASK_SCOPED_ACTIONS = new Set([
16834
+ "task.inspect",
16835
+ "task.revise",
16836
+ "task.progress",
16837
+ "task.pause",
16838
+ "task.resume",
16839
+ "task.close",
16840
+ "policy.assess",
16841
+ "policy.preview",
16842
+ "policy.explain",
16843
+ "evidence.record",
16844
+ "finding.record",
16845
+ "finding.resolve",
16846
+ "decision.record",
16847
+ "decision.revoke",
16848
+ "worker.assign",
16849
+ "worker.report",
16850
+ "worker.cancel",
16851
+ "writer.acquire",
16852
+ "writer.release",
16853
+ "state.export"
16854
+ ]);
16855
+ CREATING_ACTIONS = new Set([
16856
+ "task.revise",
16857
+ "task.progress",
16858
+ "policy.assess",
16859
+ "evidence.record",
16860
+ "finding.record",
16861
+ "decision.record",
16862
+ "worker.assign",
16863
+ "writer.acquire"
16864
+ ]);
15785
16865
  retryPause = new Int32Array(new SharedArrayBuffer(4));
15786
16866
  });
15787
16867
 
@@ -15812,12 +16892,12 @@ var support = (descriptor, axis) => {
15812
16892
  });
15813
16893
 
15814
16894
  // packages/workit-core/src/hooks/context.ts
15815
- import path13 from "node:path";
16895
+ import path15 from "node:path";
15816
16896
  function sessionCompactContext(store, session, context, selection = "session-bound") {
15817
16897
  const listed = store.listTaskIndex();
15818
16898
  if (!listed.ok)
15819
- return null;
15820
- const entry = currentTaskEntry(listed.data, session, selection);
16899
+ return migrationPending(listed) ? MIGRATION_PENDING_NOTE : null;
16900
+ const entry = currentTaskEntry(listed.data, session, selection) ?? implicitTaskEntry(store, listed.data, session);
15821
16901
  return entry ? entryCompactContext(store, entry, session, context) : null;
15822
16902
  }
15823
16903
  function entryCompactContext(store, entry, session, context) {
@@ -15842,7 +16922,7 @@ function entryCompactContext(store, entry, session, context) {
15842
16922
  return null;
15843
16923
  const freshness = { mode: "signal", current: () => recentSignal(store.root) };
15844
16924
  const observed = task.data.evidence.some(isObservedCheck);
15845
- const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path13.resolve(store.root, ref.path)] : []));
16925
+ const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path15.resolve(store.root, ref.path)] : []));
15846
16926
  const observedFiles = filesKey(files);
15847
16927
  const compact = new WorkitCore(store, context).compactContext(entry.id, freshness);
15848
16928
  if (!compact.ok)
@@ -15859,11 +16939,19 @@ function entryCompactContext(store, entry, session, context) {
15859
16939
  cache.delete(cache.keys().next().value);
15860
16940
  return compact.data;
15861
16941
  }
15862
- var boundTo = (entry, session) => entry.sessions.some((item) => item.host === session.host && item.handle === session.handle), newestFirst = (left, right) => right.updatedAt.localeCompare(left.updatedAt), sessionTaskEntry = (entries, session) => entries.filter((entry) => entry.status !== "closed" && boundTo(entry, session)).toSorted(newestFirst)[0] ?? null, unboundOpenTaskEntries = (entries, session, limit = 3) => entries.filter((entry) => entry.status !== "closed" && !boundTo(entry, session)).toSorted(newestFirst).slice(0, limit), currentTaskEntry = (entries, session, selection) => {
16942
+ var MIGRATION_PENDING_NOTE = "workit migration pending — run `workit task status`", migrationPending = (result) => !result.ok && result.code === "needs_input" && (result.error ?? "").startsWith(MIGRATION_PENDING), boundTo = (entry, session) => entry.sessions.some((item) => item.host === session.host && item.handle === session.handle), newestFirst = (left, right) => right.updatedAt.localeCompare(left.updatedAt), sessionTaskEntry = (entries, session) => entries.filter((entry) => entry.status !== "closed" && boundTo(entry, session)).toSorted(newestFirst)[0] ?? null, unboundOpenTaskEntries = (entries, session, limit = 3) => entries.filter((entry) => entry.status !== "closed" && !boundTo(entry, session)).toSorted(newestFirst).slice(0, limit), currentTaskEntry = (entries, session, selection) => {
15863
16943
  if (selection === "session-bound")
15864
16944
  return sessionTaskEntry(entries, session);
15865
16945
  const active = entries.filter((entry) => entry.status === "active");
15866
16946
  return active.length === 1 ? active[0] : null;
16947
+ }, implicitTaskEntry = (store, entries, session) => {
16948
+ 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)));
16949
+ if (open.length === 0)
16950
+ return null;
16951
+ const key = store.currentKey();
16952
+ if (!key.ok)
16953
+ return null;
16954
+ return open.filter((entry) => entry.key === key.data.key).toSorted(newestFirst)[0] ?? null;
15867
16955
  }, unfinishedTaskOffer = (entries, session, excludeTaskId = null) => {
15868
16956
  const tasks = unboundOpenTaskEntries(entries.filter((entry) => entry.id !== excludeTaskId), session);
15869
16957
  if (tasks.length === 0)
@@ -15899,9 +16987,15 @@ ${tasks.map((task) => `- ${task.id} [${task.status}; source ${task.source.host}/
15899
16987
  try {
15900
16988
  const store = new TaskStore(input.cwd);
15901
16989
  const listed = store.listTaskIndex();
16990
+ if (migrationPending(listed))
16991
+ return `<workit-contract>
16992
+ ${invariantBootstrap()}
16993
+ ${MIGRATION_PENDING_NOTE}${options.addendum ? `
16994
+ ${options.addendum}` : ""}
16995
+ </workit-contract>`;
15902
16996
  if (!listed.ok)
15903
16997
  throw new Error(listed.error);
15904
- const entry = currentTaskEntry(listed.data, session, descriptor.context.task);
16998
+ const entry = currentTaskEntry(listed.data, session, descriptor.context.task) ?? implicitTaskEntry(store, listed.data, session);
15905
16999
  const text = entry ? entryCompactContext(store, entry, session, hookOperationContext(input, descriptor)) : null;
15906
17000
  if (text)
15907
17001
  compact = `
@@ -15920,6 +17014,8 @@ ${options.addendum}` : ""}
15920
17014
  }, turnContextText = (input, descriptor) => {
15921
17015
  try {
15922
17016
  const text = sessionCompactContext(new TaskStore(input.cwd), { host: input.host, handle: input.session.id }, hookOperationContext(input, descriptor), descriptor.context.task);
17017
+ if (text === MIGRATION_PENDING_NOTE)
17018
+ return text;
15923
17019
  return text ? `<workit-task-context>${text}</workit-task-context>` : null;
15924
17020
  } catch {
15925
17021
  return null;
@@ -15989,7 +17085,10 @@ function handleHook(input, deps) {
15989
17085
  case "shell.pre":
15990
17086
  return usable2(descriptor.shellPolicy.deny) ? shellPolicy(input.cwd, event.command) : NONE2;
15991
17087
  case "subagent.start":
15992
- return { kind: "context", text: subagentStartText(input.host, descriptor, event) };
17088
+ return {
17089
+ kind: "context",
17090
+ text: subagentStartText(input.host, input.session.id ?? null, descriptor, event)
17091
+ };
15993
17092
  case "compact.pre":
15994
17093
  return {
15995
17094
  kind: "notice",
@@ -16003,7 +17102,14 @@ function handleHook(input, deps) {
16003
17102
  return NONE2;
16004
17103
  }
16005
17104
  }
16006
- var usable2 = (value) => value === "native" || value === "partial", NONE2, offered, CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer", 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.`, failureDecision = (descriptor, event, error) => {
17105
+ var usable2 = (value) => value === "native" || value === "partial", NONE2, offered, CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer", CLAUDE_JUDGES, subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128), subagentStartText = (host, session, descriptor, event) => {
17106
+ if (host === "claude_code" && event.agentType === CLAUDE_WORKTREE_IMPLEMENTER)
17107
+ 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.`;
17108
+ const readOnly = `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided; writer delegation is unavailable.`;
17109
+ if (host === "claude_code" && event.agentType && CLAUDE_JUDGES.has(event.agentType) && event.agentId)
17110
+ 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.`;
17111
+ return readOnly;
17112
+ }, failureDecision = (descriptor, event, error) => {
16007
17113
  if (event === "shell.pre" || event === "tool.pre")
16008
17114
  return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE2;
16009
17115
  if (event === "session.start" || event === "subagent.start")
@@ -16036,13 +17142,14 @@ var init_handle = __esm(() => {
16036
17142
  init_policy();
16037
17143
  NONE2 = { kind: "none" };
16038
17144
  offered = new Set;
17145
+ CLAUDE_JUDGES = new Set(["workit:verifier", "workit:reviewer"]);
16039
17146
  });
16040
17147
 
16041
17148
  // packages/workit-core/src/hooks/hosts/fields.ts
16042
17149
  import { existsSync as existsSync5, realpathSync as realpathSync7, statSync as statSync2 } from "node:fs";
16043
- import path14 from "node:path";
17150
+ import path16 from "node:path";
16044
17151
  var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value), nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "", optionalText = (value) => typeof value === "string" ? value : null, existingDirectory = (value) => {
16045
- if (!nonEmpty2(value) || !path14.isAbsolute(value) || !existsSync5(value))
17152
+ if (!nonEmpty2(value) || !path16.isAbsolute(value) || !existsSync5(value))
16046
17153
  return null;
16047
17154
  try {
16048
17155
  return statSync2(value).isDirectory() ? realpathSync7(value) : null;
@@ -16544,12 +17651,12 @@ var init_codex = __esm(() => {
16544
17651
  };
16545
17652
  },
16546
17653
  render: render2,
16547
- 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>`
17654
+ 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>`
16548
17655
  };
16549
17656
  });
16550
17657
 
16551
17658
  // packages/workit-core/src/hooks/hosts/cursor.ts
16552
- import path15 from "node:path";
17659
+ import path17 from "node:path";
16553
17660
  var undocumented2, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (value) => {
16554
17661
  if (!isRecord3(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
16555
17662
  return { ok: false, error: "hook_event_name is required" };
@@ -16613,7 +17720,7 @@ var undocumented2, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (val
16613
17720
  case "preCompact":
16614
17721
  return { kind: "compact.pre", trigger: "auto" };
16615
17722
  }
16616
- }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path15.sep) ? root : `${root}${path15.sep}`), hookCwd = (input) => {
17723
+ }, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path17.sep) ? root : `${root}${path17.sep}`), hookCwd = (input) => {
16617
17724
  const cwd = existingDirectory(input.cwd);
16618
17725
  if (cwd && input.hook_event_name === "beforeShellExecution")
16619
17726
  return cwd;