@brainervirus/workit-codex 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 (33) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +2 -2
  3. package/dist/launch-mcp.js +2383 -1259
  4. package/dist/workit-hook.js +2396 -1270
  5. package/package.json +3 -3
  6. package/skills/workit-bdd/SKILL.md +35 -29
  7. package/skills/workit-continue/SKILL.md +53 -0
  8. package/skills/workit-debug/SKILL.md +41 -36
  9. package/skills/workit-deslop/SKILL.md +35 -25
  10. package/skills/workit-fanout/SKILL.md +62 -0
  11. package/skills/workit-fanout/references/brief.md +56 -0
  12. package/skills/workit-implement/SKILL.md +48 -44
  13. package/skills/workit-review/SKILL.md +42 -51
  14. package/skills/workit-review/references/impact.md +24 -0
  15. package/skills/workit-shape/SKILL.md +71 -0
  16. package/skills/workit-shape/references/diagrams.md +17 -0
  17. package/skills/workit-shape/references/knowledge.md +58 -0
  18. package/skills/workit-shape/references/mockups.md +15 -0
  19. package/skills/workit-shape/references/slicing.md +42 -0
  20. package/skills/workit-ship/SKILL.md +52 -0
  21. package/skills/workit-test-audit/SKILL.md +10 -2
  22. package/skills/workit-verify-app/SKILL.md +63 -0
  23. package/skills/workit-verify-app/references/template.md +49 -0
  24. package/skills/workit-babysit/SKILL.md +0 -37
  25. package/skills/workit-behavioral-tdd/SKILL.md +0 -56
  26. package/skills/workit-blast-radius/SKILL.md +0 -26
  27. package/skills/workit-challenge/SKILL.md +0 -47
  28. package/skills/workit-diagram/SKILL.md +0 -27
  29. package/skills/workit-green-run/SKILL.md +0 -24
  30. package/skills/workit-handoff/SKILL.md +0 -37
  31. package/skills/workit-mockup/SKILL.md +0 -23
  32. package/skills/workit-plan/SKILL.md +0 -45
  33. package/skills/workit-steer/SKILL.md +0 -39
@@ -6622,12 +6622,12 @@ var require_dist = __commonJS(function(exports, module) {
6622
6622
 
6623
6623
  // packages/workit-codex/scripts/launch-mcp.ts
6624
6624
  import { existsSync as existsSync8, realpathSync as realpathSync9, statSync as statSync3 } from "node:fs";
6625
- import path18 from "node:path";
6625
+ import path20 from "node:path";
6626
6626
 
6627
6627
  // packages/workit-core/src/core.ts
6628
- import { spawnSync as spawnSync6 } from "node:child_process";
6628
+ import { spawnSync as spawnSync7 } from "node:child_process";
6629
6629
  import { existsSync as existsSync5, realpathSync as realpathSync7 } from "node:fs";
6630
- import path13 from "node:path";
6630
+ import path15 from "node:path";
6631
6631
 
6632
6632
  // packages/workit-core/src/core/task-contract.ts
6633
6633
  import { createHash, randomUUID } from "node:crypto";
@@ -15249,16 +15249,6 @@ var stateOperations = {
15249
15249
  expectedWorkspaceRevision: revision.nullable().optional(),
15250
15250
  bundle: exportBundleSchema,
15251
15251
  authorityRefs: array(refSchema)
15252
- }),
15253
- recover: operation({
15254
- action: literal("recover"),
15255
- taskId: id.optional(),
15256
- expectedWorkspaceRevision: revision.optional(),
15257
- target: _enum(["task", "workspace"]),
15258
- expectedBytes: digest,
15259
- snapshotDigest: digest,
15260
- reason: text,
15261
- authorityRefs: array(refSchema)
15262
15252
  })
15263
15253
  };
15264
15254
  var operationSchemas = {
@@ -15271,10 +15261,17 @@ var operationSchemas = {
15271
15261
  writer: discriminatedUnion("action", Object.values(writerOperations)),
15272
15262
  state: discriminatedUnion("action", Object.values(stateOperations))
15273
15263
  };
15274
- var { recover: _unadvertisedRecover, ...advertisedStateOperations } = stateOperations;
15264
+ var withImplicitTask = (options) => Object.values(options).map((option) => ("taskId" in option.shape) ? option.extend({ taskId: id.optional() }) : option);
15265
+ var advertised = (options) => discriminatedUnion("action", withImplicitTask(options));
15275
15266
  var advertisedOperationSchemas = {
15276
- ...operationSchemas,
15277
- state: discriminatedUnion("action", Object.values(advertisedStateOperations))
15267
+ task: advertised(taskOperations),
15268
+ policy: advertised(policyOperations),
15269
+ evidence: advertised(evidenceOperations),
15270
+ finding: advertised(findingOperations),
15271
+ decision: advertised(decisionOperations),
15272
+ worker: advertised(workerOperations),
15273
+ writer: advertised(writerOperations),
15274
+ state: advertised(stateOperations)
15278
15275
  };
15279
15276
  var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
15280
15277
  var success = (revisionValue, workspaceRevisionValue, data) => ({
@@ -15489,29 +15486,10 @@ function requirementId(input) {
15489
15486
  });
15490
15487
  }
15491
15488
  // packages/workit-core/src/core/task-store.ts
15492
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
15493
15489
  import { AsyncLocalStorage } from "node:async_hooks";
15494
- import * as fs5 from "node:fs";
15495
- import path4 from "node:path";
15496
-
15497
- // packages/workit-core/src/core/package-root.ts
15498
- import { existsSync } from "node:fs";
15499
- import path from "node:path";
15500
- import { fileURLToPath } from "node:url";
15501
- var packageRoot = () => {
15502
- let dir = path.dirname(fileURLToPath(import.meta.url));
15503
- while (true) {
15504
- const parent = path.dirname(dir);
15505
- if (existsSync(path.join(dir, "package.json")) || parent === dir)
15506
- return dir;
15507
- dir = parent;
15508
- }
15509
- };
15510
- var assetRoot = () => {
15511
- const root = packageRoot();
15512
- const assets = path.join(root, "assets");
15513
- return existsSync(assets) ? assets : root;
15514
- };
15490
+ import { createHash as createHash4, randomUUID as randomUUID3 } from "node:crypto";
15491
+ import * as fs7 from "node:fs";
15492
+ import path6 from "node:path";
15515
15493
 
15516
15494
  // node_modules/@openclaw/fs-safe/dist/errors.js
15517
15495
  var OPERATIONAL_CODES = new Set([
@@ -15562,7 +15540,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
15562
15540
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
15563
15541
  import { randomBytes } from "node:crypto";
15564
15542
  import fsSync from "node:fs";
15565
- import path2 from "node:path";
15543
+ import path from "node:path";
15566
15544
 
15567
15545
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
15568
15546
  import fs from "node:fs";
@@ -15652,13 +15630,13 @@ ${ownershipToken}
15652
15630
  return { raw, ownershipToken };
15653
15631
  }
15654
15632
  function relativeSidecarLockPath(lockRoot, lockPath) {
15655
- const resolved = path2.resolve(lockPath);
15656
- const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
15657
- const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
15658
- if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
15633
+ const resolved = path.resolve(lockPath);
15634
+ const lexicalRelative = path.relative(lockRoot.rootDir, resolved);
15635
+ const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path.sep}`) && !path.isAbsolute(lexicalRelative) ? lexicalRelative : path.relative(lockRoot.rootReal, resolved);
15636
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
15659
15637
  throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
15660
15638
  }
15661
- return relative.split(path2.sep).join(path2.posix.sep);
15639
+ return relative.split(path.sep).join(path.posix.sep);
15662
15640
  }
15663
15641
  function parseSidecarLockPayload(raw, parser) {
15664
15642
  if (parser) {
@@ -15805,7 +15783,7 @@ function getFsSafeLockConfig() {
15805
15783
 
15806
15784
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
15807
15785
  import fs3 from "node:fs";
15808
- import path3 from "node:path";
15786
+ import path2 from "node:path";
15809
15787
 
15810
15788
  // node_modules/@openclaw/fs-safe/dist/timing.js
15811
15789
  function sleepSync(ms) {
@@ -15896,26 +15874,26 @@ function canonicalLockParentSync(parent) {
15896
15874
  return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
15897
15875
  }
15898
15876
  function normalizeTargetPath(targetPath) {
15899
- const resolved = path3.resolve(targetPath);
15900
- fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
15877
+ const resolved = path2.resolve(targetPath);
15878
+ fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
15901
15879
  try {
15902
- return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
15880
+ return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
15903
15881
  } catch {
15904
15882
  return resolved;
15905
15883
  }
15906
15884
  }
15907
15885
  function boundedLockPath(lockPath, lockRoot) {
15908
- const resolved = path3.resolve(lockPath);
15886
+ const resolved = path2.resolve(lockPath);
15909
15887
  if (!lockRoot)
15910
15888
  return resolved;
15911
15889
  relativeSidecarLockPath(lockRoot, resolved);
15912
- const parent = path3.dirname(resolved);
15890
+ const parent = path2.dirname(resolved);
15913
15891
  const parentReal = canonicalLockParentSync(parent);
15914
- const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
15915
- if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
15892
+ const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
15893
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
15916
15894
  throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
15917
15895
  }
15918
- return path3.join(parentReal, path3.basename(resolved));
15896
+ return path2.join(parentReal, path2.basename(resolved));
15919
15897
  }
15920
15898
  function defaultShouldReclaim(snapshot, staleMs, nowMs) {
15921
15899
  const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
@@ -16114,10 +16092,311 @@ function acquireFileLockSync(targetPath, options) {
16114
16092
  }
16115
16093
  }
16116
16094
  }
16095
+ // packages/workit-core/src/core/package-root.ts
16096
+ import { existsSync } from "node:fs";
16097
+ import path3 from "node:path";
16098
+ import { fileURLToPath } from "node:url";
16099
+ var packageRoot = () => {
16100
+ let dir = path3.dirname(fileURLToPath(import.meta.url));
16101
+ while (true) {
16102
+ const parent = path3.dirname(dir);
16103
+ if (existsSync(path3.join(dir, "package.json")) || parent === dir)
16104
+ return dir;
16105
+ dir = parent;
16106
+ }
16107
+ };
16108
+ var assetRoot = () => {
16109
+ const root = packageRoot();
16110
+ const assets = path3.join(root, "assets");
16111
+ return existsSync(assets) ? assets : root;
16112
+ };
16113
+
16117
16114
  // packages/workit-core/src/core/store-lock.ts
16115
+ import { spawnSync as spawnSync2 } from "node:child_process";
16116
+ import fs5 from "node:fs";
16117
+ import { hostname } from "node:os";
16118
+
16119
+ // packages/workit-core/src/store/paths.ts
16118
16120
  import { spawnSync } from "node:child_process";
16121
+ import { createHash as createHash2 } from "node:crypto";
16119
16122
  import fs4 from "node:fs";
16120
- import { hostname } from "node:os";
16123
+ import path4 from "node:path";
16124
+ var NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
16125
+ var gitRun = (cwd, args) => {
16126
+ const run = spawnSync("git", args, {
16127
+ cwd,
16128
+ encoding: "utf8",
16129
+ timeout: 20000,
16130
+ killSignal: "SIGKILL",
16131
+ windowsHide: true,
16132
+ env: {
16133
+ ...process.env,
16134
+ GIT_TERMINAL_PROMPT: "0",
16135
+ GIT_OPTIONAL_LOCKS: "0",
16136
+ LC_ALL: "C",
16137
+ LANGUAGE: "C"
16138
+ }
16139
+ });
16140
+ const error = run.error;
16141
+ return {
16142
+ ok: run.status === 0,
16143
+ stdout: run.stdout ?? "",
16144
+ stderr: error ? String(error.message) : run.stderr ?? "",
16145
+ missing: error?.code === "ENOENT"
16146
+ };
16147
+ };
16148
+ var shortHash = (value) => createHash2("sha256").update(value).digest("hex").slice(0, 12);
16149
+ var real = (file) => {
16150
+ try {
16151
+ return fs4.realpathSync.native(file);
16152
+ } catch {
16153
+ return file;
16154
+ }
16155
+ };
16156
+ var DISCOVERY_ENV = [
16157
+ "GIT_DIR",
16158
+ "GIT_COMMON_DIR",
16159
+ "GIT_WORK_TREE",
16160
+ "GIT_CEILING_DIRECTORIES",
16161
+ "GIT_DISCOVERY_ACROSS_FILESYSTEM"
16162
+ ];
16163
+ var isGitDir = (dir) => {
16164
+ try {
16165
+ return fs4.statSync(path4.join(dir, "HEAD")).isFile() && fs4.statSync(path4.join(dir, "objects")).isDirectory() && fs4.statSync(path4.join(dir, "refs")).isDirectory();
16166
+ } catch {
16167
+ return false;
16168
+ }
16169
+ };
16170
+ var reftableRefs = (common) => {
16171
+ try {
16172
+ return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs4.readFileSync(path4.join(common, "config"), "utf8"));
16173
+ } catch {
16174
+ return false;
16175
+ }
16176
+ };
16177
+ function fastGit(root) {
16178
+ if (DISCOVERY_ENV.some((name) => process.env[name]))
16179
+ return null;
16180
+ let dir = real(root);
16181
+ let device = null;
16182
+ for (;; ) {
16183
+ let stat;
16184
+ try {
16185
+ stat = fs4.statSync(dir);
16186
+ } catch {
16187
+ return null;
16188
+ }
16189
+ if (device !== null && stat.dev !== device)
16190
+ return { kind: "none" };
16191
+ device = stat.dev;
16192
+ const dotGit = path4.join(dir, ".git");
16193
+ let entry = null;
16194
+ try {
16195
+ entry = fs4.statSync(dotGit);
16196
+ } catch {}
16197
+ let gitDir = null;
16198
+ let top = null;
16199
+ if (entry) {
16200
+ top = dir;
16201
+ if (entry.isDirectory())
16202
+ gitDir = dotGit;
16203
+ else if (entry.isFile()) {
16204
+ let text = "";
16205
+ try {
16206
+ text = fs4.readFileSync(dotGit, "utf8");
16207
+ } catch {
16208
+ return null;
16209
+ }
16210
+ const match = /^gitdir: (.+)$/mu.exec(text);
16211
+ if (!match)
16212
+ return null;
16213
+ gitDir = path4.resolve(dir, match[1].trim());
16214
+ } else
16215
+ return null;
16216
+ if (!fs4.existsSync(path4.join(gitDir, "HEAD")))
16217
+ return null;
16218
+ } else if (isGitDir(dir))
16219
+ gitDir = dir;
16220
+ if (gitDir) {
16221
+ let common = gitDir;
16222
+ try {
16223
+ common = path4.resolve(gitDir, fs4.readFileSync(path4.join(gitDir, "commondir"), "utf8").trim());
16224
+ } catch {}
16225
+ if (!fs4.existsSync(path4.join(common, "objects")))
16226
+ return null;
16227
+ return {
16228
+ kind: "repo",
16229
+ common: real(common),
16230
+ gitDir: real(gitDir),
16231
+ top: top === null ? null : real(top),
16232
+ reftable: reftableRefs(common)
16233
+ };
16234
+ }
16235
+ const parent = path4.dirname(dir);
16236
+ if (parent === dir)
16237
+ return { kind: "none" };
16238
+ dir = parent;
16239
+ }
16240
+ }
16241
+ function resolveStore(root) {
16242
+ root = path4.resolve(root);
16243
+ let directory = false;
16244
+ try {
16245
+ directory = fs4.statSync(root).isDirectory();
16246
+ } catch {}
16247
+ if (!directory)
16248
+ return { dir: path4.join(root, ".workit"), shared: false, top: null };
16249
+ const fast = fastGit(root);
16250
+ if (fast?.kind === "none")
16251
+ return { dir: path4.join(root, ".workit"), shared: false, top: null };
16252
+ if (fast?.kind === "repo")
16253
+ return {
16254
+ dir: path4.join(fast.common, "workit"),
16255
+ shared: true,
16256
+ top: fast.top,
16257
+ git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
16258
+ };
16259
+ const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
16260
+ const common = run.stdout.trim();
16261
+ if (run.ok && common) {
16262
+ const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
16263
+ return {
16264
+ dir: path4.join(real(path4.resolve(root, common)), "workit"),
16265
+ shared: true,
16266
+ top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
16267
+ };
16268
+ }
16269
+ if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
16270
+ return { dir: path4.join(root, ".workit"), shared: false, top: null };
16271
+ return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
16272
+ `)[0] || "no output"}); fix the repository (git status should work here)`);
16273
+ }
16274
+ var rebasingBranch = (gitDir) => {
16275
+ for (const state of ["rebase-merge", "rebase-apply"]) {
16276
+ try {
16277
+ const ref = fs4.readFileSync(path4.join(gitDir, state, "head-name"), "utf8").trim();
16278
+ const match = /^refs\/heads\/(.+)$/u.exec(ref);
16279
+ if (match)
16280
+ return match[1];
16281
+ } catch {}
16282
+ }
16283
+ return null;
16284
+ };
16285
+ function resolveTaskKey(root, location) {
16286
+ if (!location.shared)
16287
+ return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
16288
+ const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
16289
+ if (location.git && !location.git.reftable) {
16290
+ let head = "";
16291
+ try {
16292
+ head = fs4.readFileSync(path4.join(location.git.gitDir, "HEAD"), "utf8").trim();
16293
+ } catch {}
16294
+ const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
16295
+ if (ref && ref[1] !== ".invalid")
16296
+ return branchKey(ref[1]);
16297
+ if (/^[0-9a-f]{40,64}$/u.test(head)) {
16298
+ const rebasing = rebasingBranch(location.git.gitDir);
16299
+ if (rebasing)
16300
+ return branchKey(rebasing);
16301
+ return {
16302
+ key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
16303
+ kind: "detached",
16304
+ branch: null
16305
+ };
16306
+ }
16307
+ }
16308
+ const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
16309
+ const name = branch.ok ? branch.stdout.trim() : "";
16310
+ if (name && name !== ".invalid")
16311
+ return branchKey(name);
16312
+ const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
16313
+ if (gitDir.ok && gitDir.stdout.trim()) {
16314
+ const rebasing = rebasingBranch(gitDir.stdout.trim());
16315
+ if (rebasing)
16316
+ return branchKey(rebasing);
16317
+ }
16318
+ const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
16319
+ const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
16320
+ return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
16321
+ }
16322
+ function checkoutRootOf(root, location) {
16323
+ const resolved = path4.resolve(root);
16324
+ let canonical = resolved;
16325
+ try {
16326
+ canonical = fs4.realpathSync(resolved);
16327
+ } catch {
16328
+ return resolved;
16329
+ }
16330
+ const found = location ?? resolveStore(canonical);
16331
+ if (found instanceof Error || !found.shared || !found.top)
16332
+ return canonical;
16333
+ if (real(canonical) === found.top)
16334
+ return canonical;
16335
+ try {
16336
+ return fs4.realpathSync(found.top);
16337
+ } catch {
16338
+ return canonical;
16339
+ }
16340
+ }
16341
+ function branchRenames(location) {
16342
+ if (!location.shared)
16343
+ return [];
16344
+ const files = [];
16345
+ const walk = (dir) => {
16346
+ let entries;
16347
+ try {
16348
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
16349
+ } catch {
16350
+ return;
16351
+ }
16352
+ for (const entry of entries)
16353
+ if (entry.isDirectory())
16354
+ walk(path4.join(dir, entry.name));
16355
+ else
16356
+ files.push(path4.join(dir, entry.name));
16357
+ };
16358
+ walk(path4.join(path4.dirname(location.dir), "logs", "refs", "heads"));
16359
+ if (location.git)
16360
+ files.push(path4.join(location.git.gitDir, "logs", "HEAD"));
16361
+ const seen = new Set;
16362
+ const renames = [];
16363
+ for (const file of files) {
16364
+ let text = "";
16365
+ try {
16366
+ text = fs4.readFileSync(file, "utf8");
16367
+ } catch {
16368
+ continue;
16369
+ }
16370
+ for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
16371
+ const rename = { from: match[2], to: match[3], at: Number(match[1]) };
16372
+ const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
16373
+ if (seen.has(id))
16374
+ continue;
16375
+ seen.add(id);
16376
+ renames.push(rename);
16377
+ }
16378
+ }
16379
+ return renames.toSorted((left, right) => left.at - right.at);
16380
+ }
16381
+ function followRenames(key, boundAt, renames) {
16382
+ let current = key;
16383
+ let since = Math.floor(Date.parse(boundAt) / 1000);
16384
+ for (const rename of renames)
16385
+ if (rename.from === current && rename.at >= since) {
16386
+ current = rename.to;
16387
+ since = rename.at;
16388
+ }
16389
+ return current;
16390
+ }
16391
+ var checkoutSlug = (root) => {
16392
+ let canonical = real(root);
16393
+ if (process.platform === "win32")
16394
+ canonical = canonical.toLowerCase();
16395
+ const base = path4.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
16396
+ return `${base}-${shortHash(canonical)}`;
16397
+ };
16398
+
16399
+ // packages/workit-core/src/core/store-lock.ts
16121
16400
  var metadataLockSchema = object({
16122
16401
  pid: number2().int().nonnegative().safe(),
16123
16402
  processStart: string2().nullable(),
@@ -16149,10 +16428,6 @@ var parseMetadataLockOrNull = (raw) => {
16149
16428
  return null;
16150
16429
  }
16151
16430
  };
16152
- var sameMetadataLock = (left, right) => {
16153
- const parsed = metadataLockSchema.safeParse(left);
16154
- return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
16155
- };
16156
16431
  var parseProcStatStart = (stat) => {
16157
16432
  const close = stat.lastIndexOf(")");
16158
16433
  if (close < 0)
@@ -16162,14 +16437,14 @@ var parseProcStatStart = (stat) => {
16162
16437
  var processStartOf = (pid) => {
16163
16438
  if (process.platform === "linux") {
16164
16439
  try {
16165
- return parseProcStatStart(fs4.readFileSync(`/proc/${pid}/stat`, "utf8"));
16440
+ return parseProcStatStart(fs5.readFileSync(`/proc/${pid}/stat`, "utf8"));
16166
16441
  } catch {
16167
16442
  return null;
16168
16443
  }
16169
16444
  }
16170
16445
  if (process.platform === "darwin" || process.platform === "freebsd") {
16171
16446
  try {
16172
- const run = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
16447
+ const run = spawnSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
16173
16448
  encoding: "utf8",
16174
16449
  timeout: 1000
16175
16450
  });
@@ -16183,7 +16458,7 @@ var processStartOf = (pid) => {
16183
16458
  };
16184
16459
  var readTrimmed = (file) => {
16185
16460
  try {
16186
- return fs4.readFileSync(file, "utf8").trim() || null;
16461
+ return fs5.readFileSync(file, "utf8").trim() || null;
16187
16462
  } catch {
16188
16463
  return null;
16189
16464
  }
@@ -16194,7 +16469,7 @@ var localLockHost = () => {
16194
16469
  return cachedLockHost;
16195
16470
  let pidns = null;
16196
16471
  try {
16197
- pidns = /\[(\d+)\]/.exec(fs4.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
16472
+ pidns = /\[(\d+)\]/.exec(fs5.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
16198
16473
  } catch {}
16199
16474
  const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
16200
16475
  cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
@@ -16234,7 +16509,7 @@ var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
16234
16509
  };
16235
16510
  var ageOf = (file, nowMs) => {
16236
16511
  try {
16237
- return nowMs - fs4.lstatSync(file).mtimeMs;
16512
+ return nowMs - fs5.lstatSync(file).mtimeMs;
16238
16513
  } catch {
16239
16514
  return null;
16240
16515
  }
@@ -16245,117 +16520,151 @@ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
16245
16520
  if (age === null || age <= RECLAIM_GUARD_TTL_MS)
16246
16521
  return false;
16247
16522
  try {
16248
- fs4.rmdirSync(guard);
16523
+ fs5.rmdirSync(guard);
16249
16524
  return true;
16250
16525
  } catch {
16251
16526
  return false;
16252
16527
  }
16253
16528
  };
16254
16529
 
16255
- // packages/workit-core/src/core/task-store.ts
16256
- var RECOVERY_COPIES_PER_RECORD = 3;
16257
- var STALE_TEMPORARY_MS = 60 * 60000;
16258
- var RECOVERY_NAME = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/;
16259
- var processEvidenceSchema = object({
16260
- state: _enum(["stopped", "accounted_for"]),
16261
- pid: number2().int().nonnegative(),
16262
- processStart: string2().nullable(),
16263
- ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
16264
- }).strict();
16265
- var INDEX_VERSION = 1;
16266
- var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
16267
- var indexEntry = (task, file) => {
16268
- const sessions = [];
16269
- const intent = hostSession(task.intent.provenance.session);
16270
- if (intent)
16271
- sessions.push({ ...intent, workerId: null });
16272
- for (const worker of task.workers) {
16273
- const session = hostSession(worker.data.session);
16274
- if (session)
16275
- sessions.push({ ...session, workerId: worker.id });
16530
+ // packages/workit-core/src/store/event-log.ts
16531
+ import { randomUUID as randomUUID2 } from "node:crypto";
16532
+ import fs6 from "node:fs";
16533
+ import path5 from "node:path";
16534
+ var EVENT_VERSION = 1;
16535
+
16536
+ class LogDamage extends Error {
16537
+ upgrade;
16538
+ constructor(message, upgrade = false) {
16539
+ super(message);
16540
+ this.upgrade = upgrade;
16276
16541
  }
16277
- return {
16278
- id: task.id,
16279
- revision: task.revision,
16280
- status: task.status,
16281
- createdAt: task.createdAt,
16282
- updatedAt: task.updatedAt,
16283
- objective: task.intent.data.objective,
16284
- source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
16285
- progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
16286
- sessions,
16287
- file
16288
- };
16289
- };
16290
- var fileSignature = (file) => {
16542
+ }
16543
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
16544
+ var parseEvent = (line, file, offset) => {
16545
+ let value;
16291
16546
  try {
16292
- const stat = fs5.statSync(file, { bigint: true });
16293
- return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
16547
+ value = JSON.parse(line);
16294
16548
  } catch {
16295
- return null;
16296
- }
16297
- };
16298
- var RACY_WINDOW_NS = 2000000000n;
16299
- var racySignature = (signature) => {
16300
- const [mtime = 0n, ctime = 0n] = signature.split(":").slice(3, 5).map(BigInt);
16301
- const changed = mtime > ctime ? mtime : ctime;
16302
- return BigInt(Date.now()) * 1000000n - changed < RACY_WINDOW_NS;
16549
+ throw new LogDamage(`${file}: unreadable event at byte ${offset}`);
16550
+ }
16551
+ if (!isObject2(value) || typeof value.v !== "number")
16552
+ throw new LogDamage(`${file}: malformed event at byte ${offset}`);
16553
+ if (value.v > EVENT_VERSION)
16554
+ throw new LogDamage(`${file}: event format v${value.v} was written by a newer Workit; upgrade Workit to read this task`, true);
16555
+ if (typeof value.seq !== "number" || !Number.isInteger(value.seq) || typeof value.type !== "string" || typeof value.id !== "string" || typeof value.task !== "string" || !isObject2(value.data))
16556
+ throw new LogDamage(`${file}: malformed event at byte ${offset}`);
16557
+ return value;
16303
16558
  };
16304
- var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
16305
- var cachedRuntimeVersion = null;
16306
- var runtimeVersion = () => {
16307
- if (cachedRuntimeVersion === null) {
16559
+ function readLog(file, from = 0, expectSeq = null) {
16560
+ let fd;
16561
+ try {
16562
+ fd = fs6.openSync(file, "r");
16563
+ } catch (error) {
16564
+ if (error.code === "ENOENT")
16565
+ return null;
16566
+ throw error;
16567
+ }
16568
+ let bytes;
16569
+ let size;
16570
+ try {
16571
+ size = fs6.fstatSync(fd).size;
16572
+ if (from > size)
16573
+ throw new LogDamage(`${file}: shorter than its snapshot`);
16574
+ bytes = Buffer.alloc(size - from);
16575
+ let read = 0;
16576
+ while (read < bytes.length) {
16577
+ const got = fs6.readSync(fd, bytes, read, bytes.length - read, from + read);
16578
+ if (got === 0)
16579
+ break;
16580
+ read += got;
16581
+ }
16582
+ bytes = bytes.subarray(0, read);
16583
+ size = from + read;
16584
+ } finally {
16585
+ fs6.closeSync(fd);
16586
+ }
16587
+ const events = [];
16588
+ let start = 0;
16589
+ let lastStart = from;
16590
+ let seq = expectSeq;
16591
+ for (;; ) {
16592
+ const newline = bytes.indexOf(10, start);
16593
+ if (newline < 0)
16594
+ break;
16595
+ const line = bytes.subarray(start, newline).toString("utf8");
16596
+ if (line.trim() !== "") {
16597
+ const event = parseEvent(line, file, from + start);
16598
+ if (seq !== null && event.seq !== seq)
16599
+ throw new LogDamage(`${file}: event seq ${event.seq} where ${seq} was expected`);
16600
+ seq = event.seq + 1;
16601
+ events.push(event);
16602
+ lastStart = from + start;
16603
+ }
16604
+ start = newline + 1;
16605
+ }
16606
+ return { events, end: from + start, lastStart, size };
16607
+ }
16608
+ var fsyncDirectory = (dir) => {
16609
+ try {
16610
+ const fd = fs6.openSync(dir, "r");
16308
16611
  try {
16309
- const pkg = JSON.parse(fs5.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
16310
- cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
16311
- } catch {
16312
- cachedRuntimeVersion = "0.0.0";
16612
+ fs6.fsyncSync(fd);
16613
+ } finally {
16614
+ fs6.closeSync(fd);
16313
16615
  }
16616
+ } catch (error) {
16617
+ if (process.platform !== "win32")
16618
+ throw error;
16314
16619
  }
16315
- return cachedRuntimeVersion;
16316
16620
  };
16317
- var stampNew = (record) => ({
16318
- createdWith: record.runtime?.createdWith ?? null,
16319
- updatedWith: runtimeVersion()
16320
- });
16321
- var isNewerVersion = (candidate, current) => {
16322
- const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
16323
- const [candidateParts, currentParts] = [parts(candidate), parts(current)];
16324
- for (let index = 0;index < 3; index += 1) {
16325
- const left = candidateParts[index] ?? 0;
16326
- const right = currentParts[index] ?? 0;
16327
- if (left !== right)
16328
- return left > right;
16621
+ function appendEvent(file, event, validEnd) {
16622
+ const line = Buffer.from(`${JSON.stringify(event)}
16623
+ `, "utf8");
16624
+ const created = !fs6.existsSync(file);
16625
+ if (!created && fs6.statSync(file).size !== validEnd) {
16626
+ const cut = fs6.openSync(file, "r+");
16627
+ try {
16628
+ fs6.ftruncateSync(cut, validEnd);
16629
+ fs6.fsyncSync(cut);
16630
+ } finally {
16631
+ fs6.closeSync(cut);
16632
+ }
16329
16633
  }
16330
- return false;
16331
- };
16332
- var jsonBytes = (value) => `${canonicalJson(value)}
16333
- `;
16334
- var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
16335
- var isObject2 = (value) => typeof value === "object" && value !== null;
16336
- var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
16337
- var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
16338
- var sameDirectoryIdentity = (left, right) => {
16339
- if (!path4.isAbsolute(left) || !path4.isAbsolute(right))
16340
- return false;
16341
- const normalizedLeft = path4.resolve(left);
16342
- const normalizedRight = path4.resolve(right);
16634
+ const fd = fs6.openSync(file, "a", 384);
16343
16635
  try {
16344
- const a = fs5.statSync(normalizedLeft, { bigint: true });
16345
- const b = fs5.statSync(normalizedRight, { bigint: true });
16346
- if (!a.isDirectory() || !b.isDirectory())
16347
- return false;
16348
- if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
16349
- return true;
16350
- const canonicalLeft = fs5.realpathSync(normalizedLeft);
16351
- const canonicalRight = fs5.realpathSync(normalizedRight);
16352
- if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
16353
- return true;
16354
- return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
16355
- } catch {
16356
- return false;
16636
+ let written = 0;
16637
+ while (written < line.length)
16638
+ written += fs6.writeSync(fd, line, written, line.length - written);
16639
+ fs6.fsyncSync(fd);
16640
+ } finally {
16641
+ fs6.closeSync(fd);
16357
16642
  }
16358
- };
16643
+ if (created)
16644
+ fsyncDirectory(path5.dirname(file));
16645
+ return validEnd;
16646
+ }
16647
+ function replaceFile(file, bytes, durable) {
16648
+ const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
16649
+ try {
16650
+ const fd = fs6.openSync(temporary, "wx", 384);
16651
+ try {
16652
+ fs6.writeSync(fd, bytes);
16653
+ if (durable)
16654
+ fs6.fsyncSync(fd);
16655
+ } finally {
16656
+ fs6.closeSync(fd);
16657
+ }
16658
+ renameWithRetry(temporary, file);
16659
+ if (durable)
16660
+ fsyncDirectory(path5.dirname(file));
16661
+ } catch (error) {
16662
+ try {
16663
+ fs6.unlinkSync(temporary);
16664
+ } catch {}
16665
+ throw error;
16666
+ }
16667
+ }
16359
16668
  var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
16360
16669
  var isTransientWindowsError = (error) => {
16361
16670
  if (process.platform !== "win32")
@@ -16376,138 +16685,596 @@ var retryTransient = (run) => {
16376
16685
  }
16377
16686
  }
16378
16687
  };
16688
+ var renameWithRetry = (from, to) => retryTransient(() => fs6.renameSync(from, to));
16689
+
16690
+ // packages/workit-core/src/store/patch.ts
16691
+ var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
16692
+ var equal = (left, right) => {
16693
+ if (left === right)
16694
+ return true;
16695
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null)
16696
+ return false;
16697
+ if (Array.isArray(left) !== Array.isArray(right))
16698
+ return false;
16699
+ if (Array.isArray(left)) {
16700
+ const other = right;
16701
+ return left.length === other.length && left.every((item, index) => equal(item, other[index]));
16702
+ }
16703
+ const leftKeys = Object.keys(left).filter((key) => left[key] !== undefined);
16704
+ const rightKeys = Object.keys(right).filter((key) => right[key] !== undefined);
16705
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => equal(left[key], right[key]));
16706
+ };
16707
+ function diff(before, after, path = []) {
16708
+ if (equal(before, after))
16709
+ return [];
16710
+ if (isPlainObject2(before) && isPlainObject2(after)) {
16711
+ const ops = [];
16712
+ for (const key of Object.keys(before))
16713
+ if (before[key] !== undefined && after[key] === undefined)
16714
+ ops.push({ op: "del", path: [...path, key] });
16715
+ for (const key of Object.keys(after)) {
16716
+ if (after[key] === undefined)
16717
+ continue;
16718
+ if (before[key] === undefined)
16719
+ ops.push({ op: "set", path: [...path, key], value: after[key] });
16720
+ else
16721
+ ops.push(...diff(before[key], after[key], [...path, key]));
16722
+ }
16723
+ return ops;
16724
+ }
16725
+ if (Array.isArray(before) && Array.isArray(after)) {
16726
+ const ops = [];
16727
+ if (after.length < before.length)
16728
+ ops.push({ op: "cut", path, length: after.length });
16729
+ const shared = Math.min(before.length, after.length);
16730
+ let changed = 0;
16731
+ for (let index = 0;index < shared; index += 1)
16732
+ if (!equal(before[index], after[index]))
16733
+ changed += 1;
16734
+ if (shared > 0 && changed > shared / 2 && changed > 4)
16735
+ return [{ op: "set", path, value: after }];
16736
+ for (let index = 0;index < shared; index += 1)
16737
+ ops.push(...diff(before[index], after[index], [...path, index]));
16738
+ if (after.length > before.length)
16739
+ ops.push({ op: "push", path, values: after.slice(before.length) });
16740
+ return ops;
16741
+ }
16742
+ return [{ op: "set", path, value: after }];
16743
+ }
16744
+ var clone2 = (value) => structuredClone(value);
16745
+ function apply(target, ops) {
16746
+ let root = target;
16747
+ for (const op of ops) {
16748
+ if (op.path.length === 0) {
16749
+ if (op.op === "set")
16750
+ root = clone2(op.value);
16751
+ else if (op.op === "push" && Array.isArray(root))
16752
+ root.push(...clone2(op.values));
16753
+ else if (op.op === "cut" && Array.isArray(root))
16754
+ root.length = op.length;
16755
+ else
16756
+ throw new Error(`patch: ${op.op} at the root is invalid`);
16757
+ continue;
16758
+ }
16759
+ let parent = root;
16760
+ for (const key of op.path.slice(0, -1)) {
16761
+ if (typeof parent !== "object" || parent === null)
16762
+ throw new Error(`patch: missing path ${op.path.join(".")}`);
16763
+ parent = parent[key];
16764
+ }
16765
+ if (typeof parent !== "object" || parent === null)
16766
+ throw new Error(`patch: missing path ${op.path.join(".")}`);
16767
+ const key = op.path.at(-1);
16768
+ const container = parent;
16769
+ if (op.op === "set")
16770
+ container[key] = clone2(op.value);
16771
+ else if (op.op === "del")
16772
+ delete container[key];
16773
+ else {
16774
+ const array = container[key];
16775
+ if (!Array.isArray(array))
16776
+ throw new Error(`patch: ${op.path.join(".")} is not an array`);
16777
+ if (op.op === "push")
16778
+ array.push(...clone2(op.values));
16779
+ else
16780
+ array.length = op.length;
16781
+ }
16782
+ }
16783
+ return root;
16784
+ }
16785
+ var isPath = (value) => Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isInteger(key) && key >= 0);
16786
+ var isPatch = (value) => Array.isArray(value) && value.every((op) => {
16787
+ if (!isPlainObject2(op) || !isPath(op.path))
16788
+ return false;
16789
+ if (op.op === "set")
16790
+ return "value" in op;
16791
+ if (op.op === "del")
16792
+ return op.path.length > 0;
16793
+ if (op.op === "push")
16794
+ return Array.isArray(op.values);
16795
+ if (op.op === "cut")
16796
+ return typeof op.length === "number" && Number.isInteger(op.length) && op.length >= 0;
16797
+ return false;
16798
+ });
16799
+
16800
+ // packages/workit-core/src/store/reduce.ts
16801
+ import { createHash as createHash3 } from "node:crypto";
16802
+ var OPENING_TYPES = new Set([
16803
+ "task.opened",
16804
+ "task.imported",
16805
+ "migrated.from_v2",
16806
+ "task.checkpoint"
16807
+ ]);
16808
+ var KNOWN_TYPES = new Set([
16809
+ ...OPENING_TYPES,
16810
+ "task.bound",
16811
+ "task.noted",
16812
+ "task.revised",
16813
+ "task.judged",
16814
+ "task.paused",
16815
+ "task.resumed",
16816
+ "task.closed",
16817
+ "task.updated",
16818
+ "check.ran",
16819
+ "evidence.recorded",
16820
+ "finding.recorded",
16821
+ "finding.updated",
16822
+ "decision.recorded",
16823
+ "decision.updated",
16824
+ "worker.updated"
16825
+ ]);
16826
+ var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
16827
+ var isKey = (value) => isObject3(value) && typeof value.key === "string" && (value.kind === "branch" || value.kind === "detached" || value.kind === "dir") && (value.branch === null || typeof value.branch === "string");
16828
+ var BLOB_REF = "$blob";
16829
+ var blobRef = (value) => isObject3(value) && Object.keys(value).length === 1 && typeof value[BLOB_REF] === "string" && /^[0-9a-f]{64}$/.test(value[BLOB_REF]) ? value[BLOB_REF] : null;
16830
+ var blobDigest = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
16831
+ var toBlob = (value, blobs) => {
16832
+ if (!isObject3(value))
16833
+ return value;
16834
+ const digest = blobDigest(value);
16835
+ blobs.write(digest, value);
16836
+ return { [BLOB_REF]: digest };
16837
+ };
16838
+ var encodeRecord = (record, blobs) => Array.isArray(record.candidates) ? { ...record, candidates: record.candidates.map((item) => toBlob(item, blobs)) } : record;
16839
+ var encodeOps = (ops, blobs) => ops.map((op) => {
16840
+ if (op.path[0] !== "candidates")
16841
+ return op;
16842
+ if (op.op === "push" && op.path.length === 1)
16843
+ return { ...op, values: op.values.map((item) => toBlob(item, blobs)) };
16844
+ if (op.op === "set" && op.path.length === 1 && Array.isArray(op.value))
16845
+ return { ...op, value: op.value.map((item) => toBlob(item, blobs)) };
16846
+ if (op.op === "set" && op.path.length === 2)
16847
+ return { ...op, value: toBlob(op.value, blobs) };
16848
+ return op;
16849
+ });
16850
+ var decode2 = (value, blobs) => {
16851
+ const digest = blobRef(value);
16852
+ if (digest)
16853
+ return blobs.read(digest);
16854
+ if (Array.isArray(value))
16855
+ return value.map((item) => decode2(item, blobs));
16856
+ if (isObject3(value)) {
16857
+ const out = {};
16858
+ for (const [key, item] of Object.entries(value))
16859
+ out[key] = decode2(item, blobs);
16860
+ return out;
16861
+ }
16862
+ return value;
16863
+ };
16864
+ function reduce(state, event, blobs) {
16865
+ const data = event.data;
16866
+ const known = KNOWN_TYPES.has(event.type);
16867
+ const carries = "record" in data || "ops" in data || "key" in data;
16868
+ if (!known && !carries) {
16869
+ if (event.critical === true)
16870
+ throw new LogDamage(`event ${event.type} was written by a newer Workit and must be understood; upgrade Workit to read this task`, true);
16871
+ return state;
16872
+ }
16873
+ let next = state;
16874
+ if ("record" in data) {
16875
+ if (!isObject3(data.record))
16876
+ throw new LogDamage(`event ${event.seq} carries a malformed record`);
16877
+ next = {
16878
+ record: decode2(data.record, blobs),
16879
+ key: null,
16880
+ legacy: null
16881
+ };
16882
+ } else if (next === null) {
16883
+ throw new LogDamage(`the log does not start with an opening event (found ${event.type})`);
16884
+ } else
16885
+ next = { ...next };
16886
+ if ("ops" in data) {
16887
+ if (!isPatch(data.ops))
16888
+ throw new LogDamage(`event ${event.seq} carries a malformed patch`);
16889
+ try {
16890
+ next.record = apply(next.record, decode2(data.ops, blobs));
16891
+ } catch (error) {
16892
+ throw new LogDamage(`event ${event.seq} does not apply: ${String(error)}`);
16893
+ }
16894
+ }
16895
+ if ("key" in data)
16896
+ next.key = isKey(data.key) ? data.key : null;
16897
+ if ("legacy" in data && isObject3(data.legacy))
16898
+ next.legacy = {
16899
+ path: String(data.legacy.path ?? ""),
16900
+ digest: String(data.legacy.digest ?? "")
16901
+ };
16902
+ return next;
16903
+ }
16904
+ function eventType(before, after) {
16905
+ const count = (value) => Array.isArray(value) ? value.length : 0;
16906
+ if (before.status !== after.status)
16907
+ return after.status === "closed" ? "task.closed" : after.status === "paused" ? "task.paused" : "task.resumed";
16908
+ if (count(after.evidence) > count(before.evidence)) {
16909
+ const last = after.evidence.at(-1);
16910
+ return last?.data?.observation ? "check.ran" : "evidence.recorded";
16911
+ }
16912
+ if (count(after.findings) > count(before.findings))
16913
+ return "finding.recorded";
16914
+ if (count(after.decisions) > count(before.decisions))
16915
+ return "decision.recorded";
16916
+ if (JSON.stringify(before.findings) !== JSON.stringify(after.findings))
16917
+ return "finding.updated";
16918
+ if (JSON.stringify(before.decisions) !== JSON.stringify(after.decisions))
16919
+ return "decision.updated";
16920
+ if (JSON.stringify(before.workers) !== JSON.stringify(after.workers))
16921
+ return "worker.updated";
16922
+ if (JSON.stringify(before.intent) !== JSON.stringify(after.intent))
16923
+ return "task.revised";
16924
+ if (JSON.stringify(before.assessments) !== JSON.stringify(after.assessments) || JSON.stringify(before.policy) !== JSON.stringify(after.policy))
16925
+ return "task.judged";
16926
+ if (JSON.stringify(before.progress) !== JSON.stringify(after.progress))
16927
+ return "task.noted";
16928
+ return "task.updated";
16929
+ }
16930
+
16931
+ // packages/workit-core/src/store/notes.ts
16932
+ var SLOT = Symbol.for("workit.migrationReporter");
16933
+ var reportMigration = (report) => {
16934
+ try {
16935
+ const reporter = globalThis[SLOT];
16936
+ if (typeof reporter === "function")
16937
+ reporter(report);
16938
+ } catch {}
16939
+ };
16940
+ var migrationReporterInstalled = () => typeof globalThis[SLOT] === "function";
16941
+
16942
+ // packages/workit-core/src/core/task-store.ts
16943
+ var MIGRATION_PENDING = "workit migration pending";
16944
+ var STORE_FORMAT = 3;
16945
+ var STORE_MIN_RUNTIME = "3.0.0";
16946
+ var SNAPSHOT_EVERY = 32;
16947
+ var COMPACT_ABOVE = 200;
16948
+ var COMPACT_KEEP = 50;
16949
+ var AUTO_COMPACT_BYTES = 2 * 1024 * 1024;
16950
+ var TOMBSTONE = Symbol("tombstone");
16951
+ var STALE_TEMPORARY_MS = 60 * 60000;
16952
+ var INDEX_VERSION = 3;
16953
+ var isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
16954
+ var hostSession = (value) => isObject4(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
16955
+ var indexEntry = (task, key, legacy, file) => {
16956
+ const sessions = [];
16957
+ const intent = hostSession(task.intent.provenance.session);
16958
+ if (intent)
16959
+ sessions.push({ ...intent, workerId: null });
16960
+ for (const worker of task.workers) {
16961
+ const session = hostSession(worker.data.session);
16962
+ if (session)
16963
+ sessions.push({ ...session, workerId: worker.id });
16964
+ }
16965
+ return {
16966
+ id: task.id,
16967
+ revision: task.revision,
16968
+ status: task.status,
16969
+ createdAt: task.createdAt,
16970
+ updatedAt: task.updatedAt,
16971
+ objective: task.intent.data.objective,
16972
+ source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
16973
+ progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
16974
+ sessions,
16975
+ workspaceId: task.workspaceId,
16976
+ key: key?.key ?? null,
16977
+ branch: key?.branch ?? null,
16978
+ boundAt: key?.boundAt ?? null,
16979
+ legacy,
16980
+ file
16981
+ };
16982
+ };
16983
+ var fileSignature = (file) => {
16984
+ try {
16985
+ const stat = fs7.statSync(file, { bigint: true });
16986
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
16987
+ } catch {
16988
+ return null;
16989
+ }
16990
+ };
16991
+ var RACY_WINDOW_NS = 2000000000n;
16992
+ var racySignature = (signature) => {
16993
+ const [mtime = 0n, ctime = 0n] = signature.split(":").slice(3, 5).map(BigInt);
16994
+ const changed = mtime > ctime ? mtime : ctime;
16995
+ return BigInt(Date.now()) * 1000000n - changed < RACY_WINDOW_NS;
16996
+ };
16997
+ var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
16998
+ var oldestFirst = (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
16999
+ var cachedRuntimeVersion = null;
17000
+ var runtimeVersion = () => {
17001
+ if (cachedRuntimeVersion === null) {
17002
+ try {
17003
+ const pkg = JSON.parse(fs7.readFileSync(path6.join(packageRoot(), "package.json"), "utf8"));
17004
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
17005
+ } catch {
17006
+ cachedRuntimeVersion = "0.0.0";
17007
+ }
17008
+ }
17009
+ return cachedRuntimeVersion;
17010
+ };
17011
+ var stampNew = (record) => ({
17012
+ createdWith: record.runtime?.createdWith ?? null,
17013
+ updatedWith: runtimeVersion()
17014
+ });
17015
+ var isNewerVersion = (candidate, current) => {
17016
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
17017
+ const [candidateParts, currentParts] = [parts(candidate), parts(current)];
17018
+ for (let index = 0;index < 3; index += 1) {
17019
+ const left = candidateParts[index] ?? 0;
17020
+ const right = currentParts[index] ?? 0;
17021
+ if (left !== right)
17022
+ return left > right;
17023
+ }
17024
+ return false;
17025
+ };
17026
+ var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
17027
+ var sameDirectoryIdentity = (left, right) => {
17028
+ if (!path6.isAbsolute(left) || !path6.isAbsolute(right))
17029
+ return false;
17030
+ const normalizedLeft = path6.resolve(left);
17031
+ const normalizedRight = path6.resolve(right);
17032
+ try {
17033
+ const a = fs7.statSync(normalizedLeft, { bigint: true });
17034
+ const b = fs7.statSync(normalizedRight, { bigint: true });
17035
+ if (!a.isDirectory() || !b.isDirectory())
17036
+ return false;
17037
+ if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
17038
+ return true;
17039
+ const canonicalLeft = fs7.realpathSync(normalizedLeft);
17040
+ const canonicalRight = fs7.realpathSync(normalizedRight);
17041
+ if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
17042
+ return true;
17043
+ return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
17044
+ } catch {
17045
+ return false;
17046
+ }
17047
+ };
16379
17048
  var externalActionLockRoots = new AsyncLocalStorage;
16380
17049
  var heldInProcess = new Map;
16381
- var holdRoot = (root) => heldInProcess.set(root, (heldInProcess.get(root) ?? 0) + 1);
16382
- var dropRoot = (root) => {
16383
- const count = (heldInProcess.get(root) ?? 1) - 1;
17050
+ var hold = (lock) => heldInProcess.set(lock, (heldInProcess.get(lock) ?? 0) + 1);
17051
+ var drop = (lock) => {
17052
+ const count = (heldInProcess.get(lock) ?? 1) - 1;
16384
17053
  if (count > 0)
16385
- heldInProcess.set(root, count);
17054
+ heldInProcess.set(lock, count);
16386
17055
  else
16387
- heldInProcess.delete(root);
17056
+ heldInProcess.delete(lock);
16388
17057
  };
16389
- var dedupeCandidates = (candidates) => {
16390
- const last = new Map(candidates.map((candidate, index) => [candidate.id, index]));
16391
- return candidates.filter((candidate, index) => last.get(candidate.id) === index);
17058
+ var envActor = () => {
17059
+ const value = (key) => process.env[key]?.trim() || null;
17060
+ return {
17061
+ host: value("WORKIT_HOST") ?? "runtime",
17062
+ session: value("WORKIT_SESSION_ID"),
17063
+ agentId: value("WORKIT_AGENT_ID")
17064
+ };
16392
17065
  };
16393
17066
 
16394
17067
  class TaskStore {
16395
17068
  root;
17069
+ origin;
16396
17070
  lockTimeoutMs;
17071
+ actor;
17072
+ migrateOnRead;
17073
+ located;
17074
+ migrationChecked = false;
17075
+ compactDue = new Set;
16397
17076
  constructor(root, options = {}) {
16398
- this.root = fs5.existsSync(root) ? fs5.realpathSync(root) : path4.resolve(root);
17077
+ const resolved = path6.resolve(root);
17078
+ this.origin = fs7.existsSync(resolved) ? fs7.realpathSync(resolved) : resolved;
17079
+ this.located = resolveStore(this.origin);
17080
+ this.root = checkoutRootOf(this.origin, this.located);
16399
17081
  this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
17082
+ this.actor = options.actor ?? envActor();
17083
+ this.migrateOnRead = options.migrateOnRead;
17084
+ }
17085
+ location() {
17086
+ if (this.located !== undefined && !(this.located instanceof Error) && !this.located.shared && fs7.existsSync(path6.join(this.root, ".git"))) {
17087
+ this.located = undefined;
17088
+ this.migrationChecked = false;
17089
+ }
17090
+ if (this.located === undefined)
17091
+ this.located = resolveStore(this.root);
17092
+ return this.located instanceof Error ? failure2("storage_error", this.located.message, { path: this.root }) : success(null, null, this.located);
17093
+ }
17094
+ currentKey() {
17095
+ const location = this.location();
17096
+ if (!location.ok)
17097
+ return location;
17098
+ return success(null, null, resolveTaskKey(this.root, location.data));
16400
17099
  }
16401
17100
  readTask(taskId) {
16402
17101
  if (!validId(taskId))
16403
17102
  return failure2("invalid_input", "task ID is invalid", { taskId });
16404
- const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
16405
- if (!result.exists)
17103
+ const ready = this.ready();
17104
+ if (!ready.ok)
17105
+ return ready;
17106
+ const loaded = this.load(taskId);
17107
+ if (!loaded.ok)
17108
+ return loaded;
17109
+ if (!loaded.data)
16406
17110
  return failure2("not_found", "task not found", { taskId });
16407
- if (result.result.ok && result.result.data.id !== taskId)
16408
- return failure2("recovery_required", "task filename and record ID differ", { taskId });
16409
- if (result.result.ok) {
16410
- const workspace = this.readWorkspace();
16411
- if (!workspace.ok)
16412
- return workspace;
16413
- if (!workspace.data || result.result.data.workspaceId !== workspace.data.id)
16414
- return failure2("recovery_required", "task workspace binding is invalid", { taskId });
16415
- }
16416
- return result.result;
16417
- }
16418
- listTasks() {
16419
- if (!fs5.existsSync(this.tasksDir))
16420
- return success(null, null, []);
16421
- let names;
16422
- try {
16423
- names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
16424
- } catch (error) {
16425
- return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
16426
- path: this.tasksDir
16427
- });
16428
- }
16429
17111
  const workspace = this.readWorkspace();
16430
17112
  if (!workspace.ok)
16431
17113
  return workspace;
16432
- if (!workspace.data)
16433
- return failure2("recovery_required", "task workspace binding is invalid", {
16434
- path: this.workspacePath
16435
- });
17114
+ const bound = this.bindingError(loaded.data.record, workspace.data);
17115
+ return bound ?? success(null, null, loaded.data.record);
17116
+ }
17117
+ listTasks() {
17118
+ const listed = this.listTaskIndex();
17119
+ if (!listed.ok)
17120
+ return listed;
16436
17121
  const tasks = [];
16437
- for (const name of names.toSorted()) {
16438
- const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
16439
- if (!item.exists)
16440
- continue;
16441
- if (!item.result.ok)
16442
- return item.result;
16443
- if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
16444
- return failure2("recovery_required", "task filename and record ID differ", { path: name });
16445
- if (item.result.data.workspaceId !== workspace.data.id)
16446
- return failure2("recovery_required", "task workspace binding is invalid", { path: name });
16447
- tasks.push(item.result.data);
17122
+ for (const entry of listed.data.toSorted((left, right) => left.id.localeCompare(right.id))) {
17123
+ const loaded = this.load(entry.id);
17124
+ if (!loaded.ok)
17125
+ return loaded;
17126
+ if (loaded.data)
17127
+ tasks.push(loaded.data.record);
16448
17128
  }
16449
17129
  return success(null, null, tasks);
16450
17130
  }
16451
17131
  listTaskIndex() {
16452
- if (!fs5.existsSync(this.tasksDir))
17132
+ const all = this.listStoreIndex();
17133
+ if (!all.ok)
17134
+ return all;
17135
+ const workspace = this.readWorkspace();
17136
+ if (!workspace.ok)
17137
+ return workspace;
17138
+ if (!workspace.data)
16453
17139
  return success(null, null, []);
17140
+ const id = workspace.data.id;
17141
+ return success(null, null, all.data.filter((entry) => entry.workspaceId === id));
17142
+ }
17143
+ listStoreIndex() {
17144
+ const ready = this.ready();
17145
+ if (!ready.ok)
17146
+ return ready;
17147
+ const tasksDir = this.paths().tasks;
16454
17148
  let names;
16455
17149
  try {
16456
- names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
17150
+ names = fs7.existsSync(tasksDir) ? fs7.readdirSync(tasksDir).filter((name) => validId(name)) : [];
16457
17151
  } catch (error) {
16458
17152
  return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
16459
- path: this.tasksDir
17153
+ path: tasksDir
16460
17154
  });
16461
17155
  }
16462
- const workspace = this.readWorkspace();
16463
- if (!workspace.ok)
16464
- return workspace;
16465
- if (!workspace.data)
16466
- return failure2("recovery_required", "task workspace binding is invalid", {
16467
- path: this.workspacePath
16468
- });
16469
- const stored = this.readIndex(workspace.data.id);
17156
+ const stored = this.readIndex();
16470
17157
  const entries = [];
16471
17158
  let changed = Object.keys(stored).length !== names.length;
16472
17159
  for (const name of names.toSorted()) {
16473
- const file = path4.join(this.tasksDir, name);
16474
- const signature = fileSignature(file);
17160
+ const signature = fileSignature(this.eventsPath(name));
16475
17161
  if (signature === null) {
16476
17162
  changed = true;
16477
17163
  continue;
16478
17164
  }
16479
- const cached = stored[name.slice(0, -5)];
17165
+ const cached = stored[name];
16480
17166
  if (cached && cached.file === signature && !racySignature(signature)) {
16481
17167
  entries.push(cached);
16482
17168
  continue;
16483
17169
  }
16484
- const item = this.readRecord(file, taskRecordSchema);
16485
- if (!item.exists)
17170
+ const loaded = this.load(name);
17171
+ if (!loaded.ok)
17172
+ return loaded;
17173
+ if (!loaded.data)
16486
17174
  continue;
16487
- if (!item.result.ok)
16488
- return item.result;
16489
- if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
16490
- return failure2("recovery_required", "task filename and record ID differ", { path: name });
16491
- if (item.result.data.workspaceId !== workspace.data.id)
16492
- return failure2("recovery_required", "task workspace binding is invalid", { path: name });
16493
- const entry = indexEntry(item.result.data, signature);
17175
+ const entry = indexEntry(loaded.data.record, loaded.data.key, loaded.data.legacy !== null, signature);
16494
17176
  if (!cached || canonicalJson(cached) !== canonicalJson(entry))
16495
17177
  changed = true;
16496
17178
  entries.push(entry);
16497
17179
  }
16498
17180
  if (changed)
16499
- this.writeIndex(workspace.data.id, entries);
17181
+ this.writeIndex(entries);
16500
17182
  return success(null, null, entries);
16501
17183
  }
16502
17184
  readWorkspace() {
16503
- const item = this.readRecord(this.workspacePath, workspaceRecordSchema);
16504
- if (!item.exists)
16505
- return success(null, null, null);
16506
- if (item.result.ok && item.result.data.root !== this.root && !sameDirectoryIdentity(item.result.data.root, this.root))
16507
- return failure2("recovery_required", "workspace root binding is invalid", {
16508
- path: this.workspacePath
17185
+ const ready = this.ready();
17186
+ if (!ready.ok)
17187
+ return ready;
17188
+ const file = this.paths().workspace;
17189
+ let bytes;
17190
+ try {
17191
+ bytes = retryTransient(() => fs7.readFileSync(file, "utf8"));
17192
+ } catch (error) {
17193
+ if (error.code === "ENOENT")
17194
+ return success(null, null, null);
17195
+ return failure2("recovery_required", `workspace record cannot be read: ${String(error)}`, {
17196
+ path: file
16509
17197
  });
16510
- return item.result;
17198
+ }
17199
+ const parsed = this.parseRecord(bytes, workspaceRecordSchema, "workspace");
17200
+ if (!parsed.ok)
17201
+ return parsed;
17202
+ if (parsed.data.root !== this.root && !sameDirectoryIdentity(parsed.data.root, this.root))
17203
+ return failure2("recovery_required", "workspace root binding is invalid", { path: file });
17204
+ return parsed;
17205
+ }
17206
+ implicitTask(input) {
17207
+ const ready = this.ready(input.create !== false);
17208
+ if (!ready.ok)
17209
+ return ready;
17210
+ const key = this.currentKey();
17211
+ if (!key.ok)
17212
+ return key;
17213
+ const found = this.openTaskForKey(key.data.key);
17214
+ if (!found.ok)
17215
+ return found;
17216
+ const workspace = this.readWorkspace();
17217
+ if (!workspace.ok)
17218
+ return workspace;
17219
+ if (found.data && workspace.data && found.data.workspaceId === workspace.data.id && (found.data.key === key.data.key || input.create === false)) {
17220
+ const task = this.readTask(found.data.id);
17221
+ return task.ok ? success(task.revision, null, { task: task.data, created: false, key: key.data }) : task;
17222
+ }
17223
+ if (input.create === false) {
17224
+ if (!found.data)
17225
+ return success(null, null, null);
17226
+ const loaded = this.load(found.data.id);
17227
+ if (!loaded.ok)
17228
+ return loaded;
17229
+ return loaded.data ? success(null, null, { task: loaded.data.record, created: false, key: key.data }) : success(null, null, null);
17230
+ }
17231
+ return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => this.implicitLocked(input, key.data)));
17232
+ }
17233
+ implicitLocked(input, key) {
17234
+ {
17235
+ const again = this.openTaskForKey(key.key);
17236
+ if (!again.ok)
17237
+ return again;
17238
+ if (again.data) {
17239
+ const moved = this.bindLocked(again.data.id, key, input.now);
17240
+ return moved.ok ? success(moved.data.revision, null, { task: moved.data, created: false, key }) : moved;
17241
+ }
17242
+ const label = key.kind === "branch" ? `branch ${key.branch}` : key.kind === "detached" ? "a detached HEAD" : path6.basename(this.root) || this.root;
17243
+ const intent = {
17244
+ objective: input.objective?.trim() || `Work on ${label}`,
17245
+ scope: { description: `the checkout (${label})`, paths: ["."], exclusions: [] },
17246
+ authorityRefs: []
17247
+ };
17248
+ const created = this.createLocked({ intent, provenance: input.provenance, expectedWorkspaceRevision: null, now: input.now }, false, key, false);
17249
+ return created.ok ? success(created.data.revision, null, { task: created.data, created: true, key }) : created;
17250
+ }
17251
+ }
17252
+ adoptTask(taskId) {
17253
+ if (!validId(taskId))
17254
+ return failure2("invalid_input", "task ID is invalid", { taskId });
17255
+ const ready = this.ready(true);
17256
+ if (!ready.ok)
17257
+ return ready;
17258
+ const key = this.currentKey();
17259
+ if (!key.ok)
17260
+ return key;
17261
+ return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => {
17262
+ const holder = this.openTaskForKey(key.data.key);
17263
+ if (!holder.ok)
17264
+ return holder;
17265
+ if (holder.data && holder.data.id !== taskId)
17266
+ 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 });
17267
+ return this.bindLocked(taskId, key.data);
17268
+ }));
17269
+ }
17270
+ keyReport() {
17271
+ const key = this.currentKey();
17272
+ if (!key.ok)
17273
+ return key;
17274
+ const bound = this.openTasksForKey(key.data.key);
17275
+ if (!bound.ok)
17276
+ return bound;
17277
+ return success(null, null, { key: key.data, bound: bound.data });
16511
17278
  }
16512
17279
  create(input, provenance, expectedWorkspaceRevision) {
16513
17280
  const value = "intent" in input ? input : {
@@ -16515,82 +17282,16 @@ class TaskStore {
16515
17282
  provenance,
16516
17283
  expectedWorkspaceRevision: expectedWorkspaceRevision ?? null
16517
17284
  };
16518
- return this.withLock(() => {
16519
- const current = this.readWorkspace();
16520
- if (!current.ok)
16521
- return current;
16522
- if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
16523
- return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
16524
- expectedWorkspaceRevision: value.expectedWorkspaceRevision,
16525
- actualWorkspaceRevision: current.data?.revision ?? null
16526
- });
16527
- }
16528
- if (!value.provenance)
16529
- return failure2("invalid_input", "provenance is required");
16530
- if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
16531
- return failure2("invalid_input", "task intent or provenance is invalid");
16532
- const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
16533
- if (current.data && !previousWorkspaceBytes)
16534
- return failure2("storage_error", "workspace snapshot disappeared during creation");
16535
- const workspace = current.data ? {
16536
- ...current.data,
16537
- revision: newRevision(),
16538
- root: this.root,
16539
- runtime: stampNew(current.data)
16540
- } : {
16541
- schemaVersion: SCHEMA_VERSION,
16542
- id: newId(),
16543
- revision: newRevision(),
16544
- root: this.root,
16545
- runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
16546
- writer: null
16547
- };
16548
- const timestamp = value.now ?? now();
16549
- const task = {
16550
- schemaVersion: SCHEMA_VERSION,
16551
- id: newId(),
16552
- workspaceId: workspace.id,
16553
- revision: newRevision(),
16554
- createdAt: timestamp,
16555
- updatedAt: timestamp,
16556
- origin: null,
16557
- intent: {
16558
- id: newId(),
16559
- recordedAt: timestamp,
16560
- provenance: value.provenance,
16561
- data: value.intent
16562
- },
16563
- constraints: [],
16564
- status: "active",
16565
- closure: null,
16566
- progress: { summary: "", nextAction: null, blockers: [] },
16567
- runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
16568
- assessments: [],
16569
- policy: null,
16570
- policyChanges: [],
16571
- candidates: [],
16572
- evidence: [],
16573
- decisions: [],
16574
- findings: [],
16575
- workers: []
16576
- };
16577
- const writtenWorkspace = this.replaceSnapshot(this.workspacePath, workspace, previousWorkspaceBytes);
16578
- if (!writtenWorkspace.ok)
16579
- return writtenWorkspace;
16580
- const writtenTask = this.replaceSnapshot(this.taskPath(task.id), task, null);
16581
- if (!writtenTask.ok) {
16582
- if (current.data && previousWorkspaceBytes) {
16583
- const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
16584
- if (!restored.ok)
16585
- return failure2("external_outcome_unknown", "task creation failed and workspace restoration is uncertain", { operation: "create", outcome: "unknown" });
16586
- }
16587
- return failure2("external_outcome_unknown", "workspace created but task write is uncertain", { operation: "create", outcome: "unknown" });
16588
- }
16589
- return success(task.revision, workspace.revision, task);
16590
- });
17285
+ const ready = this.ready(true);
17286
+ if (!ready.ok)
17287
+ return ready;
17288
+ return this.withLock(this.paths().checkoutLock, () => this.createLocked(value, true, null));
16591
17289
  }
16592
17290
  importTask(input) {
16593
- return this.withLock(() => {
17291
+ const ready = this.ready(true);
17292
+ if (!ready.ok)
17293
+ return ready;
17294
+ return this.withLock(this.paths().checkoutLock, () => {
16594
17295
  const current = this.readWorkspace();
16595
17296
  if (!current.ok)
16596
17297
  return current;
@@ -16622,23 +17323,8 @@ class TaskStore {
16622
17323
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
16623
17324
  actualWorkspaceRevision: current.data?.revision ?? null
16624
17325
  });
16625
- const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
16626
- if (current.data && !previousWorkspaceBytes)
16627
- return failure2("storage_error", "workspace snapshot disappeared during import");
16628
17326
  const timestamp = input.now ?? now();
16629
- const workspace = current.data ? {
16630
- ...current.data,
16631
- revision: newRevision(),
16632
- root: this.root,
16633
- runtime: stampNew(current.data)
16634
- } : {
16635
- schemaVersion: SCHEMA_VERSION,
16636
- id: input.workspaceId ?? newId(),
16637
- revision: newRevision(),
16638
- root: this.root,
16639
- runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
16640
- writer: null
16641
- };
17327
+ const workspace = this.nextWorkspace(current.data, input.workspaceId);
16642
17328
  const task = {
16643
17329
  ...input.task,
16644
17330
  workspaceId: workspace.id,
@@ -16653,62 +17339,46 @@ class TaskStore {
16653
17339
  const validTask = taskRecordSchema.safeParse(task);
16654
17340
  if (!validTask.success)
16655
17341
  return failure2("invalid_input", "imported task does not satisfy its schema");
16656
- const writtenWorkspace = this.replaceSnapshot(this.workspacePath, workspace, previousWorkspaceBytes);
16657
- if (!writtenWorkspace.ok)
16658
- return writtenWorkspace;
16659
- const writtenTask = this.replaceSnapshot(this.taskPath(task.id), validTask.data, null);
16660
- if (!writtenTask.ok) {
16661
- if (current.data && previousWorkspaceBytes) {
16662
- const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
16663
- if (!restored.ok)
16664
- return failure2("external_outcome_unknown", "import failed and workspace restoration is uncertain", { operation: "import", outcome: "unknown" });
16665
- }
16666
- return failure2("external_outcome_unknown", `workspace created but task import is uncertain: ${writtenTask.error}`, {
16667
- operation: "import",
16668
- outcome: "unknown",
16669
- path: writtenTask.details.path
16670
- });
16671
- }
16672
- return success(validTask.data.revision, workspace.revision, validTask.data);
17342
+ return this.writeNewTask(current.data, workspace, validTask.data, "task.imported", null, {
17343
+ operation: "import"
17344
+ });
16673
17345
  });
16674
17346
  }
16675
17347
  mutateTask(taskId, expected, update, timestamp) {
16676
- return this.withLock(() => {
16677
- const current = this.readTask(taskId);
16678
- if (!current.ok)
16679
- return current;
16680
- if (current.data.revision !== expected)
16681
- return this.conflict(expected, current.data.revision);
16682
- const previousBytes = this.snapshotBytes(this.taskPath(taskId));
16683
- if (!previousBytes)
16684
- return failure2("storage_error", "task snapshot disappeared during mutation");
17348
+ if (!validId(taskId))
17349
+ return failure2("invalid_input", "task ID is invalid", { taskId });
17350
+ const ready = this.ready(true);
17351
+ if (!ready.ok)
17352
+ return ready;
17353
+ const result = this.withLock(this.taskLockPath(taskId), () => {
17354
+ const loaded = this.loadBound(taskId);
17355
+ if (!loaded.ok)
17356
+ return loaded;
17357
+ if (loaded.data.record.revision !== expected)
17358
+ return this.conflict(expected, loaded.data.record.revision);
16685
17359
  const context = { now: timestamp ?? now(), revision: newRevision() };
16686
17360
  let changed;
16687
17361
  try {
16688
- changed = update(current.data, Object.freeze({ ...context }));
17362
+ changed = update(structuredClone(loaded.data.record), Object.freeze({ ...context }));
16689
17363
  } catch (error) {
16690
17364
  return failure2("storage_error", `task mutation failed: ${String(error)}`);
16691
17365
  }
16692
17366
  if (!changed.ok)
16693
17367
  return changed;
16694
- const record = {
16695
- ...changed.data,
16696
- id: taskId,
16697
- workspaceId: current.data.workspaceId,
16698
- createdAt: current.data.createdAt,
16699
- revision: context.revision,
16700
- updatedAt: context.now,
16701
- runtime: stampNew(current.data)
16702
- };
16703
- const valid = taskRecordSchema.safeParse(record);
16704
- if (!valid.success)
17368
+ const valid = this.nextTask(loaded.data.record, changed.data, context);
17369
+ if (!valid)
16705
17370
  return failure2("invalid_input", "task mutation produced an invalid record");
16706
- const written = this.replaceSnapshot(this.taskPath(taskId), valid.data, previousBytes);
16707
- return written.ok ? success(valid.data.revision, null, valid.data) : written;
17371
+ const written = this.appendPatch(loaded.data, valid);
17372
+ return written.ok ? success(valid.revision, null, valid) : written;
16708
17373
  });
17374
+ this.compactPending();
17375
+ return result;
16709
17376
  }
16710
17377
  mutateWorkspace(expected, update) {
16711
- return this.withLock(() => {
17378
+ const ready = this.ready(true);
17379
+ if (!ready.ok)
17380
+ return ready;
17381
+ return this.withLock(this.paths().checkoutLock, () => {
16712
17382
  const current = this.readWorkspace();
16713
17383
  if (!current.ok)
16714
17384
  return current;
@@ -16716,9 +17386,6 @@ class TaskStore {
16716
17386
  return failure2("not_found", "workspace not found");
16717
17387
  if (current.data.revision !== expected)
16718
17388
  return this.workspaceConflict(expected, current.data.revision);
16719
- const previousBytes = this.snapshotBytes(this.workspacePath);
16720
- if (!previousBytes)
16721
- return failure2("storage_error", "workspace snapshot disappeared during mutation");
16722
17389
  const context = { now: now(), revision: newRevision() };
16723
17390
  let changed;
16724
17391
  try {
@@ -16728,23 +17395,27 @@ class TaskStore {
16728
17395
  }
16729
17396
  if (!changed.ok)
16730
17397
  return changed;
16731
- const record = {
17398
+ const valid = workspaceRecordSchema.safeParse({
16732
17399
  ...changed.data,
16733
17400
  id: current.data.id,
16734
17401
  root: this.root,
16735
17402
  revision: context.revision,
16736
17403
  runtime: stampNew(current.data)
16737
- };
16738
- const valid = workspaceRecordSchema.safeParse(record);
17404
+ });
16739
17405
  if (!valid.success)
16740
17406
  return failure2("invalid_input", "workspace mutation produced an invalid record");
16741
- const written = this.replaceSnapshot(this.workspacePath, valid.data, previousBytes);
17407
+ const written = this.writeWorkspace(valid.data);
16742
17408
  return written.ok ? success(valid.data.revision, valid.data.revision, valid.data) : written;
16743
17409
  });
16744
17410
  }
16745
17411
  mutateTaskAndWorkspace(input) {
16746
- return this.withLock(() => {
16747
- const task = this.readTask(input.taskId);
17412
+ if (!validId(input.taskId))
17413
+ return failure2("invalid_input", "task ID is invalid", { taskId: input.taskId });
17414
+ const ready = this.ready(true);
17415
+ if (!ready.ok)
17416
+ return ready;
17417
+ const result = this.withLock(this.paths().checkoutLock, () => this.withLock(this.taskLockPath(input.taskId), () => {
17418
+ const task = this.loadBound(input.taskId);
16748
17419
  if (!task.ok)
16749
17420
  return task;
16750
17421
  const workspace = this.readWorkspace();
@@ -16755,14 +17426,10 @@ class TaskStore {
16755
17426
  const expected = input.expectedRevision;
16756
17427
  if (!expected)
16757
17428
  return failure2("invalid_input", "task revision is required");
16758
- if (task.data.revision !== expected)
16759
- return this.conflict(expected, task.data.revision);
17429
+ if (task.data.record.revision !== expected)
17430
+ return this.conflict(expected, task.data.record.revision);
16760
17431
  if (workspace.data.revision !== input.expectedWorkspaceRevision)
16761
17432
  return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
16762
- const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
16763
- const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
16764
- if (!previousWorkspaceBytes || !previousTaskBytes)
16765
- return failure2("storage_error", "snapshot disappeared during coupled mutation");
16766
17433
  const workspaceContext = { now: input.now ?? now(), revision: newRevision() };
16767
17434
  let changedWorkspace;
16768
17435
  try {
@@ -16781,286 +17448,69 @@ class TaskStore {
16781
17448
  });
16782
17449
  if (!nextWorkspace.success)
16783
17450
  return failure2("invalid_input", "workspace mutation produced an invalid record");
16784
- const reserved = this.replaceSnapshot(this.workspacePath, nextWorkspace.data, previousWorkspaceBytes);
17451
+ const reserved = this.writeWorkspace(nextWorkspace.data);
16785
17452
  if (!reserved.ok)
16786
17453
  return reserved;
17454
+ const uncertain = (message) => {
17455
+ this.markUncertain(nextWorkspace.data);
17456
+ return failure2("external_outcome_unknown", message, {
17457
+ operation: "coupled_mutation",
17458
+ outcome: "unknown"
17459
+ });
17460
+ };
16787
17461
  const taskContext = { now: input.now ?? now(), revision: newRevision() };
16788
17462
  let changedTask;
16789
17463
  try {
16790
- changedTask = input.task(task.data, Object.freeze({ ...taskContext }));
17464
+ changedTask = input.task(structuredClone(task.data.record), Object.freeze({ ...taskContext }));
16791
17465
  } catch (error) {
16792
- this.markUncertain(nextWorkspace.data);
16793
- return failure2("external_outcome_unknown", `workspace reserved but task mutation threw: ${String(error)}`, { operation: "coupled_mutation", outcome: "unknown" });
16794
- }
16795
- if (!changedTask.ok) {
16796
- this.markUncertain(nextWorkspace.data);
16797
- return failure2("external_outcome_unknown", "workspace reserved but task update is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
16798
- }
16799
- const nextTask = taskRecordSchema.safeParse({
16800
- ...changedTask.data,
16801
- id: input.taskId,
16802
- workspaceId: task.data.workspaceId,
16803
- createdAt: task.data.createdAt,
16804
- revision: taskContext.revision,
16805
- updatedAt: taskContext.now,
16806
- runtime: stampNew(task.data)
16807
- });
16808
- if (!nextTask.success) {
16809
- this.markUncertain(nextWorkspace.data);
16810
- return failure2("external_outcome_unknown", "workspace reserved but task update is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
16811
- }
16812
- const written = this.replaceSnapshot(this.taskPath(input.taskId), nextTask.data, previousTaskBytes);
16813
- if (!written.ok) {
16814
- this.markUncertain(nextWorkspace.data);
16815
- return failure2("external_outcome_unknown", "workspace reserved but task replacement is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
16816
- }
16817
- return success(nextTask.data.revision, nextWorkspace.data.revision, {
16818
- task: nextTask.data,
17466
+ return uncertain(`workspace reserved but task mutation threw: ${String(error)}`);
17467
+ }
17468
+ if (!changedTask.ok)
17469
+ return uncertain("workspace reserved but task update is uncertain");
17470
+ const nextTask = this.nextTask(task.data.record, changedTask.data, taskContext);
17471
+ if (!nextTask)
17472
+ return uncertain("workspace reserved but task update is uncertain");
17473
+ const written = this.appendPatch(task.data, nextTask);
17474
+ if (!written.ok)
17475
+ return uncertain("workspace reserved but task append is uncertain");
17476
+ return success(nextTask.revision, nextWorkspace.data.revision, {
17477
+ task: nextTask,
16819
17478
  workspace: nextWorkspace.data
16820
17479
  });
16821
- });
16822
- }
16823
- recoveryCandidates() {
16824
- if (!fs5.existsSync(this.recoveryDir))
16825
- return success(null, null, []);
16826
- try {
16827
- const candidates = [];
16828
- for (const name of fs5.readdirSync(this.recoveryDir)) {
16829
- const match = RECOVERY_NAME.exec(name);
16830
- if (match)
16831
- candidates.push({
16832
- target: match[1],
16833
- path: path4.join(this.recoveryDir, name),
16834
- digest: match[3]
16835
- });
16836
- }
16837
- return success(null, null, candidates);
16838
- } catch (error) {
16839
- return failure2("storage_error", `unable to list recovery: ${String(error)}`, {
16840
- path: this.recoveryDir
16841
- });
16842
- }
16843
- }
16844
- recoverTask(taskId, input) {
16845
- return this.recover("task", taskId, input);
16846
- }
16847
- recoverWorkspace(input) {
16848
- return this.recover("workspace", null, input);
16849
- }
16850
- recover(target, taskId, input) {
16851
- if (taskId !== null && !validId(taskId))
16852
- return failure2("invalid_input", "task ID is invalid", { taskId });
16853
- if (!input.reason || !Array.isArray(input.authorityRefs) || typeof input.processEvidence !== "function" || !validDigest(input.expectedBytes) || !validDigest(input.snapshotDigest))
16854
- return failure2("invalid_input", "recovery authority is invalid");
16855
- if (input.authorityRefs.some((ref) => !refSchema.safeParse(ref).success))
16856
- return failure2("invalid_input", "recovery references are invalid");
16857
- try {
16858
- const workspace = this.readWorkspace();
16859
- if (!workspace.ok && target === "task")
16860
- return workspace;
16861
- const workspaceValue = workspace.ok ? workspace.data : null;
16862
- if (workspaceValue && workspaceValue.revision !== input.expectedWorkspaceRevision)
16863
- return this.conflict(input.expectedWorkspaceRevision, workspaceValue.revision);
16864
- const file = target === "workspace" ? this.workspacePath : this.taskPath(taskId);
16865
- const currentBytes = this.snapshotBytes(file);
16866
- if (!currentBytes)
16867
- return failure2("not_found", "snapshot not found");
16868
- if (digestBytes(currentBytes) !== input.expectedBytes)
16869
- return failure2("revision_conflict", "snapshot bytes do not match expected bytes");
16870
- let selectedBytes = currentBytes;
16871
- if (digestBytes(currentBytes) !== input.snapshotDigest) {
16872
- const candidate = this.findRecovery(target, taskId, input.snapshotDigest, workspaceValue);
16873
- if (!candidate)
16874
- return failure2("recovery_required", "validated recovery snapshot not found");
16875
- selectedBytes = candidate;
16876
- }
16877
- const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
16878
- if (!selectedRecord.ok)
16879
- return selectedRecord;
16880
- if (target === "workspace") {
16881
- const parsed = selectedRecord;
16882
- if (!parsed.ok || parsed.data.root !== this.root)
16883
- return failure2("recovery_required", "workspace recovery binding is invalid");
16884
- } else {
16885
- const parsed = selectedRecord;
16886
- if (!parsed.ok || parsed.data.id !== taskId || !workspaceValue || parsed.data.workspaceId !== workspaceValue.id)
16887
- return failure2("recovery_required", "task recovery binding is invalid");
16888
- }
16889
- const lock = this.readLockSnapshot();
16890
- if (!lock.ok)
16891
- return lock;
16892
- const writer = target === "workspace" ? selectedRecord.data.writer : workspaceValue?.writer ?? null;
16893
- const evidence = this.processEvidence(input, lock.data?.data ?? null, writer);
16894
- if (!evidence.ok)
16895
- return evidence;
16896
- if (lock.data && (evidence.data.pid !== lock.data.data.pid || evidence.data.processStart !== lock.data.data.processStart))
16897
- return failure2("recovery_required", "process evidence does not match metadata lock");
16898
- if (writer && (evidence.data.state !== "accounted_for" || evidence.data.ownerDigest !== sha256(canonicalJson(writer))))
16899
- return failure2("recovery_required", "workspace writer is not accounted for");
16900
- const recoveryGate = { reclaimed: false };
16901
- const result = this.withLock((handle) => {
16902
- if (lock.data && !recoveryGate.reclaimed)
16903
- return failure2("recovery_required", "metadata lock changed during recovery", {
16904
- path: this.lockPath
16905
- });
16906
- if (!handle.verifyStillHeld())
16907
- return failure2("recovery_required", "metadata lock was compromised", {
16908
- path: this.lockPath
16909
- });
16910
- const reacquiredBytes = this.snapshotBytes(file);
16911
- if (!reacquiredBytes)
16912
- return failure2("recovery_required", "snapshot disappeared during recovery");
16913
- if (digestBytes(reacquiredBytes) !== input.expectedBytes)
16914
- return failure2("revision_conflict", "snapshot changed during recovery");
16915
- if (target === "task") {
16916
- const currentWorkspace = this.readWorkspace();
16917
- if (!currentWorkspace.ok || !currentWorkspace.data)
16918
- return failure2("recovery_required", "workspace changed during recovery");
16919
- if (currentWorkspace.data.revision !== input.expectedWorkspaceRevision)
16920
- return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
16921
- }
16922
- if (target === "workspace") {
16923
- const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
16924
- if (!parsed.ok)
16925
- return parsed;
16926
- const value = { ...parsed.data, revision: newRevision(), writer: null };
16927
- const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
16928
- return replaced.ok ? success(value.revision, value.revision, value) : replaced;
16929
- }
16930
- const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
16931
- if (!parsed.ok)
16932
- return parsed;
16933
- const value = {
16934
- ...parsed.data,
16935
- revision: newRevision(),
16936
- updatedAt: now(),
16937
- status: parsed.data.status === "active" ? "paused" : parsed.data.status
16938
- };
16939
- const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
16940
- return replaced.ok ? success(value.revision, null, value) : replaced;
16941
- }, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
16942
- return result;
16943
- } catch (error) {
16944
- return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
16945
- }
16946
- }
16947
- processEvidence(input, lock, writer) {
16948
- const lockCopy = lock ? this.deepFreeze(structuredClone(lock)) : null;
16949
- const writerCopy = writer ? this.deepFreeze(structuredClone(writer)) : null;
16950
- const lockDigest = lockCopy ? sha256(canonicalJson(lockCopy)) : null;
16951
- const writerDigest = writerCopy ? sha256(canonicalJson(writerCopy)) : null;
16952
- try {
16953
- const result = input.processEvidence(lockCopy, writerCopy);
16954
- if (lockCopy && sha256(canonicalJson(lockCopy)) !== lockDigest)
16955
- return failure2("recovery_required", "process evidence mutated lock identity");
16956
- if (writerCopy && sha256(canonicalJson(writerCopy)) !== writerDigest)
16957
- return failure2("recovery_required", "process evidence mutated writer identity");
16958
- if (!result.ok)
16959
- return result;
16960
- const parsed = processEvidenceSchema.safeParse(result.data);
16961
- return parsed.success ? success(null, null, parsed.data) : failure2("recovery_required", "process evidence is invalid");
16962
- } catch (error) {
16963
- return failure2("recovery_required", `process evidence failed: ${String(error)}`);
16964
- }
16965
- }
16966
- deepFreeze(value) {
16967
- if (value && typeof value === "object") {
16968
- for (const child of Object.values(value))
16969
- this.deepFreeze(child);
16970
- Object.freeze(value);
16971
- }
16972
- return value;
16973
- }
16974
- readLockSnapshot() {
16975
- try {
16976
- const stat = fs5.lstatSync(this.lockPath);
16977
- if (!stat.isFile() || stat.isSymbolicLink())
16978
- return failure2("recovery_required", "metadata lock is not a regular file", {
16979
- path: this.lockPath
16980
- });
16981
- const raw = fs5.readFileSync(this.lockPath, "utf8");
16982
- return success(null, null, { raw, data: parseMetadataLock(raw) });
16983
- } catch (error) {
16984
- if (error?.code === "ENOENT")
16985
- return success(null, null, null);
16986
- return failure2("recovery_required", `metadata lock is invalid: ${String(error)}`, {
16987
- path: this.lockPath
16988
- });
16989
- }
16990
- }
16991
- metadataLockOptions() {
16992
- const inProcess = heldInProcess.has(this.root);
16993
- return {
16994
- lockPath: this.lockPath,
16995
- staleMs: Number.MAX_SAFE_INTEGER,
16996
- timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
16997
- retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
16998
- staleRecovery: "remove-if-unchanged",
16999
- shouldReclaim: ({ payload, nowMs }) => {
17000
- let ageMs = null;
17001
- try {
17002
- ageMs = nowMs - fs5.lstatSync(this.lockPath).mtimeMs;
17003
- } catch {}
17004
- return classifyLockOwner(payload, ageMs).state === "stale";
17005
- },
17006
- shouldRemoveStaleLock: () => true,
17007
- parsePayload: parseMetadataLockOrNull,
17008
- payload: () => ({
17009
- pid: process.pid,
17010
- processStart: processStartOf(process.pid),
17011
- host: localLockHost(),
17012
- nonce: randomUUID2()
17013
- })
17014
- };
17015
- }
17016
- acquireMetadataLock(options) {
17017
- clearAbandonedReclaimGuard(this.lockPath);
17018
- const deadline = Date.now() + (options.timeoutMs ?? 0);
17019
- while (true) {
17020
- try {
17021
- return acquireFileLockSync(this.workspacePath, {
17022
- ...options,
17023
- timeoutMs: Math.max(0, deadline - Date.now())
17024
- });
17025
- } catch (error) {
17026
- const code = error?.code;
17027
- if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
17028
- throw error;
17029
- if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
17030
- throw error;
17031
- if (code !== "file_lock_stale")
17032
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
17033
- }
17034
- }
17035
- }
17036
- externalActionLockOptions() {
17037
- const options = this.metadataLockOptions();
17038
- return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
17480
+ }));
17481
+ this.compactPending();
17482
+ return result;
17039
17483
  }
17040
17484
  async withExternalActionLock(operation, reentrant = false) {
17041
17485
  const activeRoots = externalActionLockRoots.getStore();
17042
17486
  if (activeRoots && [...activeRoots].some((root) => root === this.root || sameDirectoryIdentity(root, this.root)))
17043
17487
  return operation();
17488
+ const ready = this.ready(true);
17489
+ if (!ready.ok)
17490
+ return ready;
17491
+ const lockPath = this.paths().checkoutLock;
17044
17492
  try {
17045
- this.initializeMutationStorage();
17493
+ this.initializeStorage();
17046
17494
  } catch (error) {
17047
17495
  return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
17048
- path: this.workitDir
17496
+ path: this.paths().dir
17049
17497
  });
17050
17498
  }
17051
17499
  let handle;
17052
17500
  try {
17053
- handle = this.acquireMetadataLock(this.externalActionLockOptions());
17501
+ const options = this.lockOptions(lockPath);
17502
+ handle = this.acquire(lockPath, {
17503
+ ...options,
17504
+ payload: () => ({ ...options.payload(), externalAction: true })
17505
+ });
17054
17506
  } catch (error) {
17055
- return this.lockFailure(error);
17507
+ return this.lockFailure(lockPath, error);
17056
17508
  }
17057
- holdRoot(this.root);
17509
+ hold(lockPath);
17058
17510
  let result;
17059
17511
  try {
17060
17512
  if (!handle.verifyStillHeld())
17061
- result = failure2("recovery_required", "metadata lock was compromised", {
17062
- path: this.lockPath
17063
- });
17513
+ result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
17064
17514
  else {
17065
17515
  try {
17066
17516
  const run = () => operation();
@@ -17074,526 +17524,1144 @@ class TaskStore {
17074
17524
  if (!handle.verifyStillHeld())
17075
17525
  result = failure2("external_outcome_unknown", "metadata lock was compromised", {
17076
17526
  outcome: "unknown",
17077
- path: this.lockPath
17527
+ path: lockPath
17078
17528
  });
17079
17529
  } catch {
17080
17530
  result = failure2("external_outcome_unknown", "metadata lock could not be verified", {
17081
17531
  outcome: "unknown",
17082
- path: this.lockPath
17532
+ path: lockPath
17083
17533
  });
17084
17534
  }
17085
17535
  }
17086
17536
  } catch {
17087
17537
  result = failure2("external_outcome_unknown", "external action lock operation failed", {
17088
17538
  outcome: "unknown",
17089
- path: this.lockPath
17539
+ path: lockPath
17090
17540
  });
17091
17541
  }
17092
- dropRoot(this.root);
17542
+ drop(lockPath);
17093
17543
  try {
17094
17544
  retryTransient(() => handle.release());
17095
17545
  } catch (error) {
17096
- const released = this.lockFailure(error);
17546
+ const released = this.lockFailure(lockPath, error);
17097
17547
  const releaseError = released.ok ? "metadata lock release failed" : released.error;
17098
17548
  return result.ok ? failure2("external_outcome_unknown", "external action metadata lock release is uncertain", {
17099
17549
  outcome: "unknown",
17100
- path: this.lockPath
17550
+ path: lockPath
17101
17551
  }) : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
17102
17552
  }
17103
17553
  return result;
17104
17554
  }
17105
- recoveryLockOptions(observed, evidence, gate) {
17106
- const options = this.metadataLockOptions();
17107
- options.timeoutMs = 0;
17108
- options.retry = { retries: 0 };
17109
- options.parsePayload = parseMetadataLock;
17110
- options.staleRecovery = "remove-if-unchanged";
17111
- options.shouldReclaim = ({ payload }) => Boolean(observed && sameMetadataLock(payload, observed.data) && (evidence.state === "stopped" || evidence.state === "accounted_for"));
17112
- options.shouldRemoveStaleLock = ({ raw, payload }) => {
17113
- if (!observed || raw !== observed.raw || !sameMetadataLock(payload, observed.data))
17114
- return false;
17115
- gate.reclaimed = true;
17116
- return true;
17555
+ collectGarbage(options = {}) {
17556
+ const dryRun = options.dryRun === true;
17557
+ const compactAbove = options.compactAbove ?? COMPACT_ABOVE;
17558
+ const report = {
17559
+ dryRun,
17560
+ compacted: { tasks: [], eventsFolded: 0, bytesBefore: 0, bytesAfter: 0 },
17561
+ blobs: { removed: 0, removedBytes: 0, kept: 0 },
17562
+ temporary: { removed: 0 },
17563
+ legacyRecovery: null,
17564
+ retried: [],
17565
+ failed: []
17566
+ };
17567
+ const ready = this.ready(!dryRun);
17568
+ if (!ready.ok)
17569
+ return ready;
17570
+ const dirs = this.paths();
17571
+ const recovery = path6.join(this.root, ".workit", "recovery");
17572
+ if (fs7.existsSync(recovery)) {
17573
+ let files = 0;
17574
+ let bytes = 0;
17575
+ try {
17576
+ for (const name of fs7.readdirSync(recovery)) {
17577
+ const stat = fs7.lstatSync(path6.join(recovery, name));
17578
+ if (!stat.isFile())
17579
+ continue;
17580
+ files += 1;
17581
+ bytes += stat.size;
17582
+ }
17583
+ } catch {}
17584
+ const remove = options.pruneRecovery === true && !dryRun;
17585
+ if (remove)
17586
+ fs7.rmSync(recovery, { recursive: true, force: true });
17587
+ report.legacyRecovery = { path: recovery, files, bytes, removed: remove };
17588
+ }
17589
+ if (!fs7.existsSync(dirs.tasks))
17590
+ return success(null, null, report);
17591
+ const names = fs7.readdirSync(dirs.tasks).filter((name) => validId(name));
17592
+ for (const name of names) {
17593
+ const file = this.eventsPath(name);
17594
+ let size = 0;
17595
+ try {
17596
+ size = fs7.statSync(file).size;
17597
+ } catch {
17598
+ continue;
17599
+ }
17600
+ const lines = this.countEvents(file);
17601
+ if (lines > compactAbove) {
17602
+ if (dryRun) {
17603
+ report.compacted.tasks.push(name);
17604
+ report.compacted.eventsFolded += lines - COMPACT_KEEP;
17605
+ report.compacted.bytesBefore += size;
17606
+ } else {
17607
+ const compacted = this.compact(name);
17608
+ if (!compacted.ok)
17609
+ (compacted.code === "busy" ? report.retried : report.failed).push(name);
17610
+ else {
17611
+ report.compacted.tasks.push(name);
17612
+ report.compacted.eventsFolded += compacted.data.folded;
17613
+ report.compacted.bytesBefore += size;
17614
+ report.compacted.bytesAfter += compacted.data.bytes;
17615
+ }
17616
+ }
17617
+ }
17618
+ }
17619
+ const nowMs = Date.now();
17620
+ const sweep = () => {
17621
+ const references = new Set;
17622
+ for (const name of names)
17623
+ try {
17624
+ for (const match of fs7.readFileSync(this.eventsPath(name), "utf8").matchAll(/"\$blob":"([0-9a-f]{64})"/g))
17625
+ references.add(match[1]);
17626
+ } catch {}
17627
+ if (fs7.existsSync(dirs.blobs))
17628
+ for (const name of fs7.readdirSync(dirs.blobs)) {
17629
+ const digest = name.replace(/\.json$/, "");
17630
+ const file = path6.join(dirs.blobs, name);
17631
+ let stat;
17632
+ try {
17633
+ stat = fs7.lstatSync(file);
17634
+ } catch {
17635
+ continue;
17636
+ }
17637
+ if (references.has(digest) || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS) {
17638
+ report.blobs.kept += 1;
17639
+ continue;
17640
+ }
17641
+ report.blobs.removed += 1;
17642
+ report.blobs.removedBytes += stat.size;
17643
+ if (!dryRun)
17644
+ fs7.rmSync(file, { force: true });
17645
+ }
17646
+ return success(null, null, null);
17117
17647
  };
17118
- return options;
17648
+ const swept = dryRun ? sweep() : this.withLock(path6.join(dirs.dir, "gc.lock"), sweep);
17649
+ if (!swept.ok)
17650
+ return swept;
17651
+ const temporaryDirs = [
17652
+ dirs.dir,
17653
+ dirs.tasks,
17654
+ dirs.blobs,
17655
+ dirs.checkout,
17656
+ ...names.map((name) => path6.join(dirs.tasks, name))
17657
+ ];
17658
+ for (const directory of temporaryDirs) {
17659
+ if (!fs7.existsSync(directory))
17660
+ continue;
17661
+ for (const name of fs7.readdirSync(directory)) {
17662
+ if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
17663
+ continue;
17664
+ const file = path6.join(directory, name);
17665
+ try {
17666
+ const stat = fs7.lstatSync(file);
17667
+ if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
17668
+ continue;
17669
+ } catch {
17670
+ continue;
17671
+ }
17672
+ report.temporary.removed += 1;
17673
+ if (!dryRun)
17674
+ fs7.rmSync(file, { force: true });
17675
+ }
17676
+ }
17677
+ return success(null, null, report);
17119
17678
  }
17120
- markUncertain(workspace) {
17121
- if (!workspace.writer || workspace.writer.state === "uncertain")
17122
- return;
17123
- const value = {
17124
- ...workspace,
17125
- writer: { ...workspace.writer, state: "uncertain" },
17126
- revision: newRevision(),
17127
- runtime: stampNew(workspace)
17679
+ paths() {
17680
+ const location = this.location();
17681
+ const dir = location.ok ? location.data.dir : path6.join(this.root, ".workit");
17682
+ const checkout = path6.join(dir, "checkouts", checkoutSlug(this.root));
17683
+ return {
17684
+ dir,
17685
+ shared: location.ok ? location.data.shared : false,
17686
+ marker: path6.join(dir, "store.json"),
17687
+ checkout,
17688
+ workspace: path6.join(checkout, "workspace.json"),
17689
+ checkoutLock: path6.join(checkout, "metadata.lock"),
17690
+ tasks: path6.join(dir, "tasks"),
17691
+ index: path6.join(dir, "task-index.json"),
17692
+ blobs: path6.join(dir, "blobs", "candidates"),
17693
+ legacy: path6.join(dir, "legacy", checkoutSlug(this.root), "v2"),
17694
+ v2: path6.join(this.root, ".workit")
17128
17695
  };
17129
- this.replaceSnapshot(this.workspacePath, value, workspace);
17130
17696
  }
17131
- withLock(operation, options = this.metadataLockOptions(), contention = "busy") {
17697
+ eventsPath(taskId) {
17698
+ return path6.join(this.paths().tasks, taskId, "events.jsonl");
17699
+ }
17700
+ snapshotPath(taskId) {
17701
+ return path6.join(this.paths().tasks, taskId, "snapshot.json");
17702
+ }
17703
+ taskLockPath(taskId) {
17704
+ return path6.join(this.paths().tasks, taskId, "lock");
17705
+ }
17706
+ ready(write = false) {
17707
+ const location = this.location();
17708
+ if (!location.ok)
17709
+ return location;
17710
+ if (this.migrationChecked)
17711
+ return success(null, null, null);
17712
+ const dirs = this.paths();
17132
17713
  try {
17133
- this.initializeMutationStorage();
17134
- } catch (error) {
17135
- return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
17136
- path: this.workitDir
17137
- });
17714
+ const marker = JSON.parse(fs7.readFileSync(dirs.marker, "utf8"));
17715
+ if (typeof marker.version === "number" && marker.version > STORE_FORMAT)
17716
+ return failure2("recovery_required", `the workit store at ${dirs.dir} has format ${marker.version}; upgrade Workit to use it`, { path: dirs.marker });
17717
+ } catch {}
17718
+ this.migrationChecked = true;
17719
+ const settled = this.settleMigrations(write);
17720
+ if (!settled.ok)
17721
+ this.migrationChecked = false;
17722
+ return settled;
17723
+ }
17724
+ settleMigrations(write) {
17725
+ const moved = this.adoptLocalStore();
17726
+ if (!moved.ok)
17727
+ return moved;
17728
+ const pending = this.pendingMigrations();
17729
+ if (pending.length > 0 && !write && !(this.migrateOnRead ?? migrationReporterInstalled()))
17730
+ 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" });
17731
+ for (const v2 of this.v2Dirs()) {
17732
+ const migrated = this.migrateV2(v2);
17733
+ if (!migrated.ok)
17734
+ return migrated;
17138
17735
  }
17139
- let handle;
17140
- let result = failure2("storage_error", "metadata lock operation did not produce a result");
17736
+ return success(null, null, null);
17737
+ }
17738
+ blobStore() {
17739
+ const dir = this.paths().blobs;
17740
+ return {
17741
+ read: (digest) => {
17742
+ try {
17743
+ return JSON.parse(fs7.readFileSync(path6.join(dir, `${digest}.json`), "utf8"));
17744
+ } catch (error) {
17745
+ throw new LogDamage(`stored candidate ${digest} is missing or unreadable: ${String(error)}`);
17746
+ }
17747
+ },
17748
+ write: (digest, value) => {
17749
+ const file = path6.join(dir, `${digest}.json`);
17750
+ if (fs7.existsSync(file)) {
17751
+ const stamp = new Date;
17752
+ try {
17753
+ fs7.utimesSync(file, stamp, stamp);
17754
+ return;
17755
+ } catch {}
17756
+ }
17757
+ fs7.mkdirSync(dir, { recursive: true });
17758
+ replaceFile(file, JSON.stringify(value), true);
17759
+ }
17760
+ };
17761
+ }
17762
+ load(taskId) {
17141
17763
  try {
17142
- handle = this.acquireMetadataLock(options);
17764
+ const loaded = this.loadOrThrow(taskId);
17765
+ if (!loaded)
17766
+ return success(null, null, null);
17767
+ const parsed = this.parseTask(loaded.state.record, taskId);
17768
+ if (!parsed.ok)
17769
+ return parsed;
17770
+ return success(null, null, {
17771
+ record: parsed.data,
17772
+ key: loaded.state.key,
17773
+ legacy: loaded.state.legacy,
17774
+ seq: loaded.seq,
17775
+ lastId: loaded.lastId,
17776
+ lastStart: loaded.lastStart,
17777
+ end: loaded.end,
17778
+ size: loaded.size,
17779
+ snapshotSeq: loaded.snapshotSeq
17780
+ });
17143
17781
  } catch (error) {
17144
- result = this.lockFailure(error, contention);
17782
+ if (error instanceof LogDamage)
17783
+ return failure2("recovery_required", error.upgrade ? error.message : `task ${taskId} log is damaged: ${error.message}`, { taskId, path: this.eventsPath(taskId) });
17784
+ return failure2("recovery_required", `task ${taskId} cannot be read: ${String(error)}`, {
17785
+ taskId
17786
+ });
17145
17787
  }
17146
- if (handle)
17147
- holdRoot(this.root);
17148
- if (handle) {
17788
+ }
17789
+ loadOrThrow(taskId) {
17790
+ const blobs = this.blobStore();
17791
+ const file = this.eventsPath(taskId);
17792
+ const snapshot = this.readSnapshot(taskId);
17793
+ if (snapshot) {
17149
17794
  try {
17150
- if (!handle.verifyStillHeld())
17151
- result = failure2("recovery_required", "metadata lock was compromised", {
17152
- path: this.lockPath
17153
- });
17154
- else {
17155
- try {
17156
- result = operation(handle);
17157
- } catch (error) {
17158
- result = failure2("storage_error", `mutation failed: ${String(error)}`);
17159
- }
17795
+ const tail = readLog(file, snapshot.lastStart, snapshot.seq);
17796
+ const first = tail?.events[0];
17797
+ if (tail && first && first.id === snapshot.lastId) {
17798
+ let state = {
17799
+ record: snapshot.record,
17800
+ key: snapshot.key,
17801
+ legacy: snapshot.legacy
17802
+ };
17803
+ for (const event of tail.events.slice(1))
17804
+ state = reduce(state, event, blobs);
17805
+ return this.loadedFrom(state, tail, snapshot.seq);
17160
17806
  }
17161
17807
  } catch (error) {
17162
- result = this.lockFailure(error);
17808
+ if (error instanceof LogDamage && error.upgrade)
17809
+ throw error;
17163
17810
  }
17164
17811
  }
17165
- if (handle) {
17166
- dropRoot(this.root);
17167
- try {
17168
- retryTransient(() => handle.release());
17169
- } catch (error) {
17170
- const releaseFailure = this.lockFailure(error);
17171
- const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
17172
- result = result.ok ? releaseFailure : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
17173
- }
17812
+ const log = readLog(file, 0, null);
17813
+ if (!log || log.events.length === 0) {
17814
+ if (log && log.size > 0)
17815
+ throw new LogDamage("the log holds no complete event");
17816
+ return null;
17174
17817
  }
17175
- return result;
17818
+ if (!OPENING_TYPES.has(log.events[0].type))
17819
+ throw new LogDamage(`the log starts with ${log.events[0].type}, not an opening event`);
17820
+ let state = null;
17821
+ for (const event of log.events)
17822
+ state = reduce(state, event, blobs);
17823
+ return this.loadedFrom(state, log, 0);
17176
17824
  }
17177
- lockDirWritable() {
17178
- const probe = `${this.lockPath}.${process.pid}.${randomUUID2()}.probe`;
17825
+ loadedFrom(state, log, snapshotSeq) {
17826
+ const last = log.events.at(-1);
17827
+ return {
17828
+ state,
17829
+ seq: last.seq,
17830
+ lastId: last.id,
17831
+ lastStart: log.lastStart,
17832
+ end: log.end,
17833
+ size: log.size,
17834
+ snapshotSeq
17835
+ };
17836
+ }
17837
+ readSnapshot(taskId) {
17179
17838
  try {
17180
- fs5.closeSync(fs5.openSync(probe, "wx", 384));
17839
+ const value = JSON.parse(fs7.readFileSync(this.snapshotPath(taskId), "utf8"));
17840
+ 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))
17841
+ return null;
17842
+ return value;
17181
17843
  } catch {
17182
- return false;
17844
+ return null;
17183
17845
  }
17846
+ }
17847
+ writeSnapshot(taskId, loaded) {
17848
+ const snapshot = {
17849
+ v: 1,
17850
+ task: taskId,
17851
+ seq: loaded.seq,
17852
+ lastId: loaded.lastId,
17853
+ lastStart: loaded.lastStart,
17854
+ end: loaded.end,
17855
+ key: loaded.key,
17856
+ legacy: loaded.legacy,
17857
+ record: loaded.record
17858
+ };
17184
17859
  try {
17185
- fs5.rmSync(probe, { force: true });
17860
+ replaceFile(this.snapshotPath(taskId), JSON.stringify(snapshot), false);
17186
17861
  } catch {}
17187
- return true;
17188
17862
  }
17189
- lockHolderPresent() {
17863
+ loadBound(taskId) {
17864
+ const loaded = this.load(taskId);
17865
+ if (!loaded.ok)
17866
+ return loaded;
17867
+ if (!loaded.data)
17868
+ return failure2("not_found", "task not found", { taskId });
17869
+ const workspace = this.readWorkspace();
17870
+ if (!workspace.ok)
17871
+ return workspace;
17872
+ const bound = this.bindingError(loaded.data.record, workspace.data);
17873
+ return bound ?? success(null, null, loaded.data);
17874
+ }
17875
+ bindingError(task, workspace) {
17876
+ if (workspace && task.workspaceId === workspace.id)
17877
+ return null;
17878
+ 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 });
17879
+ }
17880
+ nextTask(current, changed, context) {
17881
+ const valid = taskRecordSchema.safeParse({
17882
+ ...changed,
17883
+ id: current.id,
17884
+ workspaceId: current.workspaceId,
17885
+ createdAt: current.createdAt,
17886
+ revision: context.revision,
17887
+ updatedAt: context.now,
17888
+ runtime: stampNew(current)
17889
+ });
17890
+ return valid.success ? valid.data : null;
17891
+ }
17892
+ event(taskId, seq, type, data) {
17893
+ return {
17894
+ v: EVENT_VERSION,
17895
+ seq,
17896
+ at: new Date().toISOString(),
17897
+ id: newId(),
17898
+ task: taskId,
17899
+ actor: this.actor,
17900
+ type,
17901
+ data
17902
+ };
17903
+ }
17904
+ appendPatch(loaded, next, extra = {}) {
17905
+ const taskId = next.id;
17190
17906
  try {
17191
- fs5.lstatSync(this.lockPath);
17192
- return true;
17907
+ const blobs = this.blobStore();
17908
+ const before = loaded.record;
17909
+ const after = next;
17910
+ const ops = encodeOps(diff(before, after), blobs);
17911
+ const { type, ...data } = extra;
17912
+ if (data.key)
17913
+ data.key = { ...data.key, boundAt: data.key.boundAt ?? new Date().toISOString() };
17914
+ const event = this.event(taskId, loaded.seq + 1, type ?? ("key" in extra ? "task.bound" : eventType(before, after)), { ops, ...data });
17915
+ const start = appendEvent(this.eventsPath(taskId), event, loaded.end);
17916
+ const end = start + Buffer.byteLength(`${JSON.stringify(event)}
17917
+ `);
17918
+ const key = "key" in data ? data.key ?? null : loaded.key;
17919
+ const state = {
17920
+ record: next,
17921
+ key,
17922
+ legacy: extra.legacy ?? loaded.legacy,
17923
+ seq: event.seq,
17924
+ lastId: event.id,
17925
+ lastStart: start,
17926
+ end
17927
+ };
17928
+ if (event.seq - loaded.snapshotSeq >= SNAPSHOT_EVERY || next.status !== loaded.record.status || loaded.snapshotSeq === 0)
17929
+ this.writeSnapshot(taskId, state);
17930
+ if (end > AUTO_COMPACT_BYTES)
17931
+ this.compactDue.add(taskId);
17932
+ this.indexTask(next, key, (extra.legacy ?? loaded.legacy) !== null);
17933
+ return success(null, null, null);
17193
17934
  } catch (error) {
17194
- return error?.code !== "ENOENT";
17935
+ return failure2("storage_error", `task append failed: ${String(error)}`, {
17936
+ path: this.eventsPath(taskId)
17937
+ });
17195
17938
  }
17196
17939
  }
17197
- lockFailure(error, contention = "busy") {
17198
- const value = error;
17199
- const code = typeof value?.code === "string" ? value.code : "";
17200
- if (isTransientWindowsError(error) && !this.lockHolderPresent() && !this.lockDirWritable())
17201
- return failure2("storage_error", `cannot create the workspace metadata lock (${code}); no other Workit call holds it`, {
17202
- path: this.lockPath,
17203
- guidance: `Check permissions and the read-only attribute on ${path4.dirname(this.lockPath)}.`
17940
+ openTask(record, type, key, legacy = null) {
17941
+ const dir = path6.join(this.paths().tasks, record.id);
17942
+ fs7.mkdirSync(dir, { recursive: true });
17943
+ const blobs = this.blobStore();
17944
+ const bound = key ? { ...key, boundAt: key.boundAt ?? new Date().toISOString() } : null;
17945
+ const event = this.event(record.id, 1, type, {
17946
+ record: encodeRecord(record, blobs),
17947
+ key: bound,
17948
+ ...legacy ? { legacy } : {}
17949
+ });
17950
+ const file = this.eventsPath(record.id);
17951
+ const existing = readLog(file, 0, null);
17952
+ if (existing && existing.events.length > 0)
17953
+ throw new Error(`task ${record.id} already has a log`);
17954
+ appendEvent(file, event, 0);
17955
+ this.indexTask(record, bound, legacy !== null);
17956
+ }
17957
+ compactPending() {
17958
+ for (const taskId of this.compactDue) {
17959
+ this.compactDue.delete(taskId);
17960
+ this.compact(taskId);
17961
+ }
17962
+ }
17963
+ compact(taskId) {
17964
+ try {
17965
+ const file = this.eventsPath(taskId);
17966
+ const log = readLog(file, 0, null);
17967
+ if (!log || log.events.length <= COMPACT_KEEP + 1)
17968
+ return success(null, null, { folded: 0, bytes: log?.size ?? 0 });
17969
+ const blobs = this.blobStore();
17970
+ const cut = log.events.length - COMPACT_KEEP;
17971
+ let state = null;
17972
+ for (const event of log.events.slice(0, cut))
17973
+ state = reduce(state, event, blobs);
17974
+ const folded = log.events[cut - 1];
17975
+ const checkpoint = {
17976
+ ...this.event(taskId, folded.seq, "task.checkpoint", {
17977
+ record: encodeRecord(state.record, blobs),
17978
+ key: state.key,
17979
+ ...state.legacy ? { legacy: state.legacy } : {},
17980
+ folded: { from: log.events[0].seq, to: folded.seq }
17981
+ })
17982
+ };
17983
+ const prefix = `${[checkpoint, ...log.events.slice(cut)].map((event) => JSON.stringify(event)).join(`
17984
+ `)}
17985
+ `;
17986
+ const last = log.events.at(-1);
17987
+ return this.withLock(this.taskLockPath(taskId), () => {
17988
+ let latest = null;
17989
+ try {
17990
+ latest = readLog(file, log.lastStart, last.seq);
17991
+ } catch {}
17992
+ if (!latest || latest.events[0]?.id !== last.id)
17993
+ return failure2("busy", "the task log changed during compaction; retry", { taskId });
17994
+ const appended = fs7.readFileSync(file).subarray(log.end, latest.end);
17995
+ const bytes = Buffer.concat([Buffer.from(prefix, "utf8"), appended]);
17996
+ replaceFile(file, bytes.toString("utf8"), true);
17997
+ try {
17998
+ fs7.rmSync(this.snapshotPath(taskId), { force: true });
17999
+ } catch {}
18000
+ const reloaded = this.load(taskId);
18001
+ if (reloaded.ok && reloaded.data)
18002
+ this.writeSnapshot(taskId, reloaded.data);
18003
+ return success(null, null, { folded: cut, bytes: bytes.length });
17204
18004
  });
17205
- if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
17206
- let lock = null;
17207
- try {
17208
- lock = parseMetadataLockOrNull(fs5.readFileSync(this.lockPath, "utf8"));
17209
- } catch {}
17210
- if (lock?.externalAction)
17211
- return failure2("writer_conflict", "workspace is reserved by a managed external action", {
17212
- outcome: "not_started",
17213
- path: this.lockPath
17214
- });
17215
- if (contention === "busy")
17216
- return failure2("busy", `workspace metadata lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
17217
- outcome: "not_started",
17218
- path: this.lockPath,
17219
- guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
18005
+ } catch (error) {
18006
+ return failure2("storage_error", `compaction failed: ${String(error)}`, { taskId });
18007
+ }
18008
+ }
18009
+ countEvents(file) {
18010
+ try {
18011
+ const bytes = fs7.readFileSync(file);
18012
+ let count = 0;
18013
+ for (let index = bytes.indexOf(10);index >= 0; index = bytes.indexOf(10, index + 1))
18014
+ count += 1;
18015
+ return count;
18016
+ } catch {
18017
+ return 0;
18018
+ }
18019
+ }
18020
+ openTasksForKey(key) {
18021
+ const all = this.listStoreIndex();
18022
+ if (!all.ok)
18023
+ return all;
18024
+ const location = this.location();
18025
+ const renames = location.ok ? branchRenames(location.data) : [];
18026
+ 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));
18027
+ }
18028
+ openTaskForKey(key) {
18029
+ const bound = this.openTasksForKey(key);
18030
+ return bound.ok ? success(null, null, bound.data[0] ?? null) : bound;
18031
+ }
18032
+ withKeyLock(key, operation) {
18033
+ const digest = createHash4("sha256").update(key).digest("hex").slice(0, 32);
18034
+ return this.withLock(path6.join(this.paths().dir, "keys", `${digest}.lock`), operation);
18035
+ }
18036
+ bindLocked(taskId, key, timestamp) {
18037
+ const current = this.readWorkspace();
18038
+ if (!current.ok)
18039
+ return current;
18040
+ let workspace = current.data;
18041
+ if (!workspace) {
18042
+ workspace = this.nextWorkspace(null);
18043
+ const written = this.writeWorkspace(workspace);
18044
+ if (!written.ok)
18045
+ return written;
18046
+ }
18047
+ const target = workspace;
18048
+ return this.withLock(this.taskLockPath(taskId), () => {
18049
+ const loaded = this.load(taskId);
18050
+ if (!loaded.ok)
18051
+ return loaded;
18052
+ if (!loaded.data)
18053
+ return failure2("not_found", "task not found", { taskId });
18054
+ if (loaded.data.record.status === "closed")
18055
+ return failure2("invalid_transition", "a closed task cannot be adopted", { taskId });
18056
+ if (loaded.data.record.workspaceId === target.id && loaded.data.key?.key === key.key)
18057
+ return success(loaded.data.record.revision, null, loaded.data.record);
18058
+ const next = taskRecordSchema.safeParse({
18059
+ ...loaded.data.record,
18060
+ workspaceId: target.id,
18061
+ revision: newRevision(),
18062
+ updatedAt: timestamp ?? now(),
18063
+ runtime: stampNew(loaded.data.record)
18064
+ });
18065
+ if (!next.success)
18066
+ return failure2("invalid_input", "rebound task is invalid");
18067
+ const written = this.appendPatch(loaded.data, next.data, { key });
18068
+ return written.ok ? success(next.data.revision, null, next.data) : written;
18069
+ });
18070
+ }
18071
+ unbindLocked(taskId) {
18072
+ return this.withLock(this.taskLockPath(taskId), () => {
18073
+ const loaded = this.load(taskId);
18074
+ if (!loaded.ok)
18075
+ return loaded;
18076
+ if (!loaded.data || loaded.data.key === null)
18077
+ return success(null, null, null);
18078
+ return this.appendPatch(loaded.data, loaded.data.record, { key: null });
18079
+ });
18080
+ }
18081
+ createLocked(value, bindCurrent, key, checkRevision = true) {
18082
+ const current = this.readWorkspace();
18083
+ if (!current.ok)
18084
+ return current;
18085
+ if (checkRevision && (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null))
18086
+ return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
18087
+ expectedWorkspaceRevision: value.expectedWorkspaceRevision,
18088
+ actualWorkspaceRevision: current.data?.revision ?? null
18089
+ });
18090
+ if (!value.provenance)
18091
+ return failure2("invalid_input", "provenance is required");
18092
+ if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
18093
+ return failure2("invalid_input", "task intent or provenance is invalid");
18094
+ if (bindCurrent) {
18095
+ const currentKey = this.currentKey();
18096
+ if (currentKey.ok)
18097
+ return this.withKeyLock(currentKey.data.key, () => {
18098
+ const holder = this.openTaskForKey(currentKey.data.key);
18099
+ if (!holder.ok)
18100
+ return holder;
18101
+ if (holder.data) {
18102
+ const released = this.unbindLocked(holder.data.id);
18103
+ if (!released.ok)
18104
+ return released;
18105
+ }
18106
+ return this.newTask(value, current.data, currentKey.data);
17220
18107
  });
17221
18108
  }
17222
- 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));
17223
- return failure2(recovery ? "recovery_required" : "storage_error", `metadata lock operation failed: ${String(error)}`, { path: this.lockPath });
18109
+ return this.newTask(value, current.data, key);
18110
+ }
18111
+ newTask(value, previous, binding) {
18112
+ const current = { data: previous };
18113
+ const workspace = this.nextWorkspace(current.data);
18114
+ const timestamp = value.now ?? now();
18115
+ const task = {
18116
+ schemaVersion: SCHEMA_VERSION,
18117
+ id: newId(),
18118
+ workspaceId: workspace.id,
18119
+ revision: newRevision(),
18120
+ createdAt: timestamp,
18121
+ updatedAt: timestamp,
18122
+ origin: null,
18123
+ intent: {
18124
+ id: newId(),
18125
+ recordedAt: timestamp,
18126
+ provenance: value.provenance,
18127
+ data: value.intent
18128
+ },
18129
+ constraints: [],
18130
+ status: "active",
18131
+ closure: null,
18132
+ progress: { summary: "", nextAction: null, blockers: [] },
18133
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
18134
+ assessments: [],
18135
+ policy: null,
18136
+ policyChanges: [],
18137
+ candidates: [],
18138
+ evidence: [],
18139
+ decisions: [],
18140
+ findings: [],
18141
+ workers: []
18142
+ };
18143
+ return this.writeNewTask(current.data, workspace, task, "task.opened", binding, {
18144
+ operation: "create"
18145
+ });
17224
18146
  }
17225
- initializeMutationStorage() {
17226
- retryTransient(() => fs5.mkdirSync(this.tasksDir, { recursive: true }));
17227
- retryTransient(() => fs5.mkdirSync(this.recoveryDir, { recursive: true }));
17228
- let current = null;
18147
+ nextWorkspace(current, id) {
18148
+ return current ? { ...current, revision: newRevision(), root: this.root, runtime: stampNew(current) } : {
18149
+ schemaVersion: SCHEMA_VERSION,
18150
+ id: id ?? newId(),
18151
+ revision: newRevision(),
18152
+ root: this.root,
18153
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
18154
+ writer: null
18155
+ };
18156
+ }
18157
+ writeNewTask(previous, workspace, task, type, key, details) {
18158
+ const written = this.writeWorkspace(workspace);
18159
+ if (!written.ok)
18160
+ return written;
17229
18161
  try {
17230
- current = retryTransient(() => fs5.readFileSync(this.gitignorePath, "utf8"));
17231
- } catch {}
17232
- if (current !== `*
17233
- `)
17234
- retryTransient(() => fs5.writeFileSync(this.gitignorePath, `*
17235
- `));
18162
+ this.openTask(task, type, key);
18163
+ } catch (error) {
18164
+ if (previous) {
18165
+ const restored = this.writeWorkspace(previous);
18166
+ if (!restored.ok)
18167
+ return failure2("external_outcome_unknown", `task ${details.operation} failed and workspace restoration is uncertain`, { ...details, outcome: "unknown" });
18168
+ }
18169
+ return failure2("external_outcome_unknown", `workspace written but the task log is uncertain: ${String(error)}`, { ...details, outcome: "unknown" });
18170
+ }
18171
+ return success(task.revision, workspace.revision, task);
17236
18172
  }
17237
- replaceSnapshot(file, value, previous) {
17238
- let temporary;
18173
+ writeWorkspace(record) {
18174
+ const file = this.paths().workspace;
17239
18175
  try {
17240
- if (Buffer.isBuffer(previous))
17241
- this.saveRecovery(file, previous);
17242
- else if (previous !== null && typeof previous === "object")
17243
- this.saveRecovery(file, jsonBytes(previous));
17244
- else if (typeof previous === "string")
17245
- this.saveRecovery(file, previous);
17246
- temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
17247
- const fd = fs5.openSync(temporary, "wx", 384);
17248
- try {
17249
- fs5.writeSync(fd, jsonBytes(value));
17250
- fs5.fsyncSync(fd);
17251
- } finally {
17252
- fs5.closeSync(fd);
17253
- }
17254
- retryTransient(() => fs5.renameSync(temporary, file));
17255
- temporary = undefined;
17256
- this.fsyncDirectory(path4.dirname(file));
17257
- if (path4.dirname(file) === this.tasksDir)
17258
- this.indexTaskWrite(file, value);
17259
- return success(null, null, value);
18176
+ fs7.mkdirSync(path6.dirname(file), { recursive: true });
18177
+ replaceFile(file, `${canonicalJson(record)}
18178
+ `, true);
18179
+ return success(null, null, null);
17260
18180
  } catch (error) {
17261
- return failure2("storage_error", `snapshot replacement failed: ${String(error)}`, {
17262
- path: file
17263
- });
17264
- } finally {
17265
- if (temporary)
17266
- try {
17267
- fs5.unlinkSync(temporary);
17268
- } catch {}
18181
+ return failure2("storage_error", `workspace write failed: ${String(error)}`, { path: file });
17269
18182
  }
17270
18183
  }
17271
- readIndex(workspaceId) {
18184
+ markUncertain(workspace) {
18185
+ if (!workspace.writer || workspace.writer.state === "uncertain")
18186
+ return;
18187
+ this.writeWorkspace({
18188
+ ...workspace,
18189
+ writer: { ...workspace.writer, state: "uncertain" },
18190
+ revision: newRevision(),
18191
+ runtime: stampNew(workspace)
18192
+ });
18193
+ }
18194
+ readIndex() {
17272
18195
  try {
17273
- const value = JSON.parse(fs5.readFileSync(this.indexPath, "utf8"));
17274
- if (!isObject2(value) || value.version !== INDEX_VERSION || value.workspaceId !== workspaceId || !isObject2(value.tasks))
18196
+ const value = JSON.parse(fs7.readFileSync(this.paths().index, "utf8"));
18197
+ if (!isObject4(value) || value.version !== INDEX_VERSION || !isObject4(value.tasks))
17275
18198
  return {};
17276
18199
  const tasks = {};
17277
18200
  for (const [id, entry] of Object.entries(value.tasks))
17278
- 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))
18201
+ 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))
17279
18202
  tasks[id] = entry;
17280
18203
  return tasks;
17281
18204
  } catch {
17282
18205
  return {};
17283
18206
  }
17284
18207
  }
17285
- writeIndex(workspaceId, entries) {
17286
- const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
18208
+ writeIndex(entries) {
17287
18209
  try {
17288
- fs5.writeFileSync(temporary, JSON.stringify({
18210
+ fs7.mkdirSync(this.paths().tasks, { recursive: true });
18211
+ replaceFile(this.paths().index, JSON.stringify({
17289
18212
  version: INDEX_VERSION,
17290
- workspaceId,
17291
18213
  tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
17292
- }), { mode: 384, flag: "wx" });
17293
- retryTransient(() => fs5.renameSync(temporary, this.indexPath));
18214
+ }), false);
18215
+ } catch {}
18216
+ }
18217
+ indexTask(task, key, legacy) {
18218
+ try {
18219
+ const signature = fileSignature(this.eventsPath(task.id));
18220
+ if (signature === null)
18221
+ return;
18222
+ const tasks = this.readIndex();
18223
+ tasks[task.id] = indexEntry(task, key, legacy, signature);
18224
+ this.writeIndex(Object.values(tasks));
18225
+ } catch {}
18226
+ }
18227
+ parseTask(record, taskId) {
18228
+ const parsed = this.parseValue(record, taskRecordSchema, "task");
18229
+ if (!parsed.ok)
18230
+ return { ...parsed, details: { ...parsed.details, taskId } };
18231
+ if (parsed.data.id !== taskId)
18232
+ return failure2("recovery_required", "task log and record ID differ", { taskId });
18233
+ return parsed;
18234
+ }
18235
+ parseRecord(bytes, schema, label) {
18236
+ let value;
18237
+ try {
18238
+ value = JSON.parse(bytes);
18239
+ } catch {
18240
+ return failure2("recovery_required", `${label} record JSON is corrupt`);
18241
+ }
18242
+ return this.parseValue(value, schema, label);
18243
+ }
18244
+ parseValue(value, schema, label) {
18245
+ if (isObject4(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
18246
+ return failure2("unsupported_version", `unsupported ${label} schema version`);
18247
+ const parsed = parseStoredRecord(schema, value);
18248
+ if (parsed.success)
18249
+ return success(null, null, parsed.data);
18250
+ const writerVersion = isObject4(value) && isObject4(value.runtime) ? value.runtime.updatedWith : null;
18251
+ if (parsed.critical.length > 0)
18252
+ return failure2("recovery_required", `${label} record requires fields this Workit cannot read (${parsed.critical.join(", ")}); upgrade Workit before mutating this checkout`);
18253
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
18254
+ return failure2("recovery_required", `${label} record was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
18255
+ return failure2("recovery_required", `${label} record does not satisfy its schema`);
18256
+ }
18257
+ conflict(expected, actual) {
18258
+ return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", { expectedRevision: expected, actualRevision: actual });
18259
+ }
18260
+ workspaceConflict(expected, actual) {
18261
+ return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
18262
+ }
18263
+ v2Dirs() {
18264
+ return [...new Set([this.root, this.origin])].map((dir) => path6.join(dir, ".workit"));
18265
+ }
18266
+ pendingMigrations() {
18267
+ return this.v2Dirs().filter((dir) => {
18268
+ const raw = this.readV2Workspace(path6.join(dir, "workspace.json"));
18269
+ return raw !== null && raw !== TOMBSTONE;
18270
+ });
18271
+ }
18272
+ migrateV2(v2) {
18273
+ const dirs = this.paths();
18274
+ const v2Workspace = path6.join(v2, "workspace.json");
18275
+ const v2Tasks = path6.join(v2, "tasks");
18276
+ const legacy = path6.join(dirs.dir, "legacy", checkoutSlug(path6.dirname(v2)), "v2");
18277
+ const raw = this.readV2Workspace(v2Workspace);
18278
+ if (raw === null || raw === TOMBSTONE)
18279
+ return success(null, null, null);
18280
+ return this.withLock(dirs.checkoutLock, () => this.withLock(path6.join(v2, "metadata.lock"), () => {
18281
+ const workspace = this.readV2Workspace(v2Workspace);
18282
+ if (workspace === null || workspace === TOMBSTONE)
18283
+ return success(null, null, null);
18284
+ const parsedWorkspace = this.parseRecord(workspace.bytes, workspaceRecordSchema, "2.x workspace");
18285
+ if (!parsedWorkspace.ok)
18286
+ return failure2("recovery_required", `cannot migrate the 2.x store at ${v2}: ${parsedWorkspace.error}; move it aside to start fresh`, { path: v2Workspace });
18287
+ let files = [];
18288
+ try {
18289
+ files = fs7.readdirSync(v2Tasks).filter((name) => name.endsWith(".json") && validId(name.slice(0, -5))).filter((name) => !this.isStub(path6.join(v2Tasks, name)));
18290
+ } catch {}
18291
+ const current = this.readWorkspace();
18292
+ if (!current.ok)
18293
+ return current;
18294
+ let workspaceMoved = false;
18295
+ let target = current.data;
18296
+ if (!target) {
18297
+ target = { ...parsedWorkspace.data, root: this.root };
18298
+ const written = this.writeWorkspace(target);
18299
+ if (!written.ok)
18300
+ return written;
18301
+ workspaceMoved = true;
18302
+ }
18303
+ let migrated = 0;
18304
+ let skipped = 0;
18305
+ const done = new Map;
18306
+ try {
18307
+ fs7.mkdirSync(path6.join(legacy, "tasks"), { recursive: true });
18308
+ for (const name of ["workspace.json", "index.json"])
18309
+ if (fs7.existsSync(path6.join(v2, name)))
18310
+ fs7.copyFileSync(path6.join(v2, name), path6.join(legacy, name));
18311
+ for (const name of files) {
18312
+ const id = name.slice(0, -5);
18313
+ const file = path6.join(v2Tasks, name);
18314
+ const bytes = fs7.readFileSync(file, "utf8");
18315
+ const digest = sha256(bytes);
18316
+ fs7.writeFileSync(path6.join(legacy, "tasks", name), bytes);
18317
+ const parsed = this.parseRecord(bytes, taskRecordSchema, "2.x task");
18318
+ if (!parsed.ok || parsed.data.id !== id)
18319
+ 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 });
18320
+ const record = { ...parsed.data, workspaceId: target.id };
18321
+ const origin = { path: path6.join(legacy, "tasks", name), digest };
18322
+ if (!fs7.existsSync(this.eventsPath(id))) {
18323
+ this.openTask(record, "migrated.from_v2", null, origin);
18324
+ migrated += 1;
18325
+ } else {
18326
+ const resynced = this.withLock(this.taskLockPath(id), () => {
18327
+ const loaded = this.load(id);
18328
+ if (!loaded.ok)
18329
+ return loaded;
18330
+ if (!loaded.data)
18331
+ return failure2("not_found", "task not found", { taskId: id });
18332
+ if (loaded.data.legacy?.digest === digest)
18333
+ return success(null, null, false);
18334
+ const appended = this.appendPatch(loaded.data, record, {
18335
+ type: "migrated.from_v2",
18336
+ legacy: origin
18337
+ });
18338
+ return appended.ok ? success(null, null, true) : appended;
18339
+ });
18340
+ if (!resynced.ok)
18341
+ return resynced;
18342
+ if (resynced.data)
18343
+ migrated += 1;
18344
+ else
18345
+ skipped += 1;
18346
+ }
18347
+ done.set(name, digest);
18348
+ }
18349
+ } catch (error) {
18350
+ return failure2("storage_error", `cannot migrate the 2.x store at ${v2}: ${String(error)}`, {
18351
+ path: v2
18352
+ });
18353
+ }
18354
+ try {
18355
+ let complete = true;
18356
+ for (const [name, digest] of done) {
18357
+ const file = path6.join(v2Tasks, name);
18358
+ if (sha256(fs7.readFileSync(file, "utf8")) !== digest) {
18359
+ complete = false;
18360
+ continue;
18361
+ }
18362
+ replaceFile(file, this.stubFor(name.slice(0, -5)), false);
18363
+ }
18364
+ if (!complete)
18365
+ return failure2("busy", `the 2.x store at ${v2} changed during migration; retry`, {
18366
+ path: v2
18367
+ });
18368
+ fs7.rmSync(path6.join(v2, "index.json"), { force: true });
18369
+ this.writeTombstone(v2, target.id);
18370
+ } catch (error) {
18371
+ return failure2("storage_error", `cannot finish the 2.x migration: ${String(error)}`, {
18372
+ path: v2
18373
+ });
18374
+ }
18375
+ reportMigration({
18376
+ from: v2,
18377
+ to: dirs.dir,
18378
+ backup: legacy,
18379
+ tasks: migrated,
18380
+ skipped,
18381
+ workspace: workspaceMoved
18382
+ });
18383
+ return success(null, null, null);
18384
+ }));
18385
+ }
18386
+ adoptLocalStore() {
18387
+ const dirs = this.paths();
18388
+ const marker = path6.join(dirs.v2, "store.json");
18389
+ if (!dirs.shared || !fs7.existsSync(marker))
18390
+ return success(null, null, null);
18391
+ return this.withLock(dirs.checkoutLock, () => {
18392
+ if (!fs7.existsSync(marker))
18393
+ return success(null, null, null);
18394
+ let tasks = 0;
18395
+ try {
18396
+ const merge = (from, to) => {
18397
+ if (!fs7.existsSync(to)) {
18398
+ fs7.mkdirSync(path6.dirname(to), { recursive: true });
18399
+ try {
18400
+ fs7.renameSync(from, to);
18401
+ } catch {
18402
+ fs7.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
18403
+ fs7.rmSync(from, { recursive: true, force: true });
18404
+ }
18405
+ return;
18406
+ }
18407
+ if (fs7.lstatSync(from).isDirectory() && fs7.lstatSync(to).isDirectory())
18408
+ for (const name of fs7.readdirSync(from))
18409
+ merge(path6.join(from, name), path6.join(to, name));
18410
+ };
18411
+ for (const sub of ["tasks", "checkouts", "blobs", "legacy"]) {
18412
+ const from = path6.join(dirs.v2, sub);
18413
+ if (!fs7.existsSync(from))
18414
+ continue;
18415
+ if (sub === "tasks") {
18416
+ for (const name of fs7.readdirSync(from))
18417
+ if (validId(name) && !fs7.existsSync(path6.join(dirs.dir, sub, name)))
18418
+ tasks += 1;
18419
+ }
18420
+ merge(from, path6.join(dirs.dir, sub));
18421
+ }
18422
+ const ledger = path6.join(dirs.v2, "ledger");
18423
+ if (fs7.existsSync(ledger) && !fs7.existsSync(path6.join(dirs.dir, "ledger")))
18424
+ merge(ledger, path6.join(dirs.dir, "ledger"));
18425
+ fs7.rmSync(path6.join(dirs.v2, "task-index.json"), { force: true });
18426
+ fs7.renameSync(marker, path6.join(dirs.v2, "store.moved.json"));
18427
+ } catch (error) {
18428
+ return failure2("storage_error", `cannot move ${dirs.v2} into ${dirs.dir}: ${String(error)}`, {
18429
+ path: dirs.v2
18430
+ });
18431
+ }
18432
+ reportMigration({
18433
+ from: dirs.v2,
18434
+ to: dirs.dir,
18435
+ backup: dirs.v2,
18436
+ tasks,
18437
+ skipped: 0,
18438
+ workspace: false
18439
+ });
18440
+ return success(null, null, null);
18441
+ });
18442
+ }
18443
+ readV2Workspace(file) {
18444
+ let bytes;
18445
+ try {
18446
+ bytes = fs7.readFileSync(file, "utf8");
17294
18447
  } catch {
17295
- try {
17296
- fs5.unlinkSync(temporary);
17297
- } catch {}
18448
+ return null;
17298
18449
  }
17299
- }
17300
- indexTaskWrite(file, task) {
17301
18450
  try {
17302
- const signature = fileSignature(file);
17303
- if (signature === null)
17304
- return;
17305
- const tasks = this.readIndex(task.workspaceId);
17306
- tasks[task.id] = indexEntry(task, signature);
17307
- this.writeIndex(task.workspaceId, Object.values(tasks));
18451
+ const value = JSON.parse(bytes);
18452
+ if (isObject4(value) && isObject4(value.store) && value.store.format === "workit-store")
18453
+ return TOMBSTONE;
17308
18454
  } catch {}
18455
+ return { bytes };
18456
+ }
18457
+ writeTombstone(v2, workspaceId) {
18458
+ const dirs = this.paths();
18459
+ const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
18460
+ const marker = {
18461
+ schemaVersion: SCHEMA_VERSION,
18462
+ id: workspaceId ?? newId(),
18463
+ revision: newRevision(),
18464
+ root: path6.dirname(v2),
18465
+ runtime: { createdWith: version, updatedWith: version },
18466
+ writer: null,
18467
+ store: {
18468
+ format: "workit-store",
18469
+ version: STORE_FORMAT,
18470
+ path: dirs.dir,
18471
+ note: `workit ${STORE_MIN_RUNTIME}+ keeps task state in ${dirs.dir}; upgrade Workit`
18472
+ },
18473
+ critical: ["store"]
18474
+ };
18475
+ fs7.mkdirSync(v2, { recursive: true });
18476
+ const ignore = path6.join(v2, ".gitignore");
18477
+ if (!fs7.existsSync(ignore))
18478
+ fs7.writeFileSync(ignore, `*
18479
+ `);
18480
+ replaceFile(path6.join(v2, "workspace.json"), `${JSON.stringify(marker, null, 2)}
18481
+ `, true);
18482
+ }
18483
+ stubFor(taskId) {
18484
+ const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
18485
+ return `${JSON.stringify({
18486
+ schemaVersion: SCHEMA_VERSION,
18487
+ id: taskId,
18488
+ runtime: { createdWith: version, updatedWith: version },
18489
+ store: { format: "workit-store", version: STORE_FORMAT, path: this.paths().dir },
18490
+ critical: ["store"]
18491
+ })}
18492
+ `;
17309
18493
  }
17310
- saveRecovery(file, bytes) {
17311
- const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
17312
- const id = target === "task" ? path4.basename(file, ".json") : "workspace";
17313
- const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
17314
- if (fs5.existsSync(destination)) {
17315
- if (digestBytes(retryTransient(() => fs5.readFileSync(destination))) !== digestBytes(bytes))
17316
- throw new Error("recovery copy already exists with different bytes");
17317
- const stamp = new Date;
17318
- retryTransient(() => fs5.utimesSync(destination, stamp, stamp));
17319
- this.pruneRecovery(`${target}.${id}.`, destination);
17320
- return;
17321
- }
17322
- let temporary;
18494
+ isStub(file) {
17323
18495
  try {
17324
- temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
17325
- const fd = fs5.openSync(temporary, "wx", 384);
17326
- try {
17327
- fs5.writeSync(fd, bytes);
17328
- fs5.fsyncSync(fd);
17329
- } finally {
17330
- fs5.closeSync(fd);
17331
- }
17332
- retryTransient(() => fs5.renameSync(temporary, destination));
17333
- temporary = undefined;
17334
- this.fsyncDirectory(this.recoveryDir);
17335
- this.pruneRecovery(`${target}.${id}.`, destination);
17336
- } finally {
17337
- if (temporary)
17338
- try {
17339
- fs5.unlinkSync(temporary);
17340
- } catch {}
18496
+ const value = JSON.parse(fs7.readFileSync(file, "utf8"));
18497
+ return isObject4(value) && isObject4(value.store) && value.store.format === "workit-store";
18498
+ } catch {
18499
+ return false;
17341
18500
  }
17342
18501
  }
17343
- pruneRecovery(prefix, keep) {
18502
+ writeTombstoneIfMissing() {
17344
18503
  try {
17345
- const copies = this.recoveryCopies(prefix);
17346
- const surplus = this.surplusCopies(copies, path4.basename(keep));
17347
- for (const copy of surplus)
17348
- fs5.rmSync(copy.path, { force: true });
18504
+ const v2 = path6.join(this.root, ".workit");
18505
+ if (fs7.existsSync(path6.join(v2, "workspace.json")))
18506
+ return;
18507
+ let id = null;
18508
+ try {
18509
+ id = JSON.parse(fs7.readFileSync(this.paths().workspace, "utf8")).id ?? null;
18510
+ } catch {}
18511
+ this.writeTombstone(v2, id);
17349
18512
  } catch {}
17350
18513
  }
17351
- recoveryCopies(prefix = "") {
17352
- if (!fs5.existsSync(this.recoveryDir))
17353
- return [];
17354
- const copies = [];
17355
- for (const name of fs5.readdirSync(this.recoveryDir)) {
17356
- if (!name.startsWith(prefix))
17357
- continue;
17358
- const match = RECOVERY_NAME.exec(name);
17359
- if (!match)
17360
- continue;
17361
- const file = path4.join(this.recoveryDir, name);
18514
+ initializeStorage() {
18515
+ const dirs = this.paths();
18516
+ retryTransient(() => fs7.mkdirSync(dirs.checkout, { recursive: true }));
18517
+ retryTransient(() => fs7.mkdirSync(dirs.tasks, { recursive: true }));
18518
+ if (!fs7.existsSync(dirs.marker))
18519
+ replaceFile(dirs.marker, `${JSON.stringify({ format: "workit-store", version: STORE_FORMAT, createdWith: runtimeVersion() })}
18520
+ `, false);
18521
+ if (!dirs.shared) {
18522
+ const ignore = path6.join(dirs.dir, ".gitignore");
18523
+ let current = null;
17362
18524
  try {
17363
- const stat = fs5.lstatSync(file, { bigint: true });
17364
- if (!stat.isFile())
17365
- continue;
17366
- copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
18525
+ current = fs7.readFileSync(ignore, "utf8");
17367
18526
  } catch {}
18527
+ if (current !== `*
18528
+ `)
18529
+ retryTransient(() => fs7.writeFileSync(ignore, `*
18530
+ `));
17368
18531
  }
17369
- return copies;
18532
+ this.writeTombstoneIfMissing();
17370
18533
  }
17371
- surplusCopies(copies, keep) {
17372
- 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);
17373
- return ordered.slice(RECOVERY_COPIES_PER_RECORD);
17374
- }
17375
- collectGarbage(options = {}) {
17376
- const dryRun = options.dryRun === true;
17377
- const report = {
17378
- dryRun,
17379
- recovery: { removed: 0, removedBytes: 0, kept: 0 },
17380
- temporary: { removed: 0 },
17381
- candidates: { removed: 0, tasks: [], skippedActive: [], skippedClosed: [], failed: [] }
18534
+ lockOptions(lockPath) {
18535
+ const inProcess = heldInProcess.has(lockPath);
18536
+ return {
18537
+ lockPath,
18538
+ staleMs: Number.MAX_SAFE_INTEGER,
18539
+ timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
18540
+ retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
18541
+ staleRecovery: "remove-if-unchanged",
18542
+ shouldReclaim: ({ payload, nowMs }) => {
18543
+ let ageMs = null;
18544
+ try {
18545
+ ageMs = nowMs - fs7.lstatSync(lockPath).mtimeMs;
18546
+ } catch {}
18547
+ return classifyLockOwner(payload, ageMs).state === "stale";
18548
+ },
18549
+ shouldRemoveStaleLock: () => true,
18550
+ parsePayload: parseMetadataLockOrNull,
18551
+ payload: () => ({
18552
+ pid: process.pid,
18553
+ processStart: processStartOf(process.pid),
18554
+ host: localLockHost(),
18555
+ nonce: randomUUID3()
18556
+ })
17382
18557
  };
17383
- if (!fs5.existsSync(this.workitDir))
17384
- return success(null, null, report);
17385
- const sweep = () => {
18558
+ }
18559
+ acquire(lockPath, options) {
18560
+ clearAbandonedReclaimGuard(lockPath);
18561
+ const deadline = Date.now() + (options.timeoutMs ?? 0);
18562
+ while (true) {
17386
18563
  try {
17387
- const groups = new Map;
17388
- for (const copy of this.recoveryCopies())
17389
- groups.set(copy.group, [...groups.get(copy.group) ?? [], copy]);
17390
- for (const copies of groups.values()) {
17391
- const surplus = this.surplusCopies(copies);
17392
- report.recovery.kept += copies.length - surplus.length;
17393
- for (const copy of surplus) {
17394
- report.recovery.removed += 1;
17395
- try {
17396
- report.recovery.removedBytes += fs5.statSync(copy.path).size;
17397
- } catch {}
17398
- if (!dryRun)
17399
- fs5.rmSync(copy.path, { force: true });
17400
- }
17401
- }
17402
- const nowMs = Date.now();
17403
- for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
17404
- if (!fs5.existsSync(directory))
17405
- continue;
17406
- for (const name of fs5.readdirSync(directory)) {
17407
- if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
17408
- continue;
17409
- const file = path4.join(directory, name);
17410
- const stat = fs5.lstatSync(file);
17411
- if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
17412
- continue;
17413
- report.temporary.removed += 1;
17414
- if (!dryRun)
17415
- fs5.rmSync(file, { force: true });
17416
- }
17417
- }
17418
- return success(null, null, null);
17419
- } catch (error) {
17420
- return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
17421
- path: this.recoveryDir
18564
+ return acquireFileLockSync(lockPath, {
18565
+ ...options,
18566
+ timeoutMs: Math.max(0, deadline - Date.now())
17422
18567
  });
18568
+ } catch (error) {
18569
+ const code = error?.code;
18570
+ if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
18571
+ throw error;
18572
+ if (code !== "file_lock_stale" && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
18573
+ throw error;
18574
+ if (code !== "file_lock_stale")
18575
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
17423
18576
  }
17424
- };
17425
- const pruned = dryRun ? sweep() : this.withLock(sweep);
17426
- if (!pruned.ok)
17427
- return pruned;
17428
- const tasks = this.listTasks();
17429
- if (!tasks.ok)
17430
- return tasks;
17431
- for (const task of tasks.data) {
17432
- const deduped = dedupeCandidates(task.candidates);
17433
- const removed = task.candidates.length - deduped.length;
17434
- if (removed === 0)
17435
- continue;
17436
- if (task.status === "active") {
17437
- report.candidates.skippedActive.push(task.id);
17438
- continue;
17439
- }
17440
- if (task.status === "closed") {
17441
- report.candidates.skippedClosed.push(task.id);
17442
- continue;
17443
- }
17444
- if (!dryRun) {
17445
- const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
17446
- ...current,
17447
- candidates: dedupeCandidates(current.candidates)
17448
- }));
17449
- if (!written.ok) {
17450
- report.candidates.failed.push(task.id);
17451
- continue;
17452
- }
17453
- }
17454
- report.candidates.removed += removed;
17455
- report.candidates.tasks.push(task.id);
17456
18577
  }
17457
- return success(null, null, report);
17458
18578
  }
17459
- findRecovery(target, taskId, digest, workspace) {
17460
- if (!fs5.existsSync(this.recoveryDir))
17461
- return null;
17462
- const workspaceId = workspace?.id ?? this.trustedWorkspaceId();
17463
- if (target === "workspace" && !workspaceId)
17464
- return null;
17465
- const candidates = this.recoveryCandidates();
17466
- if (!candidates.ok)
17467
- return null;
17468
- for (const candidate of candidates.data) {
17469
- if (candidate.target !== target || candidate.digest !== digest)
17470
- continue;
17471
- try {
17472
- if (!fs5.lstatSync(candidate.path).isFile())
17473
- continue;
17474
- const bytes = fs5.readFileSync(candidate.path);
17475
- if (digestBytes(bytes) !== digest)
17476
- continue;
17477
- if (target === "workspace") {
17478
- const parsed = this.parseBytes(bytes, workspaceRecordSchema);
17479
- if (parsed.ok && parsed.data.id === workspaceId && parsed.data.root === this.root)
17480
- return bytes;
17481
- } else {
17482
- const parsed = this.parseBytes(bytes, taskRecordSchema);
17483
- if (parsed.ok && parsed.data.id === taskId && parsed.data.workspaceId === workspace?.id)
17484
- return bytes;
17485
- }
17486
- } catch {}
18579
+ withLock(lockPath, operation) {
18580
+ try {
18581
+ this.initializeStorage();
18582
+ fs7.mkdirSync(path6.dirname(lockPath), { recursive: true });
18583
+ } catch (error) {
18584
+ return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
18585
+ path: this.paths().dir
18586
+ });
17487
18587
  }
17488
- return null;
17489
- }
17490
- fsyncDirectory(directory) {
18588
+ let handle;
17491
18589
  try {
17492
- const fd = fs5.openSync(directory, "r");
17493
- try {
17494
- fs5.fsyncSync(fd);
17495
- } finally {
17496
- fs5.closeSync(fd);
17497
- }
18590
+ handle = this.acquire(lockPath, this.lockOptions(lockPath));
17498
18591
  } catch (error) {
17499
- if (process.platform !== "win32")
17500
- throw error;
18592
+ return this.lockFailure(lockPath, error);
17501
18593
  }
17502
- }
17503
- trustedWorkspaceId() {
17504
- if (!fs5.existsSync(this.tasksDir))
17505
- return null;
17506
- const ids = new Set;
18594
+ hold(lockPath);
18595
+ let result;
17507
18596
  try {
17508
- for (const name of fs5.readdirSync(this.tasksDir)) {
17509
- if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
17510
- return null;
17511
- const parsed = this.parseBytes(fs5.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
17512
- if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
17513
- return null;
17514
- ids.add(parsed.data.workspaceId);
18597
+ if (!handle.verifyStillHeld())
18598
+ result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
18599
+ else {
18600
+ try {
18601
+ result = operation();
18602
+ } catch (error) {
18603
+ result = failure2("storage_error", `mutation failed: ${String(error)}`);
18604
+ }
17515
18605
  }
17516
- } catch {
17517
- return null;
18606
+ } catch (error) {
18607
+ result = this.lockFailure(lockPath, error);
18608
+ }
18609
+ drop(lockPath);
18610
+ try {
18611
+ retryTransient(() => handle.release());
18612
+ } catch (error) {
18613
+ const released = this.lockFailure(lockPath, error);
18614
+ const releaseError = released.ok ? "metadata lock release failed" : released.error;
18615
+ result = result.ok ? released : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
17518
18616
  }
17519
- return ids.size === 1 ? [...ids][0] : null;
18617
+ return result;
17520
18618
  }
17521
- snapshotBytes(file) {
18619
+ lockDirWritable(lockPath) {
18620
+ const probe = `${lockPath}.${process.pid}.${randomUUID3()}.probe`;
17522
18621
  try {
17523
- return retryTransient(() => fs5.readFileSync(file));
18622
+ fs7.closeSync(fs7.openSync(probe, "wx", 384));
17524
18623
  } catch {
17525
- return null;
18624
+ return false;
17526
18625
  }
18626
+ try {
18627
+ fs7.rmSync(probe, { force: true });
18628
+ } catch {}
18629
+ return true;
17527
18630
  }
17528
- readRecord(file, schema) {
18631
+ lockHolderPresent(lockPath) {
17529
18632
  try {
17530
- const bytes = retryTransient(() => fs5.readFileSync(file, "utf8"));
17531
- return { exists: true, result: this.parseBytes(bytes, schema) };
18633
+ fs7.lstatSync(lockPath);
18634
+ return true;
17532
18635
  } catch (error) {
17533
- if (error?.code === "ENOENT")
17534
- return { exists: false, result: success(null, null, null) };
17535
- return {
17536
- exists: true,
17537
- result: failure2("recovery_required", `snapshot cannot be read: ${String(error)}`, {
17538
- path: file
17539
- })
17540
- };
18636
+ return error?.code !== "ENOENT";
17541
18637
  }
17542
18638
  }
17543
- parseBytes(bytes, schema, mode = "read") {
17544
- let value;
17545
- try {
17546
- value = JSON.parse(typeof bytes === "string" ? bytes : bytes.toString("utf8"));
17547
- } catch {
17548
- return failure2("recovery_required", "snapshot JSON is corrupt");
18639
+ lockFailure(lockPath, error) {
18640
+ const value = error;
18641
+ const code = typeof value?.code === "string" ? value.code : "";
18642
+ if (isTransientWindowsError(error) && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
18643
+ return failure2("storage_error", `cannot create the workit lock (${code}); no other Workit call holds it`, {
18644
+ path: lockPath,
18645
+ guidance: `Check permissions and the read-only attribute on ${path6.dirname(lockPath)}.`
18646
+ });
18647
+ if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
18648
+ let lock = null;
18649
+ try {
18650
+ lock = parseMetadataLockOrNull(fs7.readFileSync(lockPath, "utf8"));
18651
+ } catch {}
18652
+ if (lock?.externalAction)
18653
+ return failure2("writer_conflict", "workspace is reserved by a managed external action", {
18654
+ outcome: "not_started",
18655
+ path: lockPath
18656
+ });
18657
+ return failure2("busy", `workit lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
18658
+ outcome: "not_started",
18659
+ path: lockPath,
18660
+ guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
18661
+ });
17549
18662
  }
17550
- if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
17551
- return failure2("unsupported_version", "unsupported snapshot schema version");
17552
- const parsed = parseStoredRecord(schema, value);
17553
- if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
17554
- return success(null, null, parsed.data);
17555
- const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
17556
- const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
17557
- if (parsed.success)
17558
- return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
17559
- if (parsed.critical.length > 0)
17560
- return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
17561
- if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
17562
- return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
17563
- return failure2("recovery_required", "snapshot does not satisfy its schema");
17564
- }
17565
- conflict(expected, actual) {
17566
- return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
17567
- expectedRevision: expected,
17568
- actualRevision: actual
17569
- });
17570
- }
17571
- workspaceConflict(expected, actual) {
17572
- return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
17573
- }
17574
- taskPath(taskId) {
17575
- return path4.join(this.tasksDir, `${taskId}.json`);
17576
- }
17577
- get workitDir() {
17578
- return path4.join(this.root, ".workit");
17579
- }
17580
- get tasksDir() {
17581
- return path4.join(this.workitDir, "tasks");
17582
- }
17583
- get recoveryDir() {
17584
- return path4.join(this.workitDir, "recovery");
17585
- }
17586
- get workspacePath() {
17587
- return path4.join(this.workitDir, "workspace.json");
17588
- }
17589
- get indexPath() {
17590
- return path4.join(this.workitDir, "index.json");
17591
- }
17592
- get lockPath() {
17593
- return path4.join(this.workitDir, "metadata.lock");
17594
- }
17595
- get gitignorePath() {
17596
- return path4.join(this.workitDir, ".gitignore");
18663
+ const recovery = code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
18664
+ return failure2(recovery ? "recovery_required" : "storage_error", `lock operation failed: ${String(error)}`, { path: lockPath });
17597
18665
  }
17598
18666
  }
17599
18667
  // packages/workit-core/src/core/workers.ts
@@ -17657,30 +18725,30 @@ function assertProductWriteAllowed(input) {
17657
18725
  }
17658
18726
 
17659
18727
  // packages/workit-core/src/core/task-evaluation.ts
17660
- import { createHash as createHash7 } from "node:crypto";
17661
- import * as fs10 from "node:fs";
17662
- import { spawnSync as spawnSync5 } from "node:child_process";
17663
- import path12 from "node:path";
18728
+ import { createHash as createHash9 } from "node:crypto";
18729
+ import * as fs12 from "node:fs";
18730
+ import { spawnSync as spawnSync6 } from "node:child_process";
18731
+ import path14 from "node:path";
17664
18732
 
17665
18733
  // packages/workit-core/src/core/authority.ts
17666
- import { createHash as createHash5 } from "node:crypto";
17667
- import * as fs7 from "node:fs";
17668
- import path9 from "node:path";
18734
+ import { createHash as createHash7 } from "node:crypto";
18735
+ import * as fs9 from "node:fs";
18736
+ import path11 from "node:path";
17669
18737
 
17670
18738
  // packages/workit-core/src/core/auto-approval.ts
17671
- import { createHash as createHash4 } from "node:crypto";
18739
+ import { createHash as createHash6 } from "node:crypto";
17672
18740
  import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
17673
- import path8 from "node:path";
18741
+ import path10 from "node:path";
17674
18742
 
17675
18743
  // packages/workit-core/src/core/workspaces.ts
17676
- import { createHash as createHash3 } from "node:crypto";
18744
+ import { createHash as createHash5 } from "node:crypto";
17677
18745
  import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
17678
- import path6 from "node:path";
18746
+ import path8 from "node:path";
17679
18747
 
17680
18748
  // packages/workit-core/src/core/config.ts
17681
18749
  import {
17682
- copyFileSync,
17683
- cpSync,
18750
+ copyFileSync as copyFileSync2,
18751
+ cpSync as cpSync2,
17684
18752
  existsSync as existsSync3,
17685
18753
  mkdirSync as mkdirSync2,
17686
18754
  readFileSync as readFileSync2,
@@ -17688,7 +18756,7 @@ import {
17688
18756
  writeFileSync as writeFileSync2
17689
18757
  } from "node:fs";
17690
18758
  import os from "node:os";
17691
- import path5 from "node:path";
18759
+ import path7 from "node:path";
17692
18760
 
17693
18761
  // packages/workit-core/src/core/boundary.ts
17694
18762
  import { statSync as statSync2 } from "node:fs";
@@ -17747,7 +18815,7 @@ var mergePreset = (preset, input = {}, current = {
17747
18815
  protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
17748
18816
  };
17749
18817
  };
17750
- var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path5.join(process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config"), "workit");
18818
+ var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workit");
17751
18819
  var migratedDir = null;
17752
18820
  var migrationFailed = false;
17753
18821
  var ensureConfigDir = (dir = resolveConfigDir()) => {
@@ -17757,7 +18825,7 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
17757
18825
  migratedDir = dir;
17758
18826
  return dir;
17759
18827
  }
17760
- const legacy = path5.join(process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config"), "workflow-toolkit");
18828
+ const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workflow-toolkit");
17761
18829
  if (!existsSync3(legacy)) {
17762
18830
  migratedDir = dir;
17763
18831
  return dir;
@@ -17770,15 +18838,15 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
17770
18838
  mkdirSync2(dir, { recursive: true });
17771
18839
  diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
17772
18840
  for (const entry of readdirSync2(legacy, { withFileTypes: true })) {
17773
- const src = path5.join(legacy, entry.name);
17774
- const dest = path5.join(dir, entry.name);
18841
+ const src = path7.join(legacy, entry.name);
18842
+ const dest = path7.join(dir, entry.name);
17775
18843
  if (existsSync3(dest))
17776
18844
  continue;
17777
18845
  try {
17778
18846
  if (entry.isDirectory())
17779
- cpSync(src, dest, { recursive: true });
18847
+ cpSync2(src, dest, { recursive: true });
17780
18848
  else if (entry.isFile())
17781
- copyFileSync(src, dest);
18849
+ copyFileSync2(src, dest);
17782
18850
  } catch (err) {
17783
18851
  migrationFailed = true;
17784
18852
  diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
@@ -17843,7 +18911,7 @@ var parseConfigResult = (raw, file) => {
17843
18911
  };
17844
18912
  };
17845
18913
  var readConfigTyped = (dir) => {
17846
- const file = path5.join(dir ?? configDir(), "config.json");
18914
+ const file = path7.join(dir ?? configDir(), "config.json");
17847
18915
  return parseConfigResult(readSafe(file), file);
17848
18916
  };
17849
18917
  var readConfig = () => {
@@ -17940,7 +19008,7 @@ var releaseTrackSchema = object({
17940
19008
  context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
17941
19009
  }
17942
19010
  }
17943
- if (track.versionSource.kind === "package-json" && (path6.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
19011
+ if (track.versionSource.kind === "package-json" && (path8.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
17944
19012
  context.addIssue({
17945
19013
  code: "custom",
17946
19014
  path: ["versionSource", "path"],
@@ -17994,7 +19062,7 @@ var workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).opt
17994
19062
  names.add(workspace.name);
17995
19063
  }
17996
19064
  });
17997
- var workspacesRevision = (contents) => createHash3("sha256").update(contents ?? "<missing>").digest("hex");
19065
+ var workspacesRevision = (contents) => createHash5("sha256").update(contents ?? "<missing>").digest("hex");
17998
19066
  var validateWorkspacesDocument = (parsed, file) => {
17999
19067
  if (!isConfigObject(parsed)) {
18000
19068
  return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
@@ -18032,7 +19100,7 @@ var validateWorkspacesDocument = (parsed, file) => {
18032
19100
  };
18033
19101
  };
18034
19102
  var readWorkspacesResult = (dir = configDir()) => {
18035
- const file = path6.join(dir, "workspaces.json");
19103
+ const file = path8.join(dir, "workspaces.json");
18036
19104
  let raw;
18037
19105
  try {
18038
19106
  raw = readFileSync3(file, "utf8");
@@ -18203,7 +19271,7 @@ var runtimeWorkspaceIndexSchema = object({
18203
19271
  workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
18204
19272
  }).passthrough();
18205
19273
  var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
18206
- const file = path6.join(configDir(), "workspaces.json");
19274
+ const file = path8.join(configDir(), "workspaces.json");
18207
19275
  let raw;
18208
19276
  try {
18209
19277
  raw = readFileSync3(file, "utf8");
@@ -18334,13 +19402,13 @@ var gitContext = (workspaceRoot, paths = []) => {
18334
19402
  };
18335
19403
 
18336
19404
  // packages/workit-core/src/core/vcs-config.ts
18337
- import fs6 from "node:fs";
18338
- import path7 from "node:path";
18339
- import { spawnSync as spawnSync2 } from "node:child_process";
18340
- var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
19405
+ import fs8 from "node:fs";
19406
+ import path9 from "node:path";
19407
+ import { spawnSync as spawnSync3 } from "node:child_process";
19408
+ var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
18341
19409
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
18342
19410
  var remoteProvider = (cwd) => {
18343
- const r = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
19411
+ const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
18344
19412
  if (r.status !== 0)
18345
19413
  return null;
18346
19414
  const url = (r.stdout ?? "").trim();
@@ -18377,7 +19445,7 @@ var remoteEndpoint = (remote) => {
18377
19445
  var configuredSshEndpoint = (host) => {
18378
19446
  if (host.startsWith("-"))
18379
19447
  return null;
18380
- const resolved = spawnSync2("ssh", ["-G", host], {
19448
+ const resolved = spawnSync3("ssh", ["-G", host], {
18381
19449
  encoding: "utf8",
18382
19450
  timeout: 5000,
18383
19451
  maxBuffer: 1024 * 1024,
@@ -18427,7 +19495,7 @@ var readVcsConfig = () => {
18427
19495
  const file = vcsConfigPath();
18428
19496
  let raw;
18429
19497
  try {
18430
- raw = fs6.readFileSync(file, "utf8");
19498
+ raw = fs8.readFileSync(file, "utf8");
18431
19499
  } catch {
18432
19500
  return { status: "missing", path: file, config: {} };
18433
19501
  }
@@ -18493,7 +19561,7 @@ function vcsConfig(mode, cwd) {
18493
19561
  const prov = cfg[provider] ?? {};
18494
19562
  const out = {
18495
19563
  ok: true,
18496
- configPath: path7.resolve(cfgPath),
19564
+ configPath: path9.resolve(cfgPath),
18497
19565
  provider,
18498
19566
  defaultTargetBranch: defaultTarget,
18499
19567
  pr: cfg.pr ?? {},
@@ -18528,7 +19596,7 @@ function vcsCliIdentity(cwd) {
18528
19596
  PATH: process.env.PATH ?? "",
18529
19597
  ...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
18530
19598
  };
18531
- const result = spawnSync2(bin, ["api", "user"], {
19599
+ const result = spawnSync3(bin, ["api", "user"], {
18532
19600
  cwd: root,
18533
19601
  encoding: "utf8",
18534
19602
  env
@@ -18570,7 +19638,7 @@ function mergedPrStyle(limit = 6, cwd) {
18570
19638
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
18571
19639
  descriptionPreview: desc.slice(0, 600)
18572
19640
  });
18573
- const remoteResult = spawnSync2("git", ["remote", "get-url", "--push", "--all", "origin"], {
19641
+ const remoteResult = spawnSync3("git", ["remote", "get-url", "--push", "--all", "origin"], {
18574
19642
  cwd,
18575
19643
  encoding: "utf8"
18576
19644
  });
@@ -18586,7 +19654,7 @@ function mergedPrStyle(limit = 6, cwd) {
18586
19654
  if (!hostingApiHostMatches(remote, host))
18587
19655
  return { ok: false, error: "PR style target does not match the provider host" };
18588
19656
  if (provider === "gitlab") {
18589
- const run = (args) => spawnSync2("glab", ["api", ...args], {
19657
+ const run = (args) => spawnSync3("glab", ["api", ...args], {
18590
19658
  cwd,
18591
19659
  encoding: "utf8",
18592
19660
  env
@@ -18606,7 +19674,7 @@ function mergedPrStyle(limit = 6, cwd) {
18606
19674
  });
18607
19675
  }
18608
19676
  } else if (provider === "github") {
18609
- const r = spawnSync2("gh", [
19677
+ const r = spawnSync3("gh", [
18610
19678
  "pr",
18611
19679
  "list",
18612
19680
  "--repo",
@@ -18678,7 +19746,7 @@ var operationAutoClass = (operation) => typeof operation === "string" ? OPERATIO
18678
19746
  var digestOf = (configDirPath) => {
18679
19747
  try {
18680
19748
  const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
18681
- return createHash4("sha256").update(raw).digest("hex");
19749
+ return createHash6("sha256").update(raw).digest("hex");
18682
19750
  } catch {
18683
19751
  return null;
18684
19752
  }
@@ -18722,7 +19790,7 @@ var targetOf = (root, descriptor) => {
18722
19790
  const cwd = parsed?.payload?.cwd;
18723
19791
  if (cwd === undefined)
18724
19792
  return root;
18725
- if (typeof cwd !== "string" || !path8.isAbsolute(cwd))
19793
+ if (typeof cwd !== "string" || !path10.isAbsolute(cwd))
18726
19794
  return null;
18727
19795
  return realpathSync3(cwd);
18728
19796
  } catch {
@@ -18947,11 +20015,11 @@ function applicableDecision(task, purpose, binding, checkoutRoot) {
18947
20015
  return [];
18948
20016
  return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
18949
20017
  }
18950
- var digestBytes2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
20018
+ var digestBytes = (bytes) => createHash7("sha256").update(bytes).digest("hex");
18951
20019
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
18952
20020
  let root;
18953
20021
  try {
18954
- root = fs7.realpathSync(checkoutRoot);
20022
+ root = fs9.realpathSync(checkoutRoot);
18955
20023
  } catch {
18956
20024
  return failure2("permission_denied", "checkout root is unavailable");
18957
20025
  }
@@ -18960,23 +20028,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
18960
20028
  continue;
18961
20029
  if (!reference.digest)
18962
20030
  return failure2("invalid_input", "document references require a byte digest");
18963
- const target = path9.resolve(root, reference.path);
18964
- if (target !== root && !target.startsWith(`${root}${path9.sep}`))
20031
+ const target = path11.resolve(root, reference.path);
20032
+ if (target !== root && !target.startsWith(`${root}${path11.sep}`))
18965
20033
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
18966
20034
  fields: [
18967
20035
  { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
18968
20036
  ]
18969
20037
  });
18970
20038
  try {
18971
- const relative = path9.relative(root, target);
20039
+ const relative = path11.relative(root, target);
18972
20040
  let current = root;
18973
- for (const segment of relative.split(path9.sep)) {
20041
+ for (const segment of relative.split(path11.sep)) {
18974
20042
  if (!segment)
18975
20043
  continue;
18976
- current = path9.join(current, segment);
18977
- const stat = fs7.lstatSync(current);
18978
- const real = fs7.realpathSync(current);
18979
- if (real !== root && !real.startsWith(`${root}${path9.sep}`))
20044
+ current = path11.join(current, segment);
20045
+ const stat = fs9.lstatSync(current);
20046
+ const real = fs9.realpathSync(current);
20047
+ if (real !== root && !real.startsWith(`${root}${path11.sep}`))
18980
20048
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
18981
20049
  fields: [
18982
20050
  {
@@ -18992,7 +20060,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
18992
20060
  if (current !== target && !stat.isDirectory())
18993
20061
  return failure2("invalid_input", "document reference ancestor is not a directory");
18994
20062
  }
18995
- if (digestBytes2(fs7.readFileSync(target)) !== reference.digest)
20063
+ if (digestBytes(fs9.readFileSync(target)) !== reference.digest)
18996
20064
  return failure2("permission_denied", "approved document bytes have changed");
18997
20065
  } catch {
18998
20066
  return failure2("invalid_input", "approved document is unavailable");
@@ -19356,9 +20424,9 @@ var bindingCovers = scopeCovers;
19356
20424
  var verifyDecisionContent = verifyContentRefs;
19357
20425
 
19358
20426
  // packages/workit-core/src/check-config.ts
19359
- import { spawnSync as spawnSync3 } from "node:child_process";
19360
- import fs8 from "node:fs";
19361
- import path10 from "node:path";
20427
+ import { spawnSync as spawnSync4 } from "node:child_process";
20428
+ import fs10 from "node:fs";
20429
+ import path12 from "node:path";
19362
20430
  var SCRIPT_CHECKS = ["test", "lint", "typecheck", "check"];
19363
20431
  var CHECKS_FILE = "workit.checks.json";
19364
20432
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -19400,7 +20468,7 @@ function splitCommand(command) {
19400
20468
  return argv.length ? argv : new Error("empty command");
19401
20469
  }
19402
20470
  var git = (cwd, args) => {
19403
- const run = spawnSync3("git", args, {
20471
+ const run = spawnSync4("git", args, {
19404
20472
  cwd,
19405
20473
  encoding: "utf8",
19406
20474
  timeout: 20000,
@@ -19411,10 +20479,10 @@ var git = (cwd, args) => {
19411
20479
  return value || null;
19412
20480
  };
19413
20481
  function checkRoot(cwd) {
19414
- return path10.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
20482
+ return path12.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
19415
20483
  }
19416
20484
  var packageManager = (root) => {
19417
- const has = (file) => fs8.existsSync(path10.join(root, file));
20485
+ const has = (file) => fs10.existsSync(path12.join(root, file));
19418
20486
  if (has("bun.lock") || has("bun.lockb"))
19419
20487
  return "bun";
19420
20488
  if (has("pnpm-lock.yaml"))
@@ -19434,7 +20502,7 @@ var CHECK_NAME = /^[a-z0-9][a-z0-9:._-]{0,63}$/u;
19434
20502
  function detectedChecks(root) {
19435
20503
  const read = (file) => {
19436
20504
  try {
19437
- return fs8.readFileSync(path10.join(root, file), "utf8");
20505
+ return fs10.readFileSync(path12.join(root, file), "utf8");
19438
20506
  } catch {
19439
20507
  return null;
19440
20508
  }
@@ -19473,7 +20541,7 @@ function detectedChecks(root) {
19473
20541
  var GATES = ["testing", "verification"];
19474
20542
  function loadCheckConfig(cwd) {
19475
20543
  const root = checkRoot(cwd);
19476
- const file = path10.join(root, CHECKS_FILE);
20544
+ const file = path12.join(root, CHECKS_FILE);
19477
20545
  const none = (error = null) => ({
19478
20546
  source: error ? CHECKS_FILE : "none",
19479
20547
  root,
@@ -19483,7 +20551,7 @@ function loadCheckConfig(cwd) {
19483
20551
  });
19484
20552
  let text = null;
19485
20553
  try {
19486
- text = fs8.readFileSync(file, "utf8");
20554
+ text = fs10.readFileSync(file, "utf8");
19487
20555
  } catch (error) {
19488
20556
  if (error.code !== "ENOENT")
19489
20557
  return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
@@ -19546,10 +20614,10 @@ function workitAvailable(env = process.env) {
19546
20614
  return workitOnPath;
19547
20615
  const windows = process.platform === "win32";
19548
20616
  const exts = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
19549
- const dirs = (env.PATH ?? env.Path ?? "").split(path10.delimiter).filter(Boolean);
20617
+ const dirs = (env.PATH ?? env.Path ?? "").split(path12.delimiter).filter(Boolean);
19550
20618
  workitOnPath = dirs.some((dir) => exts.some((ext) => {
19551
20619
  try {
19552
- return fs8.statSync(path10.join(dir, `workit${ext}`)).isFile();
20620
+ return fs10.statSync(path12.join(dir, `workit${ext}`)).isFile();
19553
20621
  } catch {
19554
20622
  return false;
19555
20623
  }
@@ -19561,15 +20629,15 @@ function checkCommand(args, version, onPath = workitAvailable()) {
19561
20629
  }
19562
20630
 
19563
20631
  // packages/workit-core/src/git/rev.ts
19564
- import { spawnSync as spawnSync4 } from "node:child_process";
19565
- import { createHash as createHash6 } from "node:crypto";
19566
- import fs9 from "node:fs";
20632
+ import { spawnSync as spawnSync5 } from "node:child_process";
20633
+ import { createHash as createHash8 } from "node:crypto";
20634
+ import fs11 from "node:fs";
19567
20635
  import os2 from "node:os";
19568
- import path11 from "node:path";
20636
+ import path13 from "node:path";
19569
20637
  var MAX_BUFFER = 256 * 1024 * 1024;
19570
20638
  var GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
19571
20639
  var git2 = (cwd, args, options = {}) => {
19572
- const result = spawnSync4("git", args, {
20640
+ const result = spawnSync5("git", args, {
19573
20641
  cwd,
19574
20642
  encoding: "utf8",
19575
20643
  env: options.env ?? process.env,
@@ -19599,10 +20667,10 @@ function headSha(cwd) {
19599
20667
  var emptyTree = (cwd) => line(git2(cwd, ["mktree"], { input: "" }));
19600
20668
  var MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
19601
20669
  var seedIndex = (from, to) => {
19602
- fs9.copyFileSync(from, to);
19603
- const { atimeNs, mtimeNs } = fs9.statSync(from, { bigint: true });
20670
+ fs11.copyFileSync(from, to);
20671
+ const { atimeNs, mtimeNs } = fs11.statSync(from, { bigint: true });
19604
20672
  const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
19605
- fs9.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
20673
+ fs11.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
19606
20674
  };
19607
20675
  function worktreeTree(cwd, options = {}) {
19608
20676
  const top = line(git2(cwd, ["rev-parse", "--show-toplevel"]));
@@ -19611,10 +20679,10 @@ function worktreeTree(cwd, options = {}) {
19611
20679
  return null;
19612
20680
  const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
19613
20681
  const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
19614
- const scratch = fs9.mkdtempSync(path11.join(os2.tmpdir(), "workit-tree-"));
20682
+ const scratch = fs11.mkdtempSync(path13.join(os2.tmpdir(), "workit-tree-"));
19615
20683
  try {
19616
- const tempIndex = path11.join(scratch, "index");
19617
- if (fs9.existsSync(indexPath))
20684
+ const tempIndex = path13.join(scratch, "index");
20685
+ if (fs11.existsSync(indexPath))
19618
20686
  seedIndex(indexPath, tempIndex);
19619
20687
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
19620
20688
  const untracked = git2(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
@@ -19626,7 +20694,7 @@ function worktreeTree(cwd, options = {}) {
19626
20694
  const skipped = [];
19627
20695
  for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
19628
20696
  try {
19629
- const stat = fs9.statSync(path11.join(top, file));
20697
+ const stat = fs11.statSync(path13.join(top, file));
19630
20698
  if (stat.isFile() && stat.size > cap)
19631
20699
  skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
19632
20700
  } catch {}
@@ -19643,10 +20711,10 @@ function worktreeTree(cwd, options = {}) {
19643
20711
  if (!tree)
19644
20712
  return null;
19645
20713
  const base = line(git2(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
19646
- const key = skipped.length ? `sha256:${createHash6("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
20714
+ const key = skipped.length ? `sha256:${createHash8("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
19647
20715
  return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
19648
20716
  } finally {
19649
- fs9.rmSync(scratch, { recursive: true, force: true });
20717
+ fs11.rmSync(scratch, { recursive: true, force: true });
19650
20718
  }
19651
20719
  }
19652
20720
  function worktreeSignal(cwd, options = {}) {
@@ -19666,16 +20734,16 @@ function worktreeSignal(cwd, options = {}) {
19666
20734
  });
19667
20735
  if (!status.ok)
19668
20736
  return null;
19669
- const hash = createHash6("sha256");
20737
+ const hash = createHash8("sha256");
19670
20738
  hash.update(`${headSha(top) ?? "unborn"}\x00`);
19671
20739
  for (const entry of status.stdout.split("\x00")) {
19672
20740
  if (entry.length < 4)
19673
20741
  continue;
19674
20742
  const file = entry.slice(3);
19675
- const absolute = path11.join(top, file);
20743
+ const absolute = path13.join(top, file);
19676
20744
  let stat = "-";
19677
20745
  try {
19678
- const info = fs9.lstatSync(absolute, { bigint: true });
20746
+ const info = fs11.lstatSync(absolute, { bigint: true });
19679
20747
  stat = `${info.mode.toString(8)}:${info.size}:${info.mtimeNs}`;
19680
20748
  if (info.isDirectory())
19681
20749
  stat += `:${headSha(absolute) ?? "-"}`;
@@ -19686,15 +20754,15 @@ function worktreeSignal(cwd, options = {}) {
19686
20754
  }
19687
20755
 
19688
20756
  // packages/workit-core/src/core/task-evaluation.ts
19689
- var digestBytes3 = (value) => createHash7("sha256").update(value).digest("hex");
19690
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path12.sep}`);
19691
- var relative = (root, candidate) => path12.relative(root, candidate).split(path12.sep).join("/") || ".";
20757
+ var digestBytes2 = (value) => createHash9("sha256").update(value).digest("hex");
20758
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path14.sep}`);
20759
+ var relative = (root, candidate) => path14.relative(root, candidate).split(path14.sep).join("/") || ".";
19692
20760
  var canonicalPath = (value) => {
19693
20761
  if (typeof value !== "string" || !value)
19694
20762
  return null;
19695
20763
  if (value.includes("\\") || value.split("/").includes(".."))
19696
20764
  return null;
19697
- const normalized = path12.posix.normalize(value);
20765
+ const normalized = path14.posix.normalize(value);
19698
20766
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
19699
20767
  return null;
19700
20768
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -19730,7 +20798,7 @@ var compareCodeUnits2 = (left, right) => {
19730
20798
  return left.length - right.length;
19731
20799
  };
19732
20800
  var gitPaths = (root) => {
19733
- const result = spawnSync5("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
20801
+ const result = spawnSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
19734
20802
  cwd: root,
19735
20803
  encoding: "buffer"
19736
20804
  });
@@ -19742,21 +20810,21 @@ var gitPaths = (root) => {
19742
20810
  git: !/not a git repository/i.test(stderr)
19743
20811
  };
19744
20812
  }
19745
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path12.sep).join("/")) : [];
19746
- const stagedDeleted = spawnSync5("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
20813
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")) : [];
20814
+ const stagedDeleted = spawnSync6("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
19747
20815
  if (stagedDeleted.status !== 0) {
19748
20816
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
19749
20817
  if (!/not a git repository/i.test(stderr))
19750
20818
  return { paths, uncertain: true, git: true };
19751
20819
  } else if (stagedDeleted.stdout) {
19752
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path12.sep).join("/")));
20820
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")));
19753
20821
  }
19754
20822
  return { paths: [...new Set(paths)], uncertain: false, git: true };
19755
20823
  };
19756
20824
  var walk = (root, directory, output) => {
19757
20825
  let names;
19758
20826
  try {
19759
- names = fs10.readdirSync(directory);
20827
+ names = fs12.readdirSync(directory);
19760
20828
  } catch {
19761
20829
  return true;
19762
20830
  }
@@ -19764,10 +20832,10 @@ var walk = (root, directory, output) => {
19764
20832
  for (const name of names) {
19765
20833
  if (name === ".git" || name === ".workit")
19766
20834
  continue;
19767
- const target = path12.join(directory, name);
20835
+ const target = path14.join(directory, name);
19768
20836
  let stat;
19769
20837
  try {
19770
- stat = fs10.lstatSync(target);
20838
+ stat = fs12.lstatSync(target);
19771
20839
  } catch {
19772
20840
  uncertain = true;
19773
20841
  continue;
@@ -19782,14 +20850,14 @@ var walk = (root, directory, output) => {
19782
20850
  var scopeRoots = (root, scope) => {
19783
20851
  const roots = [];
19784
20852
  for (const item of scope.paths.length ? scope.paths : ["."]) {
19785
- const target = path12.resolve(root, item);
20853
+ const target = path14.resolve(root, item);
19786
20854
  if (!inside(root, target))
19787
20855
  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` }] });
19788
20856
  let ancestor = target;
19789
- while (!fs10.existsSync(ancestor) && ancestor !== root)
19790
- ancestor = path12.dirname(ancestor);
20857
+ while (!fs12.existsSync(ancestor) && ancestor !== root)
20858
+ ancestor = path14.dirname(ancestor);
19791
20859
  try {
19792
- if (!inside(root, fs10.realpathSync(ancestor)))
20860
+ if (!inside(root, fs12.realpathSync(ancestor)))
19793
20861
  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` }] });
19794
20862
  } catch {
19795
20863
  return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
@@ -19801,7 +20869,7 @@ var scopeRoots = (root, scope) => {
19801
20869
  function captureCandidate(root, scope, environment = []) {
19802
20870
  let checkout;
19803
20871
  try {
19804
- checkout = fs10.realpathSync(root);
20872
+ checkout = fs12.realpathSync(root);
19805
20873
  } catch {
19806
20874
  return failure2("invalid_input", "candidate root does not exist");
19807
20875
  }
@@ -19819,11 +20887,11 @@ function captureCandidate(root, scope, environment = []) {
19819
20887
  if (item === ".git" || item === ".workit")
19820
20888
  continue;
19821
20889
  names.add(item);
19822
- if (!fs10.existsSync(target))
20890
+ if (!fs12.existsSync(target))
19823
20891
  continue;
19824
20892
  let stat;
19825
20893
  try {
19826
- stat = fs10.lstatSync(target);
20894
+ stat = fs12.lstatSync(target);
19827
20895
  } catch {
19828
20896
  uncertain = true;
19829
20897
  continue;
@@ -19839,17 +20907,17 @@ function captureCandidate(root, scope, environment = []) {
19839
20907
  for (const item of [...names].toSorted()) {
19840
20908
  if (!scopeMatches(item, normalizedScope))
19841
20909
  continue;
19842
- const target = path12.join(checkout, item);
20910
+ const target = path14.join(checkout, item);
19843
20911
  try {
19844
- const stat = fs10.lstatSync(target);
20912
+ const stat = fs12.lstatSync(target);
19845
20913
  if (stat.isSymbolicLink()) {
19846
- const link = fs10.readlinkSync(target);
19847
- files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
20914
+ const link = fs12.readlinkSync(target);
20915
+ files.push({ path: item, kind: "symlink", digest: digestBytes2(link), executable: null });
19848
20916
  } else if (stat.isFile()) {
19849
20917
  files.push({
19850
20918
  path: item,
19851
20919
  kind: "file",
19852
- digest: digestBytes3(fs10.readFileSync(target)),
20920
+ digest: digestBytes2(fs12.readFileSync(target)),
19853
20921
  executable: (stat.mode & 73) !== 0
19854
20922
  });
19855
20923
  } else if (!stat.isDirectory()) {
@@ -19874,7 +20942,7 @@ function captureCandidate(root, scope, environment = []) {
19874
20942
  const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
19875
20943
  if (values.some(({ value }) => value === null))
19876
20944
  completeness = "uncertain";
19877
- const headResult = spawnSync5("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
20945
+ const headResult = spawnSync6("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
19878
20946
  if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
19879
20947
  completeness = "uncertain";
19880
20948
  const head = headResult.status === 0 ? headResult.stdout.trim() : null;
@@ -20273,13 +21341,13 @@ function evaluateClosure(requestedOutcome, view) {
20273
21341
 
20274
21342
  // packages/workit-core/src/core/methods.ts
20275
21343
  var METHODS = {
20276
- "workit-challenge": { dimensions: ["challenge", "decisions"] },
20277
- "workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
21344
+ "workit-shape": { dimensions: ["challenge", "decisions", "artifacts", "continuity"] },
21345
+ "workit-bdd": { dimensions: ["testing"] },
20278
21346
  "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
20279
- "workit-plan": { dimensions: ["artifacts", "continuity"] },
20280
- "workit-implement": { dimensions: ["delegation"] },
21347
+ "workit-implement": { ruleIds: ["mechanical-existing-checks"] },
21348
+ "workit-fanout": { dimensions: ["delegation"] },
20281
21349
  "workit-debug": {},
20282
- "workit-handoff": {},
21350
+ "workit-continue": {},
20283
21351
  "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
20284
21352
  };
20285
21353
  var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
@@ -21045,6 +22113,38 @@ var applyWorkerLifecycle = (input) => {
21045
22113
  return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
21046
22114
  };
21047
22115
  var refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }));
22116
+ var TASK_SCOPED_ACTIONS = new Set([
22117
+ "task.inspect",
22118
+ "task.revise",
22119
+ "task.progress",
22120
+ "task.pause",
22121
+ "task.resume",
22122
+ "task.close",
22123
+ "policy.assess",
22124
+ "policy.preview",
22125
+ "policy.explain",
22126
+ "evidence.record",
22127
+ "finding.record",
22128
+ "finding.resolve",
22129
+ "decision.record",
22130
+ "decision.revoke",
22131
+ "worker.assign",
22132
+ "worker.report",
22133
+ "worker.cancel",
22134
+ "writer.acquire",
22135
+ "writer.release",
22136
+ "state.export"
22137
+ ]);
22138
+ var CREATING_ACTIONS = new Set([
22139
+ "task.revise",
22140
+ "task.progress",
22141
+ "policy.assess",
22142
+ "evidence.record",
22143
+ "finding.record",
22144
+ "decision.record",
22145
+ "worker.assign",
22146
+ "writer.acquire"
22147
+ ]);
21048
22148
  var REVISION_RETRY_ATTEMPTS = 8;
21049
22149
  var retryPause = new Int32Array(new SharedArrayBuffer(4));
21050
22150
  var filledRevisions = (request) => {
@@ -21296,7 +22396,9 @@ class WorkitCore {
21296
22396
  }
21297
22397
  contextRootError() {
21298
22398
  try {
21299
- if (realpathSync6(this.context.root) !== this.store.root)
22399
+ realpathSync6(this.context.root);
22400
+ const root = checkoutRootOf(this.context.root);
22401
+ if (root !== this.store.root && !sameDirectoryIdentity(root, this.store.root))
21300
22402
  return failure2("invalid_input", "operation context root does not match the task store root");
21301
22403
  } catch {
21302
22404
  return failure2("invalid_input", "operation context root cannot be resolved");
@@ -21318,6 +22420,30 @@ class WorkitCore {
21318
22420
  retryOmittedRevisions(request, run) {
21319
22421
  return retryFilledRevisions(filledRevisions(request), run);
21320
22422
  }
22423
+ implicitTask(family, request) {
22424
+ if (typeof request !== "object" || request === null || Array.isArray(request))
22425
+ return success(null, null, request);
22426
+ const value = request;
22427
+ if (value.taskId !== undefined || typeof value.action !== "string")
22428
+ return success(null, null, request);
22429
+ const action = `${family}.${value.action}`;
22430
+ if (!TASK_SCOPED_ACTIONS.has(action))
22431
+ return success(null, null, request);
22432
+ const root = this.contextRootError();
22433
+ if (!root.ok)
22434
+ return root;
22435
+ const create = CREATING_ACTIONS.has(action) && (this.context.workerId ?? null) === null;
22436
+ const found = this.store.implicitTask({
22437
+ provenance: provenance(this.context),
22438
+ create,
22439
+ now: trustedNow(this.context)
22440
+ });
22441
+ if (!found.ok)
22442
+ return found;
22443
+ if (!found.data)
22444
+ 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');
22445
+ return success(null, null, { ...request, taskId: found.data.task.id });
22446
+ }
21321
22447
  helperEntry(task, requireSession = false, requireActive = false) {
21322
22448
  const workerId = this.context.workerId ?? null;
21323
22449
  if (workerId === null)
@@ -21349,7 +22475,11 @@ class WorkitCore {
21349
22475
  return null;
21350
22476
  }
21351
22477
  state(request) {
21352
- return request?.action === "import" ? this.retryOmittedRevisions(request, () => this.stateOnce(request)) : this.stateOnce(request);
22478
+ const resolved = this.implicitTask("state", request);
22479
+ if (!resolved.ok)
22480
+ return resolved;
22481
+ const value = resolved.data;
22482
+ return value?.action === "import" ? this.retryOmittedRevisions(value, () => this.stateOnce(value)) : this.stateOnce(value);
21353
22483
  }
21354
22484
  stateOnce(request) {
21355
22485
  const root = this.contextRootError();
@@ -21416,44 +22546,14 @@ class WorkitCore {
21416
22546
  return imported;
21417
22547
  return this.summary(imported.data);
21418
22548
  }
21419
- if (!this.context.nativeRecovery)
21420
- return failure2("permission_denied", "native recovery authority is unavailable");
21421
- const processEvidence = (lock, writer) => this.context.nativeRecovery({
21422
- lock,
21423
- writer,
21424
- reason: input.reason,
21425
- authorityRefs: input.authorityRefs
21426
- });
21427
- if (input.target === "workspace")
21428
- return this.store.recoverWorkspace({
21429
- expectedBytes: input.expectedBytes,
21430
- snapshotDigest: input.snapshotDigest,
21431
- reason: input.reason,
21432
- authorityRefs: input.authorityRefs,
21433
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
21434
- processEvidence
21435
- });
21436
- const workspace = this.store.readWorkspace();
21437
- if (!workspace.ok)
21438
- return workspace;
21439
- if (!workspace.data)
21440
- return failure2("not_found", "workspace not found");
21441
- this.fillRevisions(input, undefined, workspace.data);
21442
- if (input.target === "workspace" && input.taskId !== undefined)
21443
- return failure2("invalid_input", "workspace recovery takes no task ID");
21444
- if (input.target === "task" && input.taskId === undefined)
21445
- return failure2("invalid_input", "task recovery requires a task ID");
21446
- return this.store.recoverTask(input.taskId, {
21447
- expectedBytes: input.expectedBytes,
21448
- snapshotDigest: input.snapshotDigest,
21449
- reason: input.reason,
21450
- authorityRefs: input.authorityRefs,
21451
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
21452
- processEvidence
21453
- });
22549
+ return failure2("invalid_input", "unknown state action");
21454
22550
  }
21455
22551
  task(request) {
21456
- return this.retryOmittedRevisions(request, () => this.taskOnce(request));
22552
+ const resolved = this.implicitTask("task", request);
22553
+ if (!resolved.ok)
22554
+ return resolved;
22555
+ const value = resolved.data;
22556
+ return this.retryOmittedRevisions(value, () => this.taskOnce(value));
21457
22557
  }
21458
22558
  taskOnce(request) {
21459
22559
  const root = this.contextRootError();
@@ -21555,7 +22655,11 @@ class WorkitCore {
21555
22655
  }
21556
22656
  }
21557
22657
  policy(request) {
21558
- return this.retryOmittedRevisions(request, () => this.policyOnce(request));
22658
+ const resolved = this.implicitTask("policy", request);
22659
+ if (!resolved.ok)
22660
+ return resolved;
22661
+ const value = resolved.data;
22662
+ return this.retryOmittedRevisions(value, () => this.policyOnce(value));
21559
22663
  }
21560
22664
  policyOnce(request) {
21561
22665
  const root = this.contextRootError();
@@ -21604,7 +22708,11 @@ class WorkitCore {
21604
22708
  return success(changed.data.revision, null, changed.data.policy);
21605
22709
  }
21606
22710
  evidence(request) {
21607
- return this.retryOmittedRevisions(request, () => this.evidenceOnce(request));
22711
+ const resolved = this.implicitTask("evidence", request);
22712
+ if (!resolved.ok)
22713
+ return resolved;
22714
+ const value = resolved.data;
22715
+ return this.retryOmittedRevisions(value, () => this.evidenceOnce(value));
21608
22716
  }
21609
22717
  evidenceOnce(request) {
21610
22718
  const root = this.contextRootError();
@@ -21745,13 +22853,16 @@ class WorkitCore {
21745
22853
  return success(changed.data.revision, null, changed.data.evidence.at(-1));
21746
22854
  }
21747
22855
  decision(request) {
21748
- return this.recordDecision(request);
22856
+ const resolved = this.implicitTask("decision", request);
22857
+ return resolved.ok ? this.recordDecision(resolved.data) : resolved;
21749
22858
  }
21750
22859
  observeDecision(request, observation) {
21751
- return this.recordDecision(request, observation, true);
22860
+ const resolved = this.implicitTask("decision", request);
22861
+ return resolved.ok ? this.recordDecision(resolved.data, observation, true) : resolved;
21752
22862
  }
21753
22863
  observeStandingDecision(request) {
21754
- return this.recordDecision(request, undefined, true, true);
22864
+ const resolved = this.implicitTask("decision", request);
22865
+ return resolved.ok ? this.recordDecision(resolved.data, undefined, true, true) : resolved;
21755
22866
  }
21756
22867
  commitTask(taskId, expected, filled, recheck, update) {
21757
22868
  let revision = expected;
@@ -21913,7 +23024,11 @@ class WorkitCore {
21913
23024
  return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
21914
23025
  }
21915
23026
  finding(request) {
21916
- return this.retryOmittedRevisions(request, () => this.findingOnce(request));
23027
+ const resolved = this.implicitTask("finding", request);
23028
+ if (!resolved.ok)
23029
+ return resolved;
23030
+ const value = resolved.data;
23031
+ return this.retryOmittedRevisions(value, () => this.findingOnce(value));
21917
23032
  }
21918
23033
  findingOnce(request) {
21919
23034
  const root = this.contextRootError();
@@ -22039,7 +23154,11 @@ class WorkitCore {
22039
23154
  return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
22040
23155
  }
22041
23156
  worker(request) {
22042
- return this.retryOmittedRevisions(request, () => this.workerOnce(request));
23157
+ const resolved = this.implicitTask("worker", request);
23158
+ if (!resolved.ok)
23159
+ return resolved;
23160
+ const value = resolved.data;
23161
+ return this.retryOmittedRevisions(value, () => this.workerOnce(value));
22043
23162
  }
22044
23163
  workerOnce(request) {
22045
23164
  const root = this.contextRootError();
@@ -22389,7 +23508,11 @@ class WorkitCore {
22389
23508
  return applied;
22390
23509
  }
22391
23510
  writer(request) {
22392
- return this.retryOmittedRevisions(request, () => this.writerOnce(request));
23511
+ const resolved = this.implicitTask("writer", request);
23512
+ if (!resolved.ok)
23513
+ return resolved;
23514
+ const value = resolved.data;
23515
+ return this.retryOmittedRevisions(value, () => this.writerOnce(value));
22393
23516
  }
22394
23517
  writerOnce(request) {
22395
23518
  const root = this.contextRootError();
@@ -23025,22 +24148,22 @@ function resolveGitRevision(root, value) {
23025
24148
  }
23026
24149
  function resolveInside(root, candidate) {
23027
24150
  const base = realpathSync7(root);
23028
- const target = path13.resolve(base, candidate);
23029
- if (target !== base && !target.startsWith(base + path13.sep)) {
24151
+ const target = path15.resolve(base, candidate);
24152
+ if (target !== base && !target.startsWith(base + path15.sep)) {
23030
24153
  throw new Error("path must stay inside repository root");
23031
24154
  }
23032
24155
  let ancestor = target;
23033
24156
  while (!existsSync5(ancestor))
23034
- ancestor = path13.dirname(ancestor);
24157
+ ancestor = path15.dirname(ancestor);
23035
24158
  const canonicalAncestor = realpathSync7(ancestor);
23036
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path13.sep)) {
24159
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path15.sep)) {
23037
24160
  throw new Error("path must stay inside repository root");
23038
24161
  }
23039
24162
  return target;
23040
24163
  }
23041
24164
  function run2(root, executable, args, env = {}) {
23042
24165
  const cwd = realpathSync7(root);
23043
- const result = spawnSync6(executable, args, {
24166
+ const result = spawnSync7(executable, args, {
23044
24167
  cwd,
23045
24168
  encoding: "utf8",
23046
24169
  env: { ...process.env, ...env }
@@ -23065,7 +24188,7 @@ var SUPPORT_MATRIX = {
23065
24188
 
23066
24189
  // packages/workit-mcp/src/server.ts
23067
24190
  import { existsSync as existsSync7, readFileSync as readFileSync8, realpathSync as realpathSync8 } from "node:fs";
23068
- import path17 from "node:path";
24191
+ import path19 from "node:path";
23069
24192
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
23070
24193
  function isZ4Schema(s) {
23071
24194
  const schema = s;
@@ -24837,7 +25960,7 @@ class Protocol {
24837
25960
  };
24838
25961
  }
24839
25962
  }
24840
- function isPlainObject2(value) {
25963
+ function isPlainObject3(value) {
24841
25964
  return value !== null && typeof value === "object" && !Array.isArray(value);
24842
25965
  }
24843
25966
  function mergeCapabilities(base, additional) {
@@ -24848,7 +25971,7 @@ function mergeCapabilities(base, additional) {
24848
25971
  if (addValue === undefined)
24849
25972
  continue;
24850
25973
  const baseValue = result[k];
24851
- if (isPlainObject2(baseValue) && isPlainObject2(addValue)) {
25974
+ if (isPlainObject3(baseValue) && isPlainObject3(addValue)) {
24852
25975
  result[k] = { ...baseValue, ...addValue };
24853
25976
  } else {
24854
25977
  result[k] = addValue;
@@ -25501,9 +26624,9 @@ var applyPatterns = (value) => {
25501
26624
  var redactSecrets = (value) => applyPatterns(value);
25502
26625
 
25503
26626
  // packages/workit-core/src/core/pr-create.ts
25504
- import fs11 from "node:fs";
25505
- import path14 from "node:path";
25506
- import { spawnSync as spawnSync7 } from "node:child_process";
26627
+ import fs13 from "node:fs";
26628
+ import path16 from "node:path";
26629
+ import { spawnSync as spawnSync8 } from "node:child_process";
25507
26630
  // packages/workit-core/src/forge/pr-body.ts
25508
26631
  function parseGhIssue(value) {
25509
26632
  const m = /issues\/(\d+)/.exec(value);
@@ -25513,13 +26636,13 @@ function parseGhIssue(value) {
25513
26636
  // packages/workit-core/src/core/pr-create.ts
25514
26637
  function whichOnPath(tool) {
25515
26638
  const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
25516
- for (const dir of (process.env.PATH ?? "").split(path14.delimiter)) {
26639
+ for (const dir of (process.env.PATH ?? "").split(path16.delimiter)) {
25517
26640
  if (!dir)
25518
26641
  continue;
25519
26642
  for (const name of names) {
25520
- const candidate = path14.join(dir, name);
26643
+ const candidate = path16.join(dir, name);
25521
26644
  try {
25522
- fs11.accessSync(candidate, fs11.constants.X_OK);
26645
+ fs13.accessSync(candidate, fs13.constants.X_OK);
25523
26646
  return candidate;
25524
26647
  } catch {}
25525
26648
  }
@@ -25528,7 +26651,7 @@ function whichOnPath(tool) {
25528
26651
  }
25529
26652
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
25530
26653
  var pushRemote = (cwd) => {
25531
- const result = spawnSync7("git", ["remote", "get-url", "--push", "--all", "origin"], {
26654
+ const result = spawnSync8("git", ["remote", "get-url", "--push", "--all", "origin"], {
25532
26655
  cwd,
25533
26656
  encoding: "utf8"
25534
26657
  });
@@ -25539,9 +26662,9 @@ var pushRemote = (cwd) => {
25539
26662
  };
25540
26663
 
25541
26664
  // packages/workit-core/src/core/repo-context.ts
25542
- import { spawnSync as spawnSync8 } from "node:child_process";
26665
+ import { spawnSync as spawnSync9 } from "node:child_process";
25543
26666
  import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
25544
- import path15 from "node:path";
26667
+ import path17 from "node:path";
25545
26668
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
25546
26669
  try {
25547
26670
  gitRevisionParts(value);
@@ -25568,7 +26691,7 @@ var invalidRange = (cwd) => ({
25568
26691
  cwd
25569
26692
  });
25570
26693
  var runGit = (cwd, args) => {
25571
- const result = spawnSync8("git", args, { cwd, encoding: "utf8" });
26694
+ const result = spawnSync9("git", args, { cwd, encoding: "utf8" });
25572
26695
  return {
25573
26696
  stdout: (result.stdout ?? "").trimEnd(),
25574
26697
  stderr: (result.stderr ?? "").trimEnd(),
@@ -25675,7 +26798,7 @@ var findPrTemplate = (cwd) => {
25675
26798
  ]) {
25676
26799
  const parts = rel.split("/");
25677
26800
  const base = parts[parts.length - 1];
25678
- const dir = parts.length > 1 ? path15.join(cwd, ...parts.slice(0, -1)) : cwd;
26801
+ const dir = parts.length > 1 ? path17.join(cwd, ...parts.slice(0, -1)) : cwd;
25679
26802
  const actual = probeCaseInsensitive(dir, base);
25680
26803
  if (actual)
25681
26804
  return [...parts.slice(0, -1), actual].join("/");
@@ -25706,7 +26829,7 @@ var diffStatForRange = (cwd, range) => {
25706
26829
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
25707
26830
  };
25708
26831
  var packageScripts = (cwd, keys) => {
25709
- const pkgPath = path15.join(cwd, "package.json");
26832
+ const pkgPath = path17.join(cwd, "package.json");
25710
26833
  if (!existsSync6(pkgPath))
25711
26834
  return [];
25712
26835
  try {
@@ -25732,9 +26855,9 @@ var documentationFiles = (cwd) => {
25732
26855
  if (entry.isDirectory()) {
25733
26856
  if (excluded.has(entry.name))
25734
26857
  continue;
25735
- walk(path15.join(dir, entry.name), depth + 1);
26858
+ walk(path17.join(dir, entry.name), depth + 1);
25736
26859
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
25737
- matches.push("./" + path15.relative(cwd, path15.join(dir, entry.name)).replaceAll("\\", "/"));
26860
+ matches.push("./" + path17.relative(cwd, path17.join(dir, entry.name)).replaceAll("\\", "/"));
25738
26861
  }
25739
26862
  }
25740
26863
  };
@@ -25750,7 +26873,7 @@ var readTrimmed2 = (file, maxLines) => {
25750
26873
  `);
25751
26874
  };
25752
26875
  function prReadyContext(root, range) {
25753
- const cwd = path15.resolve(repoRoot(root));
26876
+ const cwd = path17.resolve(repoRoot(root));
25754
26877
  if (range !== undefined && !isResolvableContextRange(cwd, range))
25755
26878
  return invalidRange(cwd);
25756
26879
  let stdout = "";
@@ -25808,7 +26931,7 @@ ${status.stderr}` : "");
25808
26931
  stdout += `template_path: ${template}
25809
26932
 
25810
26933
  `;
25811
- stdout += readTrimmed2(path15.join(cwd, template), 220);
26934
+ stdout += readTrimmed2(path17.join(cwd, template), 220);
25812
26935
  } else {
25813
26936
  stdout += `template_path: none
25814
26937
 
@@ -25818,7 +26941,7 @@ ${status.stderr}` : "");
25818
26941
  stdout += `
25819
26942
  `;
25820
26943
  stdout += printSection("Recent Validation Signals");
25821
- if (existsSync6(path15.join(cwd, "package.json"))) {
26944
+ if (existsSync6(path17.join(cwd, "package.json"))) {
25822
26945
  stdout += `package.json detected. Common scripts:
25823
26946
  `;
25824
26947
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -25827,7 +26950,7 @@ ${status.stderr}` : "");
25827
26950
  `) + `
25828
26951
  `;
25829
26952
  }
25830
- if (existsSync6(path15.join(cwd, "Cargo.toml")) || existsSync6(path15.join(cwd, "src-tauri/Cargo.toml"))) {
26953
+ if (existsSync6(path17.join(cwd, "Cargo.toml")) || existsSync6(path17.join(cwd, "src-tauri/Cargo.toml"))) {
25831
26954
  stdout += `Rust project detected.
25832
26955
  `;
25833
26956
  }
@@ -25860,7 +26983,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
25860
26983
  - Apply with the host-native editor, preserving the repository changelog conventions.
25861
26984
  - If Unreleased already has duplicate category headings, normalize_only first.`;
25862
26985
  function changelogContext(root, range) {
25863
- const cwd = path15.resolve(repoRoot(root));
26986
+ const cwd = path17.resolve(repoRoot(root));
25864
26987
  if (range !== undefined && !isResolvableContextRange(cwd, range))
25865
26988
  return invalidRange(cwd);
25866
26989
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -25878,7 +27001,7 @@ function changelogContext(root, range) {
25878
27001
  stdout += CHANGELOG_RULES + `
25879
27002
  `;
25880
27003
  stdout += printSection("Existing CHANGELOG.md");
25881
- const changelogPath = path15.join(cwd, "CHANGELOG.md");
27004
+ const changelogPath = path17.join(cwd, "CHANGELOG.md");
25882
27005
  if (existsSync6(changelogPath)) {
25883
27006
  stdout += readTrimmed2(changelogPath, 260) + `
25884
27007
  `;
@@ -25898,7 +27021,7 @@ function changelogContext(root, range) {
25898
27021
  return { stdout, stderr: "", exitCode: 0, cwd };
25899
27022
  }
25900
27023
  function docsRefreshContext(root, range) {
25901
- const cwd = path15.resolve(repoRoot(root));
27024
+ const cwd = path17.resolve(repoRoot(root));
25902
27025
  if (range !== undefined && !isResolvableContextRange(cwd, range))
25903
27026
  return invalidRange(cwd);
25904
27027
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -25920,7 +27043,7 @@ function docsRefreshContext(root, range) {
25920
27043
  `) + `
25921
27044
  `;
25922
27045
  stdout += printSection("README Preview");
25923
- const readme = path15.join(cwd, "README.md");
27046
+ const readme = path17.join(cwd, "README.md");
25924
27047
  if (existsSync6(readme)) {
25925
27048
  stdout += readTrimmed2(readme, 220) + `
25926
27049
  `;
@@ -25929,7 +27052,7 @@ function docsRefreshContext(root, range) {
25929
27052
  `;
25930
27053
  }
25931
27054
  stdout += printSection("Package Scripts");
25932
- const pkgPath = path15.join(cwd, "package.json");
27055
+ const pkgPath = path17.join(cwd, "package.json");
25933
27056
  if (existsSync6(pkgPath)) {
25934
27057
  try {
25935
27058
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
@@ -25943,7 +27066,7 @@ function docsRefreshContext(root, range) {
25943
27066
  return { stdout, stderr: "", exitCode: 0, cwd };
25944
27067
  }
25945
27068
  function releaseNotesContext(root, rangeOrTag) {
25946
- const cwd = path15.resolve(repoRoot(root));
27069
+ const cwd = path17.resolve(repoRoot(root));
25947
27070
  if (!rangeOrTag) {
25948
27071
  return { stdout: "", stderr: `ERROR: release tag or range required
25949
27072
  `, exitCode: 1, cwd };
@@ -25981,7 +27104,7 @@ function releaseNotesContext(root, rangeOrTag) {
25981
27104
  `;
25982
27105
  stdout += printSection("Existing Release Files");
25983
27106
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
25984
- if (existsSync6(path15.join(cwd, rel)))
27107
+ if (existsSync6(path17.join(cwd, rel)))
25985
27108
  stdout += rel + `
25986
27109
  `;
25987
27110
  }
@@ -26009,7 +27132,7 @@ function releaseNotesContext(root, rangeOrTag) {
26009
27132
  }
26010
27133
 
26011
27134
  // packages/workit-core/src/core/tracker-issues.ts
26012
- import { spawnSync as spawnSync9 } from "node:child_process";
27135
+ import { spawnSync as spawnSync10 } from "node:child_process";
26013
27136
  var repoPathFromRemote = (remote) => {
26014
27137
  let rest = (remote || "").trim().replace(/\/+$/, "");
26015
27138
  if (!rest)
@@ -26030,7 +27153,7 @@ var defaultCli = (bin, available) => {
26030
27153
  if (!available)
26031
27154
  return null;
26032
27155
  return (args, root) => {
26033
- const out = spawnSync9(bin, args, {
27156
+ const out = spawnSync10(bin, args, {
26034
27157
  cwd: root,
26035
27158
  encoding: "utf8",
26036
27159
  env: { ...process.env, PATH: process.env.PATH ?? "" }
@@ -26108,8 +27231,8 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
26108
27231
  };
26109
27232
 
26110
27233
  // packages/workit-core/src/core/youtrack.ts
26111
- import fs12 from "node:fs";
26112
- import path16 from "node:path";
27234
+ import fs14 from "node:fs";
27235
+ import path18 from "node:path";
26113
27236
 
26114
27237
  // packages/workit-core/src/core/templates.ts
26115
27238
  var repoRoot2 = assetRoot();
@@ -26122,22 +27245,22 @@ var resolveWorkspaceRoot = (explicit) => explicit || process.cwd();
26122
27245
  // packages/workit-core/src/core/youtrack.ts
26123
27246
  var ISSUE_RE = /^[A-Z]+-\d+$/;
26124
27247
  var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
26125
- var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path16.join(configDir(), "youtrack.json");
26126
- var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path16.join(resolveConfigDir(), "youtrack.json");
27248
+ var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path18.join(configDir(), "youtrack.json");
27249
+ var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path18.join(resolveConfigDir(), "youtrack.json");
26127
27250
  var youTrackTokenModeOk = (p) => {
26128
27251
  if (process.platform === "win32")
26129
27252
  return true;
26130
- const mode = fs12.statSync(p).mode & 511;
27253
+ const mode = fs14.statSync(p).mode & 511;
26131
27254
  return mode === 384;
26132
27255
  };
26133
27256
  function readYouTrackConfig(required) {
26134
27257
  const cfgPath = youTrackConfigPath();
26135
- if (!fs12.existsSync(cfgPath)) {
27258
+ if (!fs14.existsSync(cfgPath)) {
26136
27259
  return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
26137
27260
  }
26138
27261
  let parsed;
26139
27262
  try {
26140
- parsed = JSON.parse(fs12.readFileSync(cfgPath, "utf8"));
27263
+ parsed = JSON.parse(fs14.readFileSync(cfgPath, "utf8"));
26141
27264
  } catch {
26142
27265
  return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
26143
27266
  }
@@ -26148,11 +27271,11 @@ function readYouTrackConfig(required) {
26148
27271
  }
26149
27272
  var readYouTrackContextConfig = () => {
26150
27273
  const cfgPath = youTrackReadOnlyConfigPath();
26151
- if (!fs12.existsSync(cfgPath))
27274
+ if (!fs14.existsSync(cfgPath))
26152
27275
  return { data: {} };
26153
27276
  let parsed;
26154
27277
  try {
26155
- parsed = JSON.parse(fs12.readFileSync(cfgPath, "utf8"));
27278
+ parsed = JSON.parse(fs14.readFileSync(cfgPath, "utf8"));
26156
27279
  } catch {
26157
27280
  return { error: "YouTrack configuration is unavailable" };
26158
27281
  }
@@ -26191,12 +27314,12 @@ var youTrackToken = () => {
26191
27314
  if ("error" in loaded)
26192
27315
  return loaded;
26193
27316
  const tokenFile = String(loaded.config.tokenFile ?? "");
26194
- const tokenPath = tokenFile ? path16.isAbsolute(tokenFile) ? path16.resolve(tokenFile) : path16.resolve(process.cwd(), tokenFile) : "";
26195
- if (!tokenPath || !fs12.existsSync(tokenPath))
27317
+ const tokenPath = tokenFile ? path18.isAbsolute(tokenFile) ? path18.resolve(tokenFile) : path18.resolve(process.cwd(), tokenFile) : "";
27318
+ if (!tokenPath || !fs14.existsSync(tokenPath))
26196
27319
  return { error: "missing youtrack.token" };
26197
27320
  if (!youTrackTokenModeOk(tokenPath))
26198
27321
  return { error: "youtrack.token mode must be 0600" };
26199
- const token = fs12.readFileSync(tokenPath, "utf8").trim();
27322
+ const token = fs14.readFileSync(tokenPath, "utf8").trim();
26200
27323
  if (!token)
26201
27324
  return { error: "empty token file" };
26202
27325
  if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
@@ -26261,9 +27384,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
26261
27384
  } catch {
26262
27385
  continue;
26263
27386
  }
26264
- if (!fs12.existsSync(full))
27387
+ if (!fs14.existsSync(full))
26265
27388
  continue;
26266
- const text = fs12.readFileSync(full, "utf8");
27389
+ const text = fs14.readFileSync(full, "utf8");
26267
27390
  const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
26268
27391
  if (m)
26269
27392
  return m[1];
@@ -26557,13 +27680,13 @@ var rootVariants = (workspaceRoot) => {
26557
27680
  if (!workspaceRoot)
26558
27681
  return [];
26559
27682
  const roots = new Set;
26560
- for (const candidate of [workspaceRoot, path17.resolve(workspaceRoot)]) {
26561
- if (path17.isAbsolute(candidate) && candidate !== path17.parse(candidate).root)
27683
+ for (const candidate of [workspaceRoot, path19.resolve(workspaceRoot)]) {
27684
+ if (path19.isAbsolute(candidate) && candidate !== path19.parse(candidate).root)
26562
27685
  roots.add(candidate);
26563
27686
  }
26564
27687
  try {
26565
27688
  const canonical = realpathSync8(workspaceRoot);
26566
- if (canonical !== path17.parse(canonical).root)
27689
+ if (canonical !== path19.parse(canonical).root)
26567
27690
  roots.add(canonical);
26568
27691
  } catch {}
26569
27692
  return [...roots].toSorted((left, right) => right.length - left.length);
@@ -26664,11 +27787,11 @@ var thrownResult = (tool, error, workspaceRoot) => {
26664
27787
  };
26665
27788
  function createMcpServer(host, contextProvider) {
26666
27789
  assertMcpHost(host);
26667
- const here = path17.dirname(fileURLToPath2(import.meta.url));
27790
+ const here = path19.dirname(fileURLToPath2(import.meta.url));
26668
27791
  const sourceMarker = markSourcesLoaded([
26669
- path17.join(here, "server.ts"),
26670
- path17.join(here, "..", "..", "workit-core", "src", "core", "task-contract.ts"),
26671
- path17.join(here, "..", "..", "workit-core", "src", "core", "task-engine.ts")
27792
+ path19.join(here, "server.ts"),
27793
+ path19.join(here, "..", "..", "workit-core", "src", "core", "task-contract.ts"),
27794
+ path19.join(here, "..", "..", "workit-core", "src", "core", "task-engine.ts")
26672
27795
  ].filter((file) => existsSync7(file)));
26673
27796
  let staleWarned = false;
26674
27797
  const server = new Server({ name: "workit", version: VERSION }, { capabilities: { tools: {}, resources: {} } });
@@ -26862,6 +27985,7 @@ var signals = new Map;
26862
27985
  var cache = new Map;
26863
27986
  // packages/workit-core/src/hooks/handle.ts
26864
27987
  var offered = new Set;
27988
+ var CLAUDE_JUDGES = new Set(["workit:verifier", "workit:reviewer"]);
26865
27989
  // packages/workit-core/src/hooks/hosts/fields.ts
26866
27990
  var SHELLS = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
26867
27991
 
@@ -27006,8 +28130,8 @@ var codexQualification = (host) => host === "codex_cli" ? { surface: host, cli:
27006
28130
  var resolveCodexWorkspaceRoot = (pluginRoot, env) => {
27007
28131
  const explicit = env.WORKFLOW_WORKSPACE_ROOT;
27008
28132
  const inherited = env.PWD;
27009
- const candidate = explicit ?? (inherited && path18.resolve(inherited) !== path18.resolve(pluginRoot) ? inherited : undefined);
27010
- if (!candidate || !path18.isAbsolute(candidate) || !existsSync8(candidate))
28133
+ const candidate = explicit ?? (inherited && path20.resolve(inherited) !== path20.resolve(pluginRoot) ? inherited : undefined);
28134
+ if (!candidate || !path20.isAbsolute(candidate) || !existsSync8(candidate))
27011
28135
  return null;
27012
28136
  try {
27013
28137
  if (!statSync3(candidate).isDirectory())