@brainervirus/workit-cursor 2.1.5 → 2.2.1

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.
@@ -16785,7 +16785,7 @@ class StdioServerTransport {
16785
16785
  import { fileURLToPath as fileURLToPath2 } from "node:url";
16786
16786
 
16787
16787
  // packages/workit-core/src/core.ts
16788
- import { spawnSync as spawnSync3 } from "node:child_process";
16788
+ import { spawnSync as spawnSync4 } from "node:child_process";
16789
16789
  import { existsSync as existsSync5, realpathSync as realpathSync7 } from "node:fs";
16790
16790
  import path11 from "node:path";
16791
16791
 
@@ -16865,7 +16865,8 @@ var hostSchema = _enum([
16865
16865
  "codex_cli",
16866
16866
  "codex_desktop",
16867
16867
  "pi",
16868
- "workit_cli"
16868
+ "workit_cli",
16869
+ "claude_code"
16869
16870
  ]);
16870
16871
  var assuranceSchema = _enum(["enforced", "agent_guided", "unavailable"]);
16871
16872
  var outcomeSchema = _enum(["verified", "accepted_limitations", "stopped"]);
@@ -17315,15 +17316,70 @@ var taskRecordSchema = object2({
17315
17316
  decisions: array(entrySchema(decisionSchema)),
17316
17317
  actionProgress: actionProgressListSchema.optional(),
17317
17318
  findings: array(entrySchema(findingSchema)),
17318
- workers: array(entrySchema(workerSchema))
17319
+ workers: array(entrySchema(workerSchema)),
17320
+ critical: array(nonEmpty).optional()
17319
17321
  }).strict();
17322
+ var stripCount = (strips) => strips.reduce((sum, strip) => sum + strip.keys.length, 0);
17323
+ var strippable = (issues, prefix = []) => {
17324
+ const strips = [];
17325
+ for (const issue of issues) {
17326
+ if (issue.code === "unrecognized_keys") {
17327
+ strips.push({ path: [...prefix, ...issue.path], keys: issue.keys });
17328
+ continue;
17329
+ }
17330
+ if (issue.code !== "invalid_union")
17331
+ return null;
17332
+ const branch = issue.errors.map((errors) => strippable(errors, [...prefix, ...issue.path])).filter((found) => found !== null && found.length > 0).reduce((best, found) => best === null || stripCount(found) < stripCount(best) ? found : best, null);
17333
+ if (!branch)
17334
+ return null;
17335
+ strips.push(...branch);
17336
+ }
17337
+ return strips;
17338
+ };
17339
+ var recordPath = (path) => path.map((key) => typeof key === "number" ? "*" : String(key)).join(".");
17340
+ var overlaps = (left, right) => left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
17341
+ var parseStoredRecord = (schema, value) => {
17342
+ let parsed = schema.safeParse(value);
17343
+ if (parsed.success)
17344
+ return { success: true, data: parsed.data, stripped: [] };
17345
+ const first = { success: false, error: parsed.error, critical: [] };
17346
+ const declared = typeof value === "object" && value !== null && Array.isArray(value.critical) ? value.critical.filter((item) => typeof item === "string").map((item) => item.split(".").map((key) => /^\d+$/.test(key) ? "*" : key).join(".")) : [];
17347
+ const stripped = [];
17348
+ let current = structuredClone(value);
17349
+ for (let round = 0;round < 8 && !parsed.success; round++) {
17350
+ const strips = strippable(parsed.error.issues);
17351
+ if (strips === null || strips.length === 0)
17352
+ return first;
17353
+ for (const strip of strips) {
17354
+ let target = current;
17355
+ for (const key of strip.path)
17356
+ target = typeof target === "object" && target !== null ? target[key] : undefined;
17357
+ if (typeof target !== "object" || target === null)
17358
+ return first;
17359
+ for (const key of strip.keys) {
17360
+ stripped.push(recordPath([...strip.path, key]));
17361
+ delete target[key];
17362
+ }
17363
+ }
17364
+ parsed = schema.safeParse(current);
17365
+ }
17366
+ if (!parsed.success)
17367
+ return first;
17368
+ const critical = [
17369
+ ...new Set(stripped.filter((item) => declared.some((path) => overlaps(item, path))))
17370
+ ];
17371
+ if (critical.length > 0)
17372
+ return { ...first, critical };
17373
+ return { success: true, data: parsed.data, stripped: [...new Set(stripped)] };
17374
+ };
17320
17375
  var workspaceRecordSchema = object2({
17321
17376
  schemaVersion: literal(1),
17322
17377
  id,
17323
17378
  revision,
17324
17379
  root: nonEmpty,
17325
17380
  runtime: runtimeSchema.optional(),
17326
- writer: object2({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
17381
+ writer: object2({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable(),
17382
+ critical: array(nonEmpty).optional()
17327
17383
  }).strict();
17328
17384
  var capabilitySchema = object2({
17329
17385
  name: nonEmpty,
@@ -17561,6 +17617,11 @@ var operationSchemas = {
17561
17617
  writer: discriminatedUnion("action", Object.values(writerOperations)),
17562
17618
  state: discriminatedUnion("action", Object.values(stateOperations))
17563
17619
  };
17620
+ var { recover: _unadvertisedRecover, ...advertisedStateOperations } = stateOperations;
17621
+ var advertisedOperationSchemas = {
17622
+ ...operationSchemas,
17623
+ state: discriminatedUnion("action", Object.values(advertisedStateOperations))
17624
+ };
17564
17625
  var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
17565
17626
  var success = (revisionValue, workspaceRevisionValue, data) => ({
17566
17627
  ok: true,
@@ -17584,7 +17645,7 @@ function parseOperation(family, input) {
17584
17645
  return failure2("invalid_input", "operation input is invalid", { fields });
17585
17646
  }
17586
17647
  function operationJsonSchema(family) {
17587
- return toJSONSchema(operationSchemas[family], { target: "draft-2020-12" });
17648
+ return toJSONSchema(advertisedOperationSchemas[family], { target: "draft-2020-12" });
17588
17649
  }
17589
17650
  var OPERATION_SCHEMA_DEPTH = 1;
17590
17651
  var schemaShapeDescription = (value, depth = 0) => {
@@ -17776,8 +17837,7 @@ function requirementId(input) {
17776
17837
  // packages/workit-core/src/core/task-store.ts
17777
17838
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
17778
17839
  import { AsyncLocalStorage } from "node:async_hooks";
17779
- import * as fs4 from "node:fs";
17780
- import { hostname } from "node:os";
17840
+ import * as fs5 from "node:fs";
17781
17841
  import path4 from "node:path";
17782
17842
 
17783
17843
  // packages/workit-core/src/core/package-root.ts
@@ -18400,13 +18460,10 @@ function acquireFileLockSync(targetPath, options) {
18400
18460
  }
18401
18461
  }
18402
18462
  }
18403
- // packages/workit-core/src/core/task-store.ts
18404
- var processEvidenceSchema = object2({
18405
- state: _enum(["stopped", "accounted_for"]),
18406
- pid: number2().int().nonnegative(),
18407
- processStart: string2().nullable(),
18408
- ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
18409
- }).strict();
18463
+ // packages/workit-core/src/core/store-lock.ts
18464
+ import { spawnSync } from "node:child_process";
18465
+ import fs4 from "node:fs";
18466
+ import { hostname } from "node:os";
18410
18467
  var metadataLockSchema = object2({
18411
18468
  pid: number2().int().nonnegative().safe(),
18412
18469
  processStart: string2().nullable(),
@@ -18414,6 +18471,143 @@ var metadataLockSchema = object2({
18414
18471
  nonce: string2().min(1),
18415
18472
  externalAction: literal(true).optional()
18416
18473
  }).strict();
18474
+ var FOREIGN_LOCK_TTL_MS = 10 * 60000;
18475
+ var UNREADABLE_LOCK_TTL_MS = 30000;
18476
+ var RECLAIM_GUARD_TTL_MS = 30000;
18477
+ var defaultLockTimeoutMs = 250;
18478
+ var defaultLockTimeout = () => defaultLockTimeoutMs;
18479
+ var parseMetadataLock = (raw) => {
18480
+ let value;
18481
+ try {
18482
+ value = JSON.parse(raw);
18483
+ } catch {
18484
+ throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18485
+ }
18486
+ const parsed = metadataLockSchema.safeParse(value);
18487
+ if (!parsed.success)
18488
+ throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18489
+ return parsed.data;
18490
+ };
18491
+ var parseMetadataLockOrNull = (raw) => {
18492
+ try {
18493
+ return parseMetadataLock(raw);
18494
+ } catch {
18495
+ return null;
18496
+ }
18497
+ };
18498
+ var sameMetadataLock = (left, right) => {
18499
+ const parsed = metadataLockSchema.safeParse(left);
18500
+ return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
18501
+ };
18502
+ var parseProcStatStart = (stat) => {
18503
+ const close = stat.lastIndexOf(")");
18504
+ if (close < 0)
18505
+ return null;
18506
+ return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
18507
+ };
18508
+ var processStartOf = (pid) => {
18509
+ if (process.platform === "linux") {
18510
+ try {
18511
+ return parseProcStatStart(fs4.readFileSync(`/proc/${pid}/stat`, "utf8"));
18512
+ } catch {
18513
+ return null;
18514
+ }
18515
+ }
18516
+ if (process.platform === "darwin" || process.platform === "freebsd") {
18517
+ try {
18518
+ const run = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
18519
+ encoding: "utf8",
18520
+ timeout: 1000
18521
+ });
18522
+ const value = run.status === 0 ? run.stdout.trim() : "";
18523
+ return value || null;
18524
+ } catch {
18525
+ return null;
18526
+ }
18527
+ }
18528
+ return null;
18529
+ };
18530
+ var readTrimmed = (file) => {
18531
+ try {
18532
+ return fs4.readFileSync(file, "utf8").trim() || null;
18533
+ } catch {
18534
+ return null;
18535
+ }
18536
+ };
18537
+ var cachedLockHost = null;
18538
+ var localLockHost = () => {
18539
+ if (cachedLockHost !== null)
18540
+ return cachedLockHost;
18541
+ let pidns = null;
18542
+ try {
18543
+ pidns = /\[(\d+)\]/.exec(fs4.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
18544
+ } catch {}
18545
+ const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
18546
+ cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
18547
+ return cachedLockHost;
18548
+ };
18549
+ var pidAlive = (pid) => {
18550
+ if (!Number.isSafeInteger(pid) || pid <= 0)
18551
+ return false;
18552
+ try {
18553
+ process.kill(pid, 0);
18554
+ return true;
18555
+ } catch (error) {
18556
+ return error.code === "EPERM";
18557
+ }
18558
+ };
18559
+ var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
18560
+ const lock = metadataLockSchema.safeParse(payload);
18561
+ if (!lock.success)
18562
+ return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
18563
+ const { pid, processStart, host } = lock.data;
18564
+ const [lockName, lockSpace] = host.split("#");
18565
+ const [localName, localSpace] = localHost.split("#");
18566
+ if (lockSpace && localSpace && lockName === localName) {
18567
+ const [lockNs, lockBoot] = lockSpace.split(":");
18568
+ const [localNs, localBoot] = localSpace.split(":");
18569
+ if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
18570
+ return { state: "stale", reason: "lock was taken before this machine rebooted" };
18571
+ }
18572
+ if (host !== localHost)
18573
+ return ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS ? { state: "stale", reason: `lock from ${host} is older than its TTL` } : { state: "unknown", reason: `lock is held from ${host}; its pid cannot be checked here` };
18574
+ if (!pidAlive(pid))
18575
+ return { state: "stale", reason: `pid ${pid} is not running` };
18576
+ const currentStart = processStartOf(pid);
18577
+ if (processStart !== null && currentStart !== null && processStart !== currentStart)
18578
+ return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
18579
+ return { state: "live", reason: `held by running pid ${pid}` };
18580
+ };
18581
+ var ageOf = (file, nowMs) => {
18582
+ try {
18583
+ return nowMs - fs4.lstatSync(file).mtimeMs;
18584
+ } catch {
18585
+ return null;
18586
+ }
18587
+ };
18588
+ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
18589
+ const guard = `${lockPath}.reclaim`;
18590
+ const age = ageOf(guard, nowMs);
18591
+ if (age === null || age <= RECLAIM_GUARD_TTL_MS)
18592
+ return false;
18593
+ try {
18594
+ fs4.rmdirSync(guard);
18595
+ return true;
18596
+ } catch {
18597
+ return false;
18598
+ }
18599
+ };
18600
+
18601
+ // packages/workit-core/src/core/task-store.ts
18602
+ var RECOVERY_COPIES_PER_RECORD = 3;
18603
+ var STALE_TEMPORARY_MS = 60 * 60000;
18604
+ var RECOVERY_NAME = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/;
18605
+ var processEvidenceSchema = object2({
18606
+ state: _enum(["stopped", "accounted_for"]),
18607
+ pid: number2().int().nonnegative(),
18608
+ processStart: string2().nullable(),
18609
+ ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
18610
+ }).strict();
18417
18611
  var INDEX_VERSION = 1;
18418
18612
  var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
18419
18613
  var indexEntry = (task, file) => {
@@ -18441,7 +18635,7 @@ var indexEntry = (task, file) => {
18441
18635
  };
18442
18636
  var fileSignature = (file) => {
18443
18637
  try {
18444
- const stat = fs4.statSync(file, { bigint: true });
18638
+ const stat = fs5.statSync(file, { bigint: true });
18445
18639
  return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
18446
18640
  } catch {
18447
18641
  return null;
@@ -18458,7 +18652,7 @@ var cachedRuntimeVersion = null;
18458
18652
  var runtimeVersion = () => {
18459
18653
  if (cachedRuntimeVersion === null) {
18460
18654
  try {
18461
- const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
18655
+ const pkg = JSON.parse(fs5.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
18462
18656
  cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
18463
18657
  } catch {
18464
18658
  cachedRuntimeVersion = "0.0.0";
@@ -18487,36 +18681,20 @@ var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
18487
18681
  var isObject2 = (value) => typeof value === "object" && value !== null;
18488
18682
  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);
18489
18683
  var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
18490
- var parseMetadataLock = (raw) => {
18491
- let value;
18492
- try {
18493
- value = JSON.parse(raw);
18494
- } catch {
18495
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18496
- }
18497
- const parsed = metadataLockSchema.safeParse(value);
18498
- if (!parsed.success)
18499
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18500
- return parsed.data;
18501
- };
18502
- var sameMetadataLock = (left, right) => {
18503
- const parsed = metadataLockSchema.safeParse(left);
18504
- return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
18505
- };
18506
18684
  var sameDirectoryIdentity = (left, right) => {
18507
18685
  if (!path4.isAbsolute(left) || !path4.isAbsolute(right))
18508
18686
  return false;
18509
18687
  const normalizedLeft = path4.resolve(left);
18510
18688
  const normalizedRight = path4.resolve(right);
18511
18689
  try {
18512
- const a = fs4.statSync(normalizedLeft, { bigint: true });
18513
- const b = fs4.statSync(normalizedRight, { bigint: true });
18690
+ const a = fs5.statSync(normalizedLeft, { bigint: true });
18691
+ const b = fs5.statSync(normalizedRight, { bigint: true });
18514
18692
  if (!a.isDirectory() || !b.isDirectory())
18515
18693
  return false;
18516
18694
  if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
18517
18695
  return true;
18518
- const canonicalLeft = fs4.realpathSync(normalizedLeft);
18519
- const canonicalRight = fs4.realpathSync(normalizedRight);
18696
+ const canonicalLeft = fs5.realpathSync(normalizedLeft);
18697
+ const canonicalRight = fs5.realpathSync(normalizedRight);
18520
18698
  if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
18521
18699
  return true;
18522
18700
  return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
@@ -18524,12 +18702,47 @@ var sameDirectoryIdentity = (left, right) => {
18524
18702
  return false;
18525
18703
  }
18526
18704
  };
18705
+ var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
18706
+ var isTransientWindowsError = (error) => {
18707
+ if (process.platform !== "win32")
18708
+ return false;
18709
+ const code = error?.code;
18710
+ return typeof code === "string" && TRANSIENT_WINDOWS_CODES.has(code);
18711
+ };
18712
+ var retryTransient = (run) => {
18713
+ if (process.platform !== "win32")
18714
+ return run();
18715
+ for (let attempt = 0;; attempt += 1) {
18716
+ try {
18717
+ return run();
18718
+ } catch (error) {
18719
+ if (attempt >= 20 || !isTransientWindowsError(error))
18720
+ throw error;
18721
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 * (attempt + 1));
18722
+ }
18723
+ }
18724
+ };
18527
18725
  var externalActionLockRoots = new AsyncLocalStorage;
18726
+ var heldInProcess = new Map;
18727
+ var holdRoot = (root) => heldInProcess.set(root, (heldInProcess.get(root) ?? 0) + 1);
18728
+ var dropRoot = (root) => {
18729
+ const count = (heldInProcess.get(root) ?? 1) - 1;
18730
+ if (count > 0)
18731
+ heldInProcess.set(root, count);
18732
+ else
18733
+ heldInProcess.delete(root);
18734
+ };
18735
+ var dedupeCandidates = (candidates) => {
18736
+ const last = new Map(candidates.map((candidate, index) => [candidate.id, index]));
18737
+ return candidates.filter((candidate, index) => last.get(candidate.id) === index);
18738
+ };
18528
18739
 
18529
18740
  class TaskStore {
18530
18741
  root;
18531
- constructor(root) {
18532
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
18742
+ lockTimeoutMs;
18743
+ constructor(root, options = {}) {
18744
+ this.root = fs5.existsSync(root) ? fs5.realpathSync(root) : path4.resolve(root);
18745
+ this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
18533
18746
  }
18534
18747
  readTask(taskId) {
18535
18748
  if (!validId(taskId))
@@ -18549,11 +18762,11 @@ class TaskStore {
18549
18762
  return result.result;
18550
18763
  }
18551
18764
  listTasks() {
18552
- if (!fs4.existsSync(this.tasksDir))
18765
+ if (!fs5.existsSync(this.tasksDir))
18553
18766
  return success(null, null, []);
18554
18767
  let names;
18555
18768
  try {
18556
- names = fs4.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18769
+ names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18557
18770
  } catch (error) {
18558
18771
  return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
18559
18772
  path: this.tasksDir
@@ -18582,11 +18795,11 @@ class TaskStore {
18582
18795
  return success(null, null, tasks);
18583
18796
  }
18584
18797
  listTaskIndex() {
18585
- if (!fs4.existsSync(this.tasksDir))
18798
+ if (!fs5.existsSync(this.tasksDir))
18586
18799
  return success(null, null, []);
18587
18800
  let names;
18588
18801
  try {
18589
- names = fs4.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18802
+ names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18590
18803
  } catch (error) {
18591
18804
  return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
18592
18805
  path: this.tasksDir
@@ -18848,7 +19061,7 @@ class TaskStore {
18848
19061
  if (!current.data)
18849
19062
  return failure2("not_found", "workspace not found");
18850
19063
  if (current.data.revision !== expected)
18851
- return this.conflict(expected, current.data.revision);
19064
+ return this.workspaceConflict(expected, current.data.revision);
18852
19065
  const previousBytes = this.snapshotBytes(this.workspacePath);
18853
19066
  if (!previousBytes)
18854
19067
  return failure2("storage_error", "workspace snapshot disappeared during mutation");
@@ -18891,7 +19104,7 @@ class TaskStore {
18891
19104
  if (task.data.revision !== expected)
18892
19105
  return this.conflict(expected, task.data.revision);
18893
19106
  if (workspace.data.revision !== input.expectedWorkspaceRevision)
18894
- return this.conflict(input.expectedWorkspaceRevision, workspace.data.revision);
19107
+ return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
18895
19108
  const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
18896
19109
  const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
18897
19110
  if (!previousWorkspaceBytes || !previousTaskBytes)
@@ -18954,12 +19167,12 @@ class TaskStore {
18954
19167
  });
18955
19168
  }
18956
19169
  recoveryCandidates() {
18957
- if (!fs4.existsSync(this.recoveryDir))
19170
+ if (!fs5.existsSync(this.recoveryDir))
18958
19171
  return success(null, null, []);
18959
19172
  try {
18960
19173
  const candidates = [];
18961
- for (const name of fs4.readdirSync(this.recoveryDir)) {
18962
- const match = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/.exec(name);
19174
+ for (const name of fs5.readdirSync(this.recoveryDir)) {
19175
+ const match = RECOVERY_NAME.exec(name);
18963
19176
  if (match)
18964
19177
  candidates.push({
18965
19178
  target: match[1],
@@ -19007,7 +19220,7 @@ class TaskStore {
19007
19220
  return failure2("recovery_required", "validated recovery snapshot not found");
19008
19221
  selectedBytes = candidate;
19009
19222
  }
19010
- const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema) : this.parseBytes(selectedBytes, taskRecordSchema);
19223
+ const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
19011
19224
  if (!selectedRecord.ok)
19012
19225
  return selectedRecord;
19013
19226
  if (target === "workspace") {
@@ -19053,14 +19266,14 @@ class TaskStore {
19053
19266
  return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
19054
19267
  }
19055
19268
  if (target === "workspace") {
19056
- const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema);
19269
+ const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
19057
19270
  if (!parsed.ok)
19058
19271
  return parsed;
19059
19272
  const value = { ...parsed.data, revision: newRevision(), writer: null };
19060
19273
  const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
19061
19274
  return replaced.ok ? success(value.revision, value.revision, value) : replaced;
19062
19275
  }
19063
- const parsed = this.parseBytes(selectedBytes, taskRecordSchema);
19276
+ const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
19064
19277
  if (!parsed.ok)
19065
19278
  return parsed;
19066
19279
  const value = {
@@ -19071,7 +19284,7 @@ class TaskStore {
19071
19284
  };
19072
19285
  const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
19073
19286
  return replaced.ok ? success(value.revision, null, value) : replaced;
19074
- }, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate));
19287
+ }, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
19075
19288
  return result;
19076
19289
  } catch (error) {
19077
19290
  return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
@@ -19106,12 +19319,12 @@ class TaskStore {
19106
19319
  }
19107
19320
  readLockSnapshot() {
19108
19321
  try {
19109
- const stat = fs4.lstatSync(this.lockPath);
19322
+ const stat = fs5.lstatSync(this.lockPath);
19110
19323
  if (!stat.isFile() || stat.isSymbolicLink())
19111
19324
  return failure2("recovery_required", "metadata lock is not a regular file", {
19112
19325
  path: this.lockPath
19113
19326
  });
19114
- const raw = fs4.readFileSync(this.lockPath, "utf8");
19327
+ const raw = fs5.readFileSync(this.lockPath, "utf8");
19115
19328
  return success(null, null, { raw, data: parseMetadataLock(raw) });
19116
19329
  } catch (error) {
19117
19330
  if (error?.code === "ENOENT")
@@ -19122,22 +19335,50 @@ class TaskStore {
19122
19335
  }
19123
19336
  }
19124
19337
  metadataLockOptions() {
19338
+ const inProcess = heldInProcess.has(this.root);
19125
19339
  return {
19126
19340
  lockPath: this.lockPath,
19127
19341
  staleMs: Number.MAX_SAFE_INTEGER,
19128
- timeoutMs: 0,
19129
- retry: { retries: 0 },
19130
- staleRecovery: "fail-closed",
19131
- shouldReclaim: () => false,
19132
- parsePayload: parseMetadataLock,
19342
+ timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
19343
+ retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
19344
+ staleRecovery: "remove-if-unchanged",
19345
+ shouldReclaim: ({ payload, nowMs }) => {
19346
+ let ageMs = null;
19347
+ try {
19348
+ ageMs = nowMs - fs5.lstatSync(this.lockPath).mtimeMs;
19349
+ } catch {}
19350
+ return classifyLockOwner(payload, ageMs).state === "stale";
19351
+ },
19352
+ shouldRemoveStaleLock: () => true,
19353
+ parsePayload: parseMetadataLockOrNull,
19133
19354
  payload: () => ({
19134
19355
  pid: process.pid,
19135
- processStart: this.processStart(process.pid),
19136
- host: hostname(),
19356
+ processStart: processStartOf(process.pid),
19357
+ host: localLockHost(),
19137
19358
  nonce: randomUUID2()
19138
19359
  })
19139
19360
  };
19140
19361
  }
19362
+ acquireMetadataLock(options) {
19363
+ clearAbandonedReclaimGuard(this.lockPath);
19364
+ const deadline = Date.now() + (options.timeoutMs ?? 0);
19365
+ while (true) {
19366
+ try {
19367
+ return acquireFileLockSync(this.workspacePath, {
19368
+ ...options,
19369
+ timeoutMs: Math.max(0, deadline - Date.now())
19370
+ });
19371
+ } catch (error) {
19372
+ const code = error?.code;
19373
+ if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
19374
+ throw error;
19375
+ if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
19376
+ throw error;
19377
+ if (code !== "file_lock_stale")
19378
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
19379
+ }
19380
+ }
19381
+ }
19141
19382
  externalActionLockOptions() {
19142
19383
  const options = this.metadataLockOptions();
19143
19384
  return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
@@ -19155,10 +19396,11 @@ class TaskStore {
19155
19396
  }
19156
19397
  let handle;
19157
19398
  try {
19158
- handle = acquireFileLockSync(this.workspacePath, this.externalActionLockOptions());
19399
+ handle = this.acquireMetadataLock(this.externalActionLockOptions());
19159
19400
  } catch (error) {
19160
19401
  return this.lockFailure(error);
19161
19402
  }
19403
+ holdRoot(this.root);
19162
19404
  let result;
19163
19405
  try {
19164
19406
  if (!handle.verifyStillHeld())
@@ -19193,8 +19435,9 @@ class TaskStore {
19193
19435
  path: this.lockPath
19194
19436
  });
19195
19437
  }
19438
+ dropRoot(this.root);
19196
19439
  try {
19197
- handle.release();
19440
+ retryTransient(() => handle.release());
19198
19441
  } catch (error) {
19199
19442
  const released = this.lockFailure(error);
19200
19443
  const releaseError = released.ok ? "metadata lock release failed" : released.error;
@@ -19207,6 +19450,9 @@ class TaskStore {
19207
19450
  }
19208
19451
  recoveryLockOptions(observed, evidence, gate) {
19209
19452
  const options = this.metadataLockOptions();
19453
+ options.timeoutMs = 0;
19454
+ options.retry = { retries: 0 };
19455
+ options.parsePayload = parseMetadataLock;
19210
19456
  options.staleRecovery = "remove-if-unchanged";
19211
19457
  options.shouldReclaim = ({ payload }) => Boolean(observed && sameMetadataLock(payload, observed.data) && (evidence.state === "stopped" || evidence.state === "accounted_for"));
19212
19458
  options.shouldRemoveStaleLock = ({ raw, payload }) => {
@@ -19228,7 +19474,7 @@ class TaskStore {
19228
19474
  };
19229
19475
  this.replaceSnapshot(this.workspacePath, value, workspace);
19230
19476
  }
19231
- withLock(operation, options = this.metadataLockOptions()) {
19477
+ withLock(operation, options = this.metadataLockOptions(), contention = "busy") {
19232
19478
  try {
19233
19479
  this.initializeMutationStorage();
19234
19480
  } catch (error) {
@@ -19239,10 +19485,12 @@ class TaskStore {
19239
19485
  let handle;
19240
19486
  let result = failure2("storage_error", "metadata lock operation did not produce a result");
19241
19487
  try {
19242
- handle = acquireFileLockSync(this.workspacePath, options);
19488
+ handle = this.acquireMetadataLock(options);
19243
19489
  } catch (error) {
19244
- result = this.lockFailure(error);
19490
+ result = this.lockFailure(error, contention);
19245
19491
  }
19492
+ if (handle)
19493
+ holdRoot(this.root);
19246
19494
  if (handle) {
19247
19495
  try {
19248
19496
  if (!handle.verifyStillHeld())
@@ -19261,8 +19509,9 @@ class TaskStore {
19261
19509
  }
19262
19510
  }
19263
19511
  if (handle) {
19512
+ dropRoot(this.root);
19264
19513
  try {
19265
- handle.release();
19514
+ retryTransient(() => handle.release());
19266
19515
  } catch (error) {
19267
19516
  const releaseFailure = this.lockFailure(error);
19268
19517
  const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
@@ -19271,25 +19520,65 @@ class TaskStore {
19271
19520
  }
19272
19521
  return result;
19273
19522
  }
19274
- lockFailure(error) {
19523
+ lockDirWritable() {
19524
+ const probe = `${this.lockPath}.${process.pid}.${randomUUID2()}.probe`;
19525
+ try {
19526
+ fs5.closeSync(fs5.openSync(probe, "wx", 384));
19527
+ } catch {
19528
+ return false;
19529
+ }
19530
+ try {
19531
+ fs5.rmSync(probe, { force: true });
19532
+ } catch {}
19533
+ return true;
19534
+ }
19535
+ lockHolderPresent() {
19536
+ try {
19537
+ fs5.lstatSync(this.lockPath);
19538
+ return true;
19539
+ } catch (error) {
19540
+ return error?.code !== "ENOENT";
19541
+ }
19542
+ }
19543
+ lockFailure(error, contention = "busy") {
19275
19544
  const value = error;
19276
19545
  const code = typeof value?.code === "string" ? value.code : "";
19277
- if (code === "EEXIST" || code === "file_lock_timeout") {
19278
- const lock = this.readLockSnapshot();
19279
- if (lock.ok && lock.data?.data.externalAction)
19546
+ if (isTransientWindowsError(error) && !this.lockHolderPresent() && !this.lockDirWritable())
19547
+ return failure2("storage_error", `cannot create the workspace metadata lock (${code}); no other Workit call holds it`, {
19548
+ path: this.lockPath,
19549
+ guidance: `Check permissions and the read-only attribute on ${path4.dirname(this.lockPath)}.`
19550
+ });
19551
+ if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
19552
+ let lock = null;
19553
+ try {
19554
+ lock = parseMetadataLockOrNull(fs5.readFileSync(this.lockPath, "utf8"));
19555
+ } catch {}
19556
+ if (lock?.externalAction)
19280
19557
  return failure2("writer_conflict", "workspace is reserved by a managed external action", {
19281
19558
  outcome: "not_started",
19282
19559
  path: this.lockPath
19283
19560
  });
19561
+ if (contention === "busy")
19562
+ return failure2("busy", `workspace metadata lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
19563
+ outcome: "not_started",
19564
+ path: this.lockPath,
19565
+ guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
19566
+ });
19284
19567
  }
19285
19568
  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));
19286
19569
  return failure2(recovery ? "recovery_required" : "storage_error", `metadata lock operation failed: ${String(error)}`, { path: this.lockPath });
19287
19570
  }
19288
19571
  initializeMutationStorage() {
19289
- fs4.mkdirSync(this.tasksDir, { recursive: true });
19290
- fs4.mkdirSync(this.recoveryDir, { recursive: true });
19291
- fs4.writeFileSync(this.gitignorePath, `*
19292
- `);
19572
+ retryTransient(() => fs5.mkdirSync(this.tasksDir, { recursive: true }));
19573
+ retryTransient(() => fs5.mkdirSync(this.recoveryDir, { recursive: true }));
19574
+ let current = null;
19575
+ try {
19576
+ current = retryTransient(() => fs5.readFileSync(this.gitignorePath, "utf8"));
19577
+ } catch {}
19578
+ if (current !== `*
19579
+ `)
19580
+ retryTransient(() => fs5.writeFileSync(this.gitignorePath, `*
19581
+ `));
19293
19582
  }
19294
19583
  replaceSnapshot(file, value, previous) {
19295
19584
  let temporary;
@@ -19301,14 +19590,14 @@ class TaskStore {
19301
19590
  else if (typeof previous === "string")
19302
19591
  this.saveRecovery(file, previous);
19303
19592
  temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
19304
- const fd = fs4.openSync(temporary, "wx", 384);
19593
+ const fd = fs5.openSync(temporary, "wx", 384);
19305
19594
  try {
19306
- fs4.writeSync(fd, jsonBytes(value));
19307
- fs4.fsyncSync(fd);
19595
+ fs5.writeSync(fd, jsonBytes(value));
19596
+ fs5.fsyncSync(fd);
19308
19597
  } finally {
19309
- fs4.closeSync(fd);
19598
+ fs5.closeSync(fd);
19310
19599
  }
19311
- fs4.renameSync(temporary, file);
19600
+ retryTransient(() => fs5.renameSync(temporary, file));
19312
19601
  temporary = undefined;
19313
19602
  this.fsyncDirectory(path4.dirname(file));
19314
19603
  if (path4.dirname(file) === this.tasksDir)
@@ -19321,13 +19610,13 @@ class TaskStore {
19321
19610
  } finally {
19322
19611
  if (temporary)
19323
19612
  try {
19324
- fs4.unlinkSync(temporary);
19613
+ fs5.unlinkSync(temporary);
19325
19614
  } catch {}
19326
19615
  }
19327
19616
  }
19328
19617
  readIndex(workspaceId) {
19329
19618
  try {
19330
- const value = JSON.parse(fs4.readFileSync(this.indexPath, "utf8"));
19619
+ const value = JSON.parse(fs5.readFileSync(this.indexPath, "utf8"));
19331
19620
  if (!isObject2(value) || value.version !== INDEX_VERSION || value.workspaceId !== workspaceId || !isObject2(value.tasks))
19332
19621
  return {};
19333
19622
  const tasks = {};
@@ -19342,15 +19631,15 @@ class TaskStore {
19342
19631
  writeIndex(workspaceId, entries) {
19343
19632
  const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
19344
19633
  try {
19345
- fs4.writeFileSync(temporary, JSON.stringify({
19634
+ fs5.writeFileSync(temporary, JSON.stringify({
19346
19635
  version: INDEX_VERSION,
19347
19636
  workspaceId,
19348
19637
  tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
19349
19638
  }), { mode: 384, flag: "wx" });
19350
- fs4.renameSync(temporary, this.indexPath);
19639
+ retryTransient(() => fs5.renameSync(temporary, this.indexPath));
19351
19640
  } catch {
19352
19641
  try {
19353
- fs4.unlinkSync(temporary);
19642
+ fs5.unlinkSync(temporary);
19354
19643
  } catch {}
19355
19644
  }
19356
19645
  }
@@ -19368,33 +19657,153 @@ class TaskStore {
19368
19657
  const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
19369
19658
  const id = target === "task" ? path4.basename(file, ".json") : "workspace";
19370
19659
  const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
19371
- if (fs4.existsSync(destination)) {
19372
- if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
19373
- return;
19374
- throw new Error("recovery copy already exists with different bytes");
19660
+ if (fs5.existsSync(destination)) {
19661
+ if (digestBytes(retryTransient(() => fs5.readFileSync(destination))) !== digestBytes(bytes))
19662
+ throw new Error("recovery copy already exists with different bytes");
19663
+ const stamp = new Date;
19664
+ retryTransient(() => fs5.utimesSync(destination, stamp, stamp));
19665
+ this.pruneRecovery(`${target}.${id}.`, destination);
19666
+ return;
19375
19667
  }
19376
19668
  let temporary;
19377
19669
  try {
19378
19670
  temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
19379
- const fd = fs4.openSync(temporary, "wx", 384);
19671
+ const fd = fs5.openSync(temporary, "wx", 384);
19380
19672
  try {
19381
- fs4.writeSync(fd, bytes);
19382
- fs4.fsyncSync(fd);
19673
+ fs5.writeSync(fd, bytes);
19674
+ fs5.fsyncSync(fd);
19383
19675
  } finally {
19384
- fs4.closeSync(fd);
19676
+ fs5.closeSync(fd);
19385
19677
  }
19386
- fs4.renameSync(temporary, destination);
19678
+ retryTransient(() => fs5.renameSync(temporary, destination));
19387
19679
  temporary = undefined;
19388
19680
  this.fsyncDirectory(this.recoveryDir);
19681
+ this.pruneRecovery(`${target}.${id}.`, destination);
19389
19682
  } finally {
19390
19683
  if (temporary)
19391
19684
  try {
19392
- fs4.unlinkSync(temporary);
19685
+ fs5.unlinkSync(temporary);
19393
19686
  } catch {}
19394
19687
  }
19395
19688
  }
19689
+ pruneRecovery(prefix, keep) {
19690
+ try {
19691
+ const copies = this.recoveryCopies(prefix);
19692
+ const surplus = this.surplusCopies(copies, path4.basename(keep));
19693
+ for (const copy of surplus)
19694
+ fs5.rmSync(copy.path, { force: true });
19695
+ } catch {}
19696
+ }
19697
+ recoveryCopies(prefix = "") {
19698
+ if (!fs5.existsSync(this.recoveryDir))
19699
+ return [];
19700
+ const copies = [];
19701
+ for (const name of fs5.readdirSync(this.recoveryDir)) {
19702
+ if (!name.startsWith(prefix))
19703
+ continue;
19704
+ const match = RECOVERY_NAME.exec(name);
19705
+ if (!match)
19706
+ continue;
19707
+ const file = path4.join(this.recoveryDir, name);
19708
+ try {
19709
+ const stat = fs5.lstatSync(file, { bigint: true });
19710
+ if (!stat.isFile())
19711
+ continue;
19712
+ copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
19713
+ } catch {}
19714
+ }
19715
+ return copies;
19716
+ }
19717
+ surplusCopies(copies, keep) {
19718
+ 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);
19719
+ return ordered.slice(RECOVERY_COPIES_PER_RECORD);
19720
+ }
19721
+ collectGarbage(options = {}) {
19722
+ const dryRun = options.dryRun === true;
19723
+ const report = {
19724
+ dryRun,
19725
+ recovery: { removed: 0, removedBytes: 0, kept: 0 },
19726
+ temporary: { removed: 0 },
19727
+ candidates: { removed: 0, tasks: [], skippedActive: [], skippedClosed: [], failed: [] }
19728
+ };
19729
+ if (!fs5.existsSync(this.workitDir))
19730
+ return success(null, null, report);
19731
+ const sweep = () => {
19732
+ try {
19733
+ const groups = new Map;
19734
+ for (const copy of this.recoveryCopies())
19735
+ groups.set(copy.group, [...groups.get(copy.group) ?? [], copy]);
19736
+ for (const copies of groups.values()) {
19737
+ const surplus = this.surplusCopies(copies);
19738
+ report.recovery.kept += copies.length - surplus.length;
19739
+ for (const copy of surplus) {
19740
+ report.recovery.removed += 1;
19741
+ try {
19742
+ report.recovery.removedBytes += fs5.statSync(copy.path).size;
19743
+ } catch {}
19744
+ if (!dryRun)
19745
+ fs5.rmSync(copy.path, { force: true });
19746
+ }
19747
+ }
19748
+ const nowMs = Date.now();
19749
+ for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
19750
+ if (!fs5.existsSync(directory))
19751
+ continue;
19752
+ for (const name of fs5.readdirSync(directory)) {
19753
+ if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
19754
+ continue;
19755
+ const file = path4.join(directory, name);
19756
+ const stat = fs5.lstatSync(file);
19757
+ if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
19758
+ continue;
19759
+ report.temporary.removed += 1;
19760
+ if (!dryRun)
19761
+ fs5.rmSync(file, { force: true });
19762
+ }
19763
+ }
19764
+ return success(null, null, null);
19765
+ } catch (error) {
19766
+ return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
19767
+ path: this.recoveryDir
19768
+ });
19769
+ }
19770
+ };
19771
+ const pruned = dryRun ? sweep() : this.withLock(sweep);
19772
+ if (!pruned.ok)
19773
+ return pruned;
19774
+ const tasks = this.listTasks();
19775
+ if (!tasks.ok)
19776
+ return tasks;
19777
+ for (const task of tasks.data) {
19778
+ const deduped = dedupeCandidates(task.candidates);
19779
+ const removed = task.candidates.length - deduped.length;
19780
+ if (removed === 0)
19781
+ continue;
19782
+ if (task.status === "active") {
19783
+ report.candidates.skippedActive.push(task.id);
19784
+ continue;
19785
+ }
19786
+ if (task.status === "closed") {
19787
+ report.candidates.skippedClosed.push(task.id);
19788
+ continue;
19789
+ }
19790
+ if (!dryRun) {
19791
+ const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
19792
+ ...current,
19793
+ candidates: dedupeCandidates(current.candidates)
19794
+ }));
19795
+ if (!written.ok) {
19796
+ report.candidates.failed.push(task.id);
19797
+ continue;
19798
+ }
19799
+ }
19800
+ report.candidates.removed += removed;
19801
+ report.candidates.tasks.push(task.id);
19802
+ }
19803
+ return success(null, null, report);
19804
+ }
19396
19805
  findRecovery(target, taskId, digest, workspace) {
19397
- if (!fs4.existsSync(this.recoveryDir))
19806
+ if (!fs5.existsSync(this.recoveryDir))
19398
19807
  return null;
19399
19808
  const workspaceId = workspace?.id ?? this.trustedWorkspaceId();
19400
19809
  if (target === "workspace" && !workspaceId)
@@ -19406,9 +19815,9 @@ class TaskStore {
19406
19815
  if (candidate.target !== target || candidate.digest !== digest)
19407
19816
  continue;
19408
19817
  try {
19409
- if (!fs4.lstatSync(candidate.path).isFile())
19818
+ if (!fs5.lstatSync(candidate.path).isFile())
19410
19819
  continue;
19411
- const bytes = fs4.readFileSync(candidate.path);
19820
+ const bytes = fs5.readFileSync(candidate.path);
19412
19821
  if (digestBytes(bytes) !== digest)
19413
19822
  continue;
19414
19823
  if (target === "workspace") {
@@ -19426,11 +19835,11 @@ class TaskStore {
19426
19835
  }
19427
19836
  fsyncDirectory(directory) {
19428
19837
  try {
19429
- const fd = fs4.openSync(directory, "r");
19838
+ const fd = fs5.openSync(directory, "r");
19430
19839
  try {
19431
- fs4.fsyncSync(fd);
19840
+ fs5.fsyncSync(fd);
19432
19841
  } finally {
19433
- fs4.closeSync(fd);
19842
+ fs5.closeSync(fd);
19434
19843
  }
19435
19844
  } catch (error) {
19436
19845
  if (process.platform !== "win32")
@@ -19438,14 +19847,14 @@ class TaskStore {
19438
19847
  }
19439
19848
  }
19440
19849
  trustedWorkspaceId() {
19441
- if (!fs4.existsSync(this.tasksDir))
19850
+ if (!fs5.existsSync(this.tasksDir))
19442
19851
  return null;
19443
19852
  const ids = new Set;
19444
19853
  try {
19445
- for (const name of fs4.readdirSync(this.tasksDir)) {
19854
+ for (const name of fs5.readdirSync(this.tasksDir)) {
19446
19855
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
19447
19856
  return null;
19448
- const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
19857
+ const parsed = this.parseBytes(fs5.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
19449
19858
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
19450
19859
  return null;
19451
19860
  ids.add(parsed.data.workspaceId);
@@ -19457,14 +19866,14 @@ class TaskStore {
19457
19866
  }
19458
19867
  snapshotBytes(file) {
19459
19868
  try {
19460
- return fs4.readFileSync(file);
19869
+ return retryTransient(() => fs5.readFileSync(file));
19461
19870
  } catch {
19462
19871
  return null;
19463
19872
  }
19464
19873
  }
19465
19874
  readRecord(file, schema) {
19466
19875
  try {
19467
- const bytes = fs4.readFileSync(file, "utf8");
19876
+ const bytes = retryTransient(() => fs5.readFileSync(file, "utf8"));
19468
19877
  return { exists: true, result: this.parseBytes(bytes, schema) };
19469
19878
  } catch (error) {
19470
19879
  if (error?.code === "ENOENT")
@@ -19477,7 +19886,7 @@ class TaskStore {
19477
19886
  };
19478
19887
  }
19479
19888
  }
19480
- parseBytes(bytes, schema) {
19889
+ parseBytes(bytes, schema, mode = "read") {
19481
19890
  let value;
19482
19891
  try {
19483
19892
  value = JSON.parse(typeof bytes === "string" ? bytes : bytes.toString("utf8"));
@@ -19486,27 +19895,28 @@ class TaskStore {
19486
19895
  }
19487
19896
  if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
19488
19897
  return failure2("unsupported_version", "unsupported snapshot schema version");
19489
- const parsed = schema.safeParse(value);
19490
- if (parsed.success)
19898
+ const parsed = parseStoredRecord(schema, value);
19899
+ if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
19491
19900
  return success(null, null, parsed.data);
19492
19901
  const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
19902
+ const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
19903
+ if (parsed.success)
19904
+ return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
19905
+ if (parsed.critical.length > 0)
19906
+ return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
19493
19907
  if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
19494
19908
  return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
19495
19909
  return failure2("recovery_required", "snapshot does not satisfy its schema");
19496
19910
  }
19497
- processStart(pid) {
19498
- try {
19499
- return fs4.readFileSync(`/proc/${pid}/stat`, "utf8").split(" ")[21] ?? null;
19500
- } catch {
19501
- return null;
19502
- }
19503
- }
19504
19911
  conflict(expected, actual) {
19505
19912
  return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
19506
19913
  expectedRevision: expected,
19507
19914
  actualRevision: actual
19508
19915
  });
19509
19916
  }
19917
+ workspaceConflict(expected, actual) {
19918
+ return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
19919
+ }
19510
19920
  taskPath(taskId) {
19511
19921
  return path4.join(this.tasksDir, `${taskId}.json`);
19512
19922
  }
@@ -19532,53 +19942,116 @@ class TaskStore {
19532
19942
  return path4.join(this.workitDir, ".gitignore");
19533
19943
  }
19534
19944
  }
19535
- // packages/workit-core/src/core/task-engine.ts
19536
- import { realpathSync as realpathSync6 } from "node:fs";
19537
-
19538
- // packages/workit-core/src/core/authority.ts
19539
- import { createHash as createHash5 } from "node:crypto";
19540
- import * as fs6 from "node:fs";
19541
- import path9 from "node:path";
19542
-
19543
- // packages/workit-core/src/core/auto-approval.ts
19544
- import { createHash as createHash4 } from "node:crypto";
19545
- import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
19546
- import path8 from "node:path";
19547
-
19548
- // packages/workit-core/src/core/workspaces.ts
19549
- import { createHash as createHash3 } from "node:crypto";
19550
- import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
19551
- import path6 from "node:path";
19552
-
19553
- // packages/workit-core/src/core/config.ts
19554
- import {
19555
- copyFileSync,
19556
- cpSync,
19557
- existsSync as existsSync3,
19558
- mkdirSync as mkdirSync2,
19559
- readFileSync as readFileSync2,
19560
- readdirSync as readdirSync2,
19561
- writeFileSync as writeFileSync2
19562
- } from "node:fs";
19563
- import os from "node:os";
19564
- import path5 from "node:path";
19565
-
19566
- // packages/workit-core/src/core/boundary.ts
19567
- import { statSync as statSync2 } from "node:fs";
19568
- var EVENT = {
19569
- initialization: "initialization",
19570
- provenance: "provenance",
19571
- assets: "assets",
19572
- configurationSource: "configuration_source",
19573
- hooks: "hooks",
19574
- mcpConnection: "mcp_connection",
19575
- migration: "migration",
19576
- installSteps: "install_steps",
19577
- uncaughtFailure: "uncaught_failure",
19578
- toolsFailed: "tools_failed",
19579
- doctor: "doctor"
19945
+ // packages/workit-core/src/core/workers.ts
19946
+ var same = (left, right) => {
19947
+ try {
19948
+ return canonicalJson(left) === canonicalJson(right);
19949
+ } catch {
19950
+ return false;
19951
+ }
19580
19952
  };
19581
- var errorDetail = (err) => {
19953
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
19954
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
19955
+ var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
19956
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
19957
+ var validPath = (value) => typeof value === "string" && value.length > 0 && !Array.from(value).some((char) => {
19958
+ const code = char.charCodeAt(0);
19959
+ return code < 32 || code === 127;
19960
+ }) && !value.split(/[\\/]/).includes("..");
19961
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
19962
+ var workerIdOf = (value) => value.workerId ?? null;
19963
+ var currentWriterOwnsTask = (task, workspace, caller) => {
19964
+ const owner = workspace.writer?.owner;
19965
+ return task.status === "active" && task.workspaceId === workspace.id && owner?.taskId === task.id && owner.session.host === caller.host && owner.session.handle === caller.actor;
19966
+ };
19967
+ function assertProductWriteAllowed(input) {
19968
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
19969
+ return failure2("invalid_input", "invalid product write path");
19970
+ if (!input.store)
19971
+ return failure2("permission_denied", "write authorization requires core state");
19972
+ const task = input.store.readTask(input.task.id);
19973
+ if (!task.ok)
19974
+ return task;
19975
+ const workspace = input.store.readWorkspace();
19976
+ if (!workspace.ok)
19977
+ return workspace;
19978
+ if (!workspace.data)
19979
+ return failure2("not_found", "workspace not found");
19980
+ const workspaceRecord = workspace.data;
19981
+ if (task.data.status !== "active")
19982
+ return failure2("invalid_transition", "paused or closed tasks cannot own product writes");
19983
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
19984
+ return failure2("recovery_required", "task and workspace bindings are invalid");
19985
+ const writer = workspaceRecord.writer;
19986
+ if (!writer)
19987
+ return failure2("permission_denied", "checkout has no writer owner");
19988
+ if (writer.state === "uncertain")
19989
+ return failure2("recovery_required", "checkout writer requires recovery", {
19990
+ owner: writer.owner
19991
+ });
19992
+ const caller = callerValue(input.caller);
19993
+ const workerId = workerIdOf(input.caller);
19994
+ const owner = writer.owner;
19995
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
19996
+ return failure2("writer_conflict", "checkout is owned by another validated actor", { owner });
19997
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
19998
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
19999
+ return failure2("permission_denied", "only implementers can own product writes");
20000
+ if (workerId !== null && (!worker?.data.session || worker.data.state === "cancelling" || worker.data.state === "unknown" || worker.data.state === "stopped" || !same(worker.data.session, callerSession(input.caller))))
20001
+ return failure2("recovery_required", "worker session is not an active writer");
20002
+ return success(null, workspaceRecord.revision, owner);
20003
+ }
20004
+
20005
+ // packages/workit-core/src/core/task-evaluation.ts
20006
+ import { createHash as createHash6 } from "node:crypto";
20007
+ import * as fs8 from "node:fs";
20008
+ import { spawnSync as spawnSync3 } from "node:child_process";
20009
+ import path10 from "node:path";
20010
+
20011
+ // packages/workit-core/src/core/authority.ts
20012
+ import { createHash as createHash5 } from "node:crypto";
20013
+ import * as fs7 from "node:fs";
20014
+ import path9 from "node:path";
20015
+
20016
+ // packages/workit-core/src/core/auto-approval.ts
20017
+ import { createHash as createHash4 } from "node:crypto";
20018
+ import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
20019
+ import path8 from "node:path";
20020
+
20021
+ // packages/workit-core/src/core/workspaces.ts
20022
+ import { createHash as createHash3 } from "node:crypto";
20023
+ import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
20024
+ import path6 from "node:path";
20025
+
20026
+ // packages/workit-core/src/core/config.ts
20027
+ import {
20028
+ copyFileSync,
20029
+ cpSync,
20030
+ existsSync as existsSync3,
20031
+ mkdirSync as mkdirSync2,
20032
+ readFileSync as readFileSync2,
20033
+ readdirSync as readdirSync2,
20034
+ writeFileSync as writeFileSync2
20035
+ } from "node:fs";
20036
+ import os from "node:os";
20037
+ import path5 from "node:path";
20038
+
20039
+ // packages/workit-core/src/core/boundary.ts
20040
+ import { statSync as statSync2 } from "node:fs";
20041
+ var EVENT = {
20042
+ initialization: "initialization",
20043
+ provenance: "provenance",
20044
+ assets: "assets",
20045
+ configurationSource: "configuration_source",
20046
+ hooks: "hooks",
20047
+ mcpConnection: "mcp_connection",
20048
+ migration: "migration",
20049
+ installSteps: "install_steps",
20050
+ uncaughtFailure: "uncaught_failure",
20051
+ toolsFailed: "tools_failed",
20052
+ doctor: "doctor"
20053
+ };
20054
+ var errorDetail = (err) => {
19582
20055
  if (err instanceof Error) {
19583
20056
  return { error_name: err.name, error: err.message };
19584
20057
  }
@@ -20207,13 +20680,13 @@ var gitContext = (workspaceRoot, paths = []) => {
20207
20680
  };
20208
20681
 
20209
20682
  // packages/workit-core/src/core/vcs-config.ts
20210
- import fs5 from "node:fs";
20683
+ import fs6 from "node:fs";
20211
20684
  import path7 from "node:path";
20212
- import { spawnSync } from "node:child_process";
20685
+ import { spawnSync as spawnSync2 } from "node:child_process";
20213
20686
  var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
20214
20687
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
20215
20688
  var remoteProvider = (cwd) => {
20216
- const r = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
20689
+ const r = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
20217
20690
  if (r.status !== 0)
20218
20691
  return null;
20219
20692
  const url = (r.stdout ?? "").trim();
@@ -20250,7 +20723,7 @@ var remoteEndpoint = (remote) => {
20250
20723
  var configuredSshEndpoint = (host) => {
20251
20724
  if (host.startsWith("-"))
20252
20725
  return null;
20253
- const resolved = spawnSync("ssh", ["-G", host], {
20726
+ const resolved = spawnSync2("ssh", ["-G", host], {
20254
20727
  encoding: "utf8",
20255
20728
  timeout: 5000,
20256
20729
  maxBuffer: 1024 * 1024,
@@ -20300,7 +20773,7 @@ var readVcsConfig = () => {
20300
20773
  const file = vcsConfigPath();
20301
20774
  let raw;
20302
20775
  try {
20303
- raw = fs5.readFileSync(file, "utf8");
20776
+ raw = fs6.readFileSync(file, "utf8");
20304
20777
  } catch {
20305
20778
  return { status: "missing", path: file, config: {} };
20306
20779
  }
@@ -20401,7 +20874,7 @@ function vcsCliIdentity(cwd) {
20401
20874
  PATH: process.env.PATH ?? "",
20402
20875
  ...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
20403
20876
  };
20404
- const result = spawnSync(bin, ["api", "user"], {
20877
+ const result = spawnSync2(bin, ["api", "user"], {
20405
20878
  cwd: root,
20406
20879
  encoding: "utf8",
20407
20880
  env
@@ -20443,7 +20916,7 @@ function mergedPrStyle(limit = 6, cwd) {
20443
20916
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
20444
20917
  descriptionPreview: desc.slice(0, 600)
20445
20918
  });
20446
- const remoteResult = spawnSync("git", ["remote", "get-url", "--push", "--all", "origin"], {
20919
+ const remoteResult = spawnSync2("git", ["remote", "get-url", "--push", "--all", "origin"], {
20447
20920
  cwd,
20448
20921
  encoding: "utf8"
20449
20922
  });
@@ -20459,7 +20932,7 @@ function mergedPrStyle(limit = 6, cwd) {
20459
20932
  if (!hostingApiHostMatches(remote, host))
20460
20933
  return { ok: false, error: "PR style target does not match the provider host" };
20461
20934
  if (provider === "gitlab") {
20462
- const run = (args) => spawnSync("glab", ["api", ...args], {
20935
+ const run = (args) => spawnSync2("glab", ["api", ...args], {
20463
20936
  cwd,
20464
20937
  encoding: "utf8",
20465
20938
  env
@@ -20479,7 +20952,7 @@ function mergedPrStyle(limit = 6, cwd) {
20479
20952
  });
20480
20953
  }
20481
20954
  } else if (provider === "github") {
20482
- const r = spawnSync("gh", [
20955
+ const r = spawnSync2("gh", [
20483
20956
  "pr",
20484
20957
  "list",
20485
20958
  "--repo",
@@ -20538,66 +21011,6 @@ var verifyPushIdentity = (root, provider, account) => {
20538
21011
  return { ok: true };
20539
21012
  };
20540
21013
 
20541
- // packages/workit-core/src/core/workers.ts
20542
- var same = (left, right) => {
20543
- try {
20544
- return canonicalJson(left) === canonicalJson(right);
20545
- } catch {
20546
- return false;
20547
- }
20548
- };
20549
- var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
20550
- var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
20551
- var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
20552
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
20553
- var validPath = (value) => typeof value === "string" && value.length > 0 && !Array.from(value).some((char) => {
20554
- const code = char.charCodeAt(0);
20555
- return code < 32 || code === 127;
20556
- }) && !value.split(/[\\/]/).includes("..");
20557
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
20558
- var workerIdOf = (value) => value.workerId ?? null;
20559
- var currentWriterOwnsTask = (task, workspace, caller) => {
20560
- const owner = workspace.writer?.owner;
20561
- return task.status === "active" && task.workspaceId === workspace.id && owner?.taskId === task.id && owner.session.host === caller.host && owner.session.handle === caller.actor;
20562
- };
20563
- function assertProductWriteAllowed(input) {
20564
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
20565
- return failure2("invalid_input", "invalid product write path");
20566
- if (!input.store)
20567
- return failure2("permission_denied", "write authorization requires core state");
20568
- const task = input.store.readTask(input.task.id);
20569
- if (!task.ok)
20570
- return task;
20571
- const workspace = input.store.readWorkspace();
20572
- if (!workspace.ok)
20573
- return workspace;
20574
- if (!workspace.data)
20575
- return failure2("not_found", "workspace not found");
20576
- const workspaceRecord = workspace.data;
20577
- if (task.data.status !== "active")
20578
- return failure2("invalid_transition", "paused or closed tasks cannot own product writes");
20579
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
20580
- return failure2("recovery_required", "task and workspace bindings are invalid");
20581
- const writer = workspaceRecord.writer;
20582
- if (!writer)
20583
- return failure2("permission_denied", "checkout has no writer owner");
20584
- if (writer.state === "uncertain")
20585
- return failure2("recovery_required", "checkout writer requires recovery", {
20586
- owner: writer.owner
20587
- });
20588
- const caller = callerValue(input.caller);
20589
- const workerId = workerIdOf(input.caller);
20590
- const owner = writer.owner;
20591
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
20592
- return failure2("writer_conflict", "checkout is owned by another validated actor", { owner });
20593
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
20594
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
20595
- return failure2("permission_denied", "only implementers can own product writes");
20596
- if (workerId !== null && (!worker?.data.session || worker.data.state === "cancelling" || worker.data.state === "unknown" || worker.data.state === "stopped" || !same(worker.data.session, callerSession(input.caller))))
20597
- return failure2("recovery_required", "worker session is not an active writer");
20598
- return success(null, workspaceRecord.revision, owner);
20599
- }
20600
-
20601
21014
  // packages/workit-core/src/core/auto-approval.ts
20602
21015
  var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
20603
21016
  var OPERATION_CLASS = {
@@ -20884,7 +21297,7 @@ var digestBytes2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
20884
21297
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
20885
21298
  let root;
20886
21299
  try {
20887
- root = fs6.realpathSync(checkoutRoot);
21300
+ root = fs7.realpathSync(checkoutRoot);
20888
21301
  } catch {
20889
21302
  return failure2("permission_denied", "checkout root is unavailable");
20890
21303
  }
@@ -20907,8 +21320,8 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
20907
21320
  if (!segment)
20908
21321
  continue;
20909
21322
  current = path9.join(current, segment);
20910
- const stat = fs6.lstatSync(current);
20911
- const real = fs6.realpathSync(current);
21323
+ const stat = fs7.lstatSync(current);
21324
+ const real = fs7.realpathSync(current);
20912
21325
  if (real !== root && !real.startsWith(`${root}${path9.sep}`))
20913
21326
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
20914
21327
  fields: [
@@ -20925,7 +21338,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
20925
21338
  if (current !== target && !stat.isDirectory())
20926
21339
  return failure2("invalid_input", "document reference ancestor is not a directory");
20927
21340
  }
20928
- if (digestBytes2(fs6.readFileSync(target)) !== reference.digest)
21341
+ if (digestBytes2(fs7.readFileSync(target)) !== reference.digest)
20929
21342
  return failure2("permission_denied", "approved document bytes have changed");
20930
21343
  } catch {
20931
21344
  return failure2("invalid_input", "approved document is unavailable");
@@ -21288,18 +21701,41 @@ function reconcileAction(input) {
21288
21701
  var bindingCovers = scopeCovers;
21289
21702
  var verifyDecisionContent = verifyContentRefs;
21290
21703
 
21291
- // packages/workit-core/src/core/policy-resolver.ts
21292
- var resolverInputSchema = object2({
21293
- intent: intentSchema,
21294
- assessment: assessmentSchema,
21295
- constraints: array(constraintSchema),
21296
- prior: object2({
21297
- decisions: array(decisionSchema),
21298
- findings: array(findingSchema),
21299
- requirements: array(requirementSchema)
21300
- }).strict(),
21301
- preferences: object2({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
21302
- }).strict();
21704
+ // packages/workit-core/src/core/task-evaluation.ts
21705
+ var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
21706
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
21707
+ var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
21708
+ var canonicalPath = (value) => {
21709
+ if (typeof value !== "string" || !value)
21710
+ return null;
21711
+ if (value.includes("\\") || value.split("/").includes(".."))
21712
+ return null;
21713
+ const normalized = path10.posix.normalize(value);
21714
+ if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
21715
+ return null;
21716
+ return normalized === "." ? "." : normalized.replace(/\/$/, "");
21717
+ };
21718
+ var canonicalScope = (scope) => {
21719
+ const paths = scope.paths.length ? scope.paths : ["."];
21720
+ const normalizedPaths = paths.map(canonicalPath);
21721
+ const normalizedExclusions = scope.exclusions.map(canonicalPath);
21722
+ if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
21723
+ return null;
21724
+ return {
21725
+ description: scope.description,
21726
+ paths: [...new Set(normalizedPaths)],
21727
+ exclusions: [...new Set(normalizedExclusions)]
21728
+ };
21729
+ };
21730
+ var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
21731
+ var scopeMatches = (value, scope) => {
21732
+ const normalized = canonicalScope(scope);
21733
+ if (!normalized)
21734
+ return false;
21735
+ const paths = normalized.paths;
21736
+ return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
21737
+ };
21738
+ var pathRelevant = (value, scope) => scopeMatches(value, scope);
21303
21739
  var compareCodeUnits2 = (left, right) => {
21304
21740
  const length = Math.min(left.length, right.length);
21305
21741
  for (let index = 0;index < length; index += 1) {
@@ -21309,362 +21745,8 @@ var compareCodeUnits2 = (left, right) => {
21309
21745
  }
21310
21746
  return left.length - right.length;
21311
21747
  };
21312
- var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits2);
21313
- var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits2(canonicalJson(left), canonicalJson(right)));
21314
- var normalizedScope = (scope) => ({
21315
- description: scope.description,
21316
- paths: stableUnique(scope.paths),
21317
- exclusions: stableUnique(scope.exclusions)
21318
- });
21319
- var normalizedRefList = (values) => stableList(values);
21320
- var normalizeAssessment = (assessment) => ({
21321
- facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
21322
- signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
21323
- name,
21324
- { ...signal, refs: normalizedRefList(signal.refs) }
21325
- ])),
21326
- consequences: stableList(assessment.consequences.map((consequence) => ({
21327
- ...consequence,
21328
- fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
21329
- }))),
21330
- verification: stableList(assessment.verification.map((verification) => ({
21331
- ...verification,
21332
- scope: normalizedScope(verification.scope),
21333
- availableChecks: normalizedRefList(verification.availableChecks),
21334
- gaps: stableUnique(verification.gaps)
21335
- })))
21336
- });
21337
- var normalizeConstraint = (constraint) => ({
21338
- ...constraint,
21339
- source: constraint.source,
21340
- requires: stableList(constraint.requires.map((requirement) => ({
21341
- ...requirement,
21342
- scope: normalizedScope(requirement.scope),
21343
- refs: normalizedRefList(requirement.refs)
21344
- })))
21345
- });
21346
- var normalizeDecision = (decision) => ({
21347
- ...decision,
21348
- binding: {
21349
- ...decision.binding,
21350
- scope: normalizedScope(decision.binding.scope),
21351
- contentRefs: normalizedRefList(decision.binding.contentRefs)
21352
- },
21353
- requirementIds: stableUnique(decision.requirementIds)
21354
- });
21355
- var normalizeIntent = (intent) => ({
21356
- ...intent,
21357
- scope: normalizedScope(intent.scope),
21358
- authorityRefs: normalizedRefList(intent.authorityRefs)
21359
- });
21360
- var normalizePrior = (prior) => ({
21361
- decisions: stableList(prior.decisions.map(normalizeDecision)),
21362
- findings: stableList(prior.findings.map((finding) => ({
21363
- ...finding,
21364
- scope: normalizedScope(finding.scope),
21365
- refs: normalizedRefList(finding.refs),
21366
- resolution: finding.resolution ? {
21367
- ...finding.resolution,
21368
- evidenceIds: stableUnique(finding.resolution.evidenceIds),
21369
- decisionIds: stableUnique(finding.resolution.decisionIds)
21370
- } : null
21371
- }))),
21372
- requirements: stableList(prior.requirements.map((requirement) => ({
21373
- ...requirement,
21374
- scope: normalizedScope(requirement.scope)
21375
- })))
21376
- });
21377
- var invalidFields = (error) => ({
21378
- fields: error.issues.map((issue) => ({
21379
- path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
21380
- reason: issue.message
21381
- }))
21382
- });
21383
- function normalizeResolverInput(input) {
21384
- const parsed = resolverInputSchema.safeParse(input);
21385
- if (!parsed.success)
21386
- return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
21387
- const value = parsed.data;
21388
- const signals = value.assessment.signals;
21389
- if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
21390
- return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
21391
- fields: [
21392
- {
21393
- path: "assessment.signals",
21394
- reason: "contradictory behaviorChange and mechanicalLowRisk"
21395
- }
21396
- ]
21397
- });
21398
- const ids = value.constraints.map((constraint) => constraint.id);
21399
- if (new Set(ids).size !== ids.length)
21400
- return failure2("invalid_input", "constraint IDs must be unique", {
21401
- fields: [{ path: "constraints", reason: "duplicate constraint id" }]
21402
- });
21403
- if (value.preferences?.fast && value.preferences.thorough)
21404
- return failure2("invalid_input", "fast and thorough preferences contradict", {
21405
- fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
21406
- });
21407
- const normalized = {
21408
- intent: normalizeIntent(value.intent),
21409
- assessment: normalizeAssessment(value.assessment),
21410
- constraints: stableList(value.constraints.map(normalizeConstraint)),
21411
- prior: normalizePrior(value.prior),
21412
- ...value.preferences ? { preferences: { ...value.preferences } } : {},
21413
- policyVersion: POLICY_VERSION
21414
- };
21415
- return success(null, null, normalized);
21416
- }
21417
- var requirement = (spec) => ({
21418
- ...spec,
21419
- id: requirementId({
21420
- ruleId: spec.ruleId,
21421
- scope: spec.scope,
21422
- satisfaction: spec.satisfaction,
21423
- before: spec.before,
21424
- dependentAction: spec.dependentAction
21425
- })
21426
- });
21427
- var signalReason = (input, name) => input.assessment.signals[name].reason;
21428
- function resolveRequirements(input) {
21429
- const { signals } = input.assessment;
21430
- const scope = input.intent.scope;
21431
- const requirements = [];
21432
- const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
21433
- const hasUnknownFact = [
21434
- ...input.assessment.facts,
21435
- ...input.assessment.consequences.map(({ fact }) => fact)
21436
- ].some((fact) => fact.basis === "unknown");
21437
- if (hasUnknownSignal || hasUnknownFact)
21438
- requirements.push(requirement({
21439
- ruleId: "consequential-unknown",
21440
- dimension: "investigation",
21441
- scope,
21442
- reason: `Resolve consequential uncertainty before the dependent action: ${[
21443
- ...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
21444
- ...hasUnknownFact ? ["unknown fact"] : []
21445
- ].join(", ")}`,
21446
- satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
21447
- before: "dependent_action",
21448
- dependentAction: null,
21449
- acceptanceAllowed: false
21450
- }));
21451
- if (signals.productChoiceOpen.value === true)
21452
- requirements.push(requirement({
21453
- ruleId: "product-decision",
21454
- dimension: "decisions",
21455
- scope,
21456
- reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
21457
- satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
21458
- before: "dependent_action",
21459
- dependentAction: null,
21460
- acceptanceAllowed: false
21461
- }));
21462
- const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
21463
- if (signals.behaviorChange.value === true)
21464
- requirements.push(requirement({
21465
- ruleId: "behavioral-verification",
21466
- dimension: "testing",
21467
- scope,
21468
- reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
21469
- satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
21470
- before: "close",
21471
- dependentAction: null,
21472
- acceptanceAllowed: false
21473
- }), requirement({
21474
- ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
21475
- dimension: "review",
21476
- scope,
21477
- reason: independentBehaviorReview ? "Consequential behavior or the thorough preference requires fresh-context review." : "Bounded behavior changes use self-review unless consequential risks or trusted rules require more.",
21478
- satisfaction: independentBehaviorReview ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
21479
- before: "close",
21480
- dependentAction: null,
21481
- acceptanceAllowed: !independentBehaviorReview
21482
- }));
21483
- else if (signals.mechanicalLowRisk.value === true)
21484
- requirements.push(requirement({
21485
- ruleId: "mechanical-existing-checks",
21486
- dimension: "verification",
21487
- scope,
21488
- reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
21489
- satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
21490
- before: "close",
21491
- dependentAction: null,
21492
- acceptanceAllowed: true
21493
- }), requirement({
21494
- ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
21495
- dimension: "review",
21496
- scope,
21497
- reason: input.preferences?.thorough ? "The thorough preference upgrades mechanical self-review to fresh-context review." : "Mechanical low-risk work uses self-review rather than mandatory fresh-context review.",
21498
- satisfaction: input.preferences?.thorough ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
21499
- before: "close",
21500
- dependentAction: null,
21501
- acceptanceAllowed: !input.preferences?.thorough
21502
- }));
21503
- if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
21504
- requirements.push(requirement({
21505
- ruleId: "pre-pr-cleanup",
21506
- dimension: "verification",
21507
- scope,
21508
- reason: "Implementation output must be deslopped before it is delivered.",
21509
- satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
21510
- before: "dependent_action",
21511
- dependentAction: "hosting.pull_request",
21512
- acceptanceAllowed: true
21513
- }));
21514
- if (signals.durableAgreementNeeded.value === true)
21515
- requirements.push(requirement({
21516
- ruleId: "durable-spec",
21517
- dimension: "artifacts",
21518
- scope,
21519
- reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
21520
- satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
21521
- before: "write",
21522
- dependentAction: null,
21523
- acceptanceAllowed: true
21524
- }));
21525
- if (signals.coordinationPlanNeeded.value === true)
21526
- requirements.push(requirement({
21527
- ruleId: "coordination-plan",
21528
- dimension: "continuity",
21529
- scope,
21530
- reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
21531
- satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
21532
- before: "dependent_action",
21533
- dependentAction: null,
21534
- acceptanceAllowed: true
21535
- }));
21536
- if (signals.helperUseful.value === true && !input.preferences?.fast)
21537
- requirements.push(requirement({
21538
- ruleId: "helper-usefulness",
21539
- dimension: "delegation",
21540
- scope,
21541
- reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
21542
- satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
21543
- before: "dependent_action",
21544
- dependentAction: null,
21545
- acceptanceAllowed: true
21546
- }));
21547
- for (const constraint of input.constraints)
21548
- for (const obligation of constraint.requires) {
21549
- const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
21550
- requirements.push(requirement({
21551
- ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
21552
- kind: obligation.kind,
21553
- scope: normalizedScope(obligation.scope),
21554
- refs: normalizedRefList(obligation.refs),
21555
- method: obligation.method,
21556
- before: obligation.before,
21557
- dependentAction: obligation.dependentAction
21558
- })}`,
21559
- dimension,
21560
- scope: obligation.scope,
21561
- reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
21562
- satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
21563
- before: obligation.before,
21564
- dependentAction: obligation.dependentAction,
21565
- acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
21566
- }));
21567
- }
21568
- const seen = new Set;
21569
- return requirements.filter((item) => {
21570
- if (seen.has(item.id))
21571
- return false;
21572
- seen.add(item.id);
21573
- return true;
21574
- });
21575
- }
21576
- function resolvePolicy(input) {
21577
- const normalized = normalizeResolverInput(input);
21578
- if (!normalized.ok)
21579
- return normalized;
21580
- const requirements = resolveRequirements(normalized.data);
21581
- const policy = {
21582
- policyVersion: POLICY_VERSION,
21583
- inputDigest: sha256(canonicalJson(normalized.data)),
21584
- requirements
21585
- };
21586
- const parsed = policySchema.safeParse(policy);
21587
- if (!parsed.success)
21588
- return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
21589
- return success(null, null, parsed.data);
21590
- }
21591
- function diffPolicy(previous, next, reason, now) {
21592
- if (!policySchema.safeParse(previous).success && previous !== null)
21593
- throw new TypeError("previous policy is invalid");
21594
- if (!policySchema.safeParse(next).success)
21595
- throw new TypeError("next policy is invalid");
21596
- const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
21597
- const newIds = new Set(next.requirements.map((item) => item.id));
21598
- const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits2);
21599
- const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits2);
21600
- if (added.length === 0 && retired.length === 0)
21601
- return null;
21602
- const normalizedReason = reason.trim();
21603
- const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
21604
- const change = {
21605
- recordedAt: now,
21606
- fromInputDigest: previous?.inputDigest ?? null,
21607
- toInputDigest: next.inputDigest,
21608
- added,
21609
- retired,
21610
- reason: changeReason
21611
- };
21612
- const parsed = policyChangeSchema.safeParse(change);
21613
- if (!parsed.success)
21614
- throw new TypeError("policy change is invalid");
21615
- return parsed.data;
21616
- }
21617
-
21618
- // packages/workit-core/src/core/task-evaluation.ts
21619
- import { createHash as createHash6 } from "node:crypto";
21620
- import * as fs7 from "node:fs";
21621
- import { spawnSync as spawnSync2 } from "node:child_process";
21622
- import path10 from "node:path";
21623
- var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
21624
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
21625
- var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
21626
- var canonicalPath = (value) => {
21627
- if (typeof value !== "string" || !value)
21628
- return null;
21629
- if (value.includes("\\") || value.split("/").includes(".."))
21630
- return null;
21631
- const normalized = path10.posix.normalize(value);
21632
- if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
21633
- return null;
21634
- return normalized === "." ? "." : normalized.replace(/\/$/, "");
21635
- };
21636
- var canonicalScope = (scope) => {
21637
- const paths = scope.paths.length ? scope.paths : ["."];
21638
- const normalizedPaths = paths.map(canonicalPath);
21639
- const normalizedExclusions = scope.exclusions.map(canonicalPath);
21640
- if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
21641
- return null;
21642
- return {
21643
- description: scope.description,
21644
- paths: [...new Set(normalizedPaths)],
21645
- exclusions: [...new Set(normalizedExclusions)]
21646
- };
21647
- };
21648
- var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
21649
- var scopeMatches = (value, scope) => {
21650
- const normalized = canonicalScope(scope);
21651
- if (!normalized)
21652
- return false;
21653
- const paths = normalized.paths;
21654
- return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
21655
- };
21656
- var pathRelevant = (value, scope) => scopeMatches(value, scope);
21657
- var compareCodeUnits3 = (left, right) => {
21658
- const length = Math.min(left.length, right.length);
21659
- for (let index = 0;index < length; index += 1) {
21660
- const difference = left.charCodeAt(index) - right.charCodeAt(index);
21661
- if (difference !== 0)
21662
- return difference;
21663
- }
21664
- return left.length - right.length;
21665
- };
21666
21748
  var gitPaths = (root) => {
21667
- const result = spawnSync2("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
21749
+ const result = spawnSync3("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
21668
21750
  cwd: root,
21669
21751
  encoding: "buffer"
21670
21752
  });
@@ -21677,7 +21759,7 @@ var gitPaths = (root) => {
21677
21759
  };
21678
21760
  }
21679
21761
  const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")) : [];
21680
- const stagedDeleted = spawnSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
21762
+ const stagedDeleted = spawnSync3("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
21681
21763
  if (stagedDeleted.status !== 0) {
21682
21764
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
21683
21765
  if (!/not a git repository/i.test(stderr))
@@ -21690,7 +21772,7 @@ var gitPaths = (root) => {
21690
21772
  var walk = (root, directory, output) => {
21691
21773
  let names;
21692
21774
  try {
21693
- names = fs7.readdirSync(directory);
21775
+ names = fs8.readdirSync(directory);
21694
21776
  } catch {
21695
21777
  return true;
21696
21778
  }
@@ -21701,7 +21783,7 @@ var walk = (root, directory, output) => {
21701
21783
  const target = path10.join(directory, name);
21702
21784
  let stat;
21703
21785
  try {
21704
- stat = fs7.lstatSync(target);
21786
+ stat = fs8.lstatSync(target);
21705
21787
  } catch {
21706
21788
  uncertain = true;
21707
21789
  continue;
@@ -21720,10 +21802,10 @@ var scopeRoots = (root, scope) => {
21720
21802
  if (!inside(root, target))
21721
21803
  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` }] });
21722
21804
  let ancestor = target;
21723
- while (!fs7.existsSync(ancestor) && ancestor !== root)
21805
+ while (!fs8.existsSync(ancestor) && ancestor !== root)
21724
21806
  ancestor = path10.dirname(ancestor);
21725
21807
  try {
21726
- if (!inside(root, fs7.realpathSync(ancestor)))
21808
+ if (!inside(root, fs8.realpathSync(ancestor)))
21727
21809
  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` }] });
21728
21810
  } catch {
21729
21811
  return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
@@ -21735,7 +21817,7 @@ var scopeRoots = (root, scope) => {
21735
21817
  function captureCandidate(root, scope, environment = []) {
21736
21818
  let checkout;
21737
21819
  try {
21738
- checkout = fs7.realpathSync(root);
21820
+ checkout = fs8.realpathSync(root);
21739
21821
  } catch {
21740
21822
  return failure2("invalid_input", "candidate root does not exist");
21741
21823
  }
@@ -21753,11 +21835,11 @@ function captureCandidate(root, scope, environment = []) {
21753
21835
  if (item === ".git" || item === ".workit")
21754
21836
  continue;
21755
21837
  names.add(item);
21756
- if (!fs7.existsSync(target))
21838
+ if (!fs8.existsSync(target))
21757
21839
  continue;
21758
21840
  let stat;
21759
21841
  try {
21760
- stat = fs7.lstatSync(target);
21842
+ stat = fs8.lstatSync(target);
21761
21843
  } catch {
21762
21844
  uncertain = true;
21763
21845
  continue;
@@ -21775,15 +21857,15 @@ function captureCandidate(root, scope, environment = []) {
21775
21857
  continue;
21776
21858
  const target = path10.join(checkout, item);
21777
21859
  try {
21778
- const stat = fs7.lstatSync(target);
21860
+ const stat = fs8.lstatSync(target);
21779
21861
  if (stat.isSymbolicLink()) {
21780
- const link = fs7.readlinkSync(target);
21862
+ const link = fs8.readlinkSync(target);
21781
21863
  files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
21782
21864
  } else if (stat.isFile()) {
21783
21865
  files.push({
21784
21866
  path: item,
21785
21867
  kind: "file",
21786
- digest: digestBytes3(fs7.readFileSync(target)),
21868
+ digest: digestBytes3(fs8.readFileSync(target)),
21787
21869
  executable: (stat.mode & 73) !== 0
21788
21870
  });
21789
21871
  } else if (!stat.isDirectory()) {
@@ -21805,10 +21887,10 @@ function captureCandidate(root, scope, environment = []) {
21805
21887
  const namesByEnvironment = supplied.map(({ name }) => name);
21806
21888
  if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
21807
21889
  return failure2("invalid_input", "candidate environment names must be unique and non-empty");
21808
- const values = supplied.toSorted((left, right) => compareCodeUnits3(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
21890
+ const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
21809
21891
  if (values.some(({ value }) => value === null))
21810
21892
  completeness = "uncertain";
21811
- const headResult = spawnSync2("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
21893
+ const headResult = spawnSync3("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
21812
21894
  if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
21813
21895
  completeness = "uncertain";
21814
21896
  const head = headResult.status === 0 ? headResult.stdout.trim() : null;
@@ -22113,166 +22195,608 @@ function evaluateClosure(requestedOutcome, view) {
22113
22195
  });
22114
22196
  }
22115
22197
 
22116
- // packages/workit-core/src/core/methods.ts
22117
- var METHODS = {
22118
- "workit-challenge": { dimensions: ["challenge", "decisions"] },
22119
- "workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
22120
- "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
22121
- "workit-plan": { dimensions: ["artifacts", "continuity"] },
22122
- "workit-implement": { dimensions: ["delegation"] },
22123
- "workit-debug": {},
22124
- "workit-handoff": {},
22125
- "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
22126
- };
22127
- var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
22128
- var assuranceFor = (id, definition, capabilities) => {
22129
- const keys = new Set([
22130
- id,
22131
- ...definition.dimensions ?? [],
22132
- ...definition.ruleIds ?? []
22133
- ]);
22134
- const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
22135
- if (id === "workit-review" && matches.length === 0)
22136
- return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
22137
- if (matches.some((capability) => capability.assurance === "unavailable"))
22138
- return {
22139
- assurance: "unavailable",
22140
- reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
22141
- };
22142
- if (matches.some((capability) => capability.assurance === "enforced"))
22143
- return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
22144
- return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
22198
+ // packages/workit-core/src/core/methods.ts
22199
+ var METHODS = {
22200
+ "workit-challenge": { dimensions: ["challenge", "decisions"] },
22201
+ "workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
22202
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
22203
+ "workit-plan": { dimensions: ["artifacts", "continuity"] },
22204
+ "workit-implement": { dimensions: ["delegation"] },
22205
+ "workit-debug": {},
22206
+ "workit-handoff": {},
22207
+ "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
22208
+ };
22209
+ var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
22210
+ var assuranceFor = (id, definition, capabilities) => {
22211
+ const keys = new Set([
22212
+ id,
22213
+ ...definition.dimensions ?? [],
22214
+ ...definition.ruleIds ?? []
22215
+ ]);
22216
+ const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
22217
+ if (id === "workit-review" && matches.length === 0)
22218
+ return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
22219
+ if (matches.some((capability) => capability.assurance === "unavailable"))
22220
+ return {
22221
+ assurance: "unavailable",
22222
+ reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
22223
+ };
22224
+ if (matches.some((capability) => capability.assurance === "enforced"))
22225
+ return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
22226
+ return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
22227
+ };
22228
+ function selectMethods(policy, capabilities) {
22229
+ return Object.entries(METHODS).flatMap(([id, definition]) => {
22230
+ const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
22231
+ if (requirements.length === 0)
22232
+ return [];
22233
+ const capability = assuranceFor(id, definition, capabilities);
22234
+ const reasons = [
22235
+ ...new Set(requirements.map((requirement) => requirement.reason)),
22236
+ ...capability.reasons
22237
+ ];
22238
+ if (capability.assurance === "unavailable")
22239
+ reasons.unshift("required capability is unavailable");
22240
+ return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
22241
+ });
22242
+ }
22243
+
22244
+ // packages/workit-core/src/core/task-context.ts
22245
+ function reconcileResume(view, observations = []) {
22246
+ if (observations.length > 0)
22247
+ return failure2("permission_denied", "worker observations require native host verification");
22248
+ const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
22249
+ if (!candidate.ok)
22250
+ return candidate;
22251
+ const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
22252
+ const blockers = [...view.task.progress.blockers];
22253
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
22254
+ blockers.push({
22255
+ reason: "worker state requires reconciliation",
22256
+ dependentAction: "resume",
22257
+ refs: []
22258
+ });
22259
+ return success(null, null, {
22260
+ candidate: candidate.data,
22261
+ staleEvidenceIds,
22262
+ workerUpdates: [],
22263
+ blockers,
22264
+ reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
22265
+ });
22266
+ }
22267
+ var COMPACT_MAX_BYTES = 4096;
22268
+ var COMPACT_MAX_ITEMS = 8;
22269
+ var COMPACT_TEXT_BYTES = 128;
22270
+ var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
22271
+ var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
22272
+ var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
22273
+ var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
22274
+ const separator = match.search(/[:=]/u);
22275
+ if (separator >= 0)
22276
+ return `${match.slice(0, separator)}=<redacted>`;
22277
+ const space = match.search(/\s/u);
22278
+ return `${match.slice(0, space)} <redacted>`;
22279
+ }).slice(0, limit);
22280
+ var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
22281
+ var compactTimestamp = (value) => `${value.slice(0, 19)}.${value.slice(20, -1).padEnd(9, "0")}`;
22282
+ var compactDecisionChoice = (entry) => {
22283
+ const stated = entry.data.binding.statedChoice?.text;
22284
+ const approved = entry.data.response === "approved" ? entry.data.binding.approvedContent : "";
22285
+ const source = stated?.trim() || approved.trim() || null;
22286
+ return compactText(source, COMPACT_TEXT_BYTES);
22287
+ };
22288
+ var compactReference = (ref) => {
22289
+ if (ref.kind === "file")
22290
+ return {
22291
+ kind: "file",
22292
+ path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
22293
+ };
22294
+ if (ref.kind === "record")
22295
+ return {
22296
+ kind: "record",
22297
+ collection: ref.collection,
22298
+ id: ref.id
22299
+ };
22300
+ return { kind: ref.kind };
22301
+ };
22302
+ function compactTaskContext(view) {
22303
+ const decisions = view.task.decisions.slice().toSorted((left, right) => {
22304
+ const leftAt = compactTimestamp(left.recordedAt);
22305
+ const rightAt = compactTimestamp(right.recordedAt);
22306
+ if (leftAt !== rightAt)
22307
+ return leftAt > rightAt ? -1 : 1;
22308
+ return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
22309
+ }).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
22310
+ id: entry.id,
22311
+ purpose: entry.data.purpose,
22312
+ status: entry.data.response,
22313
+ digest: entry.data.digest,
22314
+ choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
22315
+ references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
22316
+ }));
22317
+ const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
22318
+ id: method.id,
22319
+ assurance: method.assurance,
22320
+ reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
22321
+ })) : [];
22322
+ const gaps = Array.from(new Set([
22323
+ ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
22324
+ ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
22325
+ ...view.task.progress.blockers.map((entry) => entry.reason)
22326
+ ])).toSorted().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
22327
+ const context = {
22328
+ objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
22329
+ status: view.task.status,
22330
+ decisions,
22331
+ methods,
22332
+ gaps,
22333
+ nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
22334
+ };
22335
+ let encoded = JSON.stringify(context);
22336
+ while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
22337
+ if (context.gaps.length)
22338
+ context.gaps.pop();
22339
+ else if (context.decisions.length)
22340
+ context.decisions.pop();
22341
+ else
22342
+ context.methods.pop();
22343
+ encoded = JSON.stringify(context);
22344
+ }
22345
+ if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
22346
+ context.objective = compactText(context.objective, 64) ?? "";
22347
+ context.nextAction = compactText(context.nextAction, 64);
22348
+ encoded = JSON.stringify(context);
22349
+ }
22350
+ return encoded;
22351
+ }
22352
+ var exportDigest = (bundle) => sha256({
22353
+ schemaVersion: bundle.schemaVersion,
22354
+ exportedAt: bundle.exportedAt,
22355
+ sourceWorkspaceId: bundle.sourceWorkspaceId,
22356
+ task: bundle.task
22357
+ });
22358
+ // packages/workit-core/src/core/external-action.ts
22359
+ var chainStepSchema = union([
22360
+ string2().min(1),
22361
+ object2({ branch: string2().min(1) }).strict(),
22362
+ object2({ pr: literal(true) }).strict()
22363
+ ]);
22364
+ var externalActionSchema = discriminatedUnion("operation", [
22365
+ object2({
22366
+ operation: literal("git.branch_setup"),
22367
+ payload: object2({
22368
+ action: _enum(["setup", "reapply_stash"]).optional(),
22369
+ sdd_dir: string2().optional(),
22370
+ target_branch: string2().optional(),
22371
+ stash: _enum(["yes", "no"]).optional(),
22372
+ cwd: string2().min(1).optional()
22373
+ }).strict()
22374
+ }).strict(),
22375
+ object2({
22376
+ operation: literal("git.commit"),
22377
+ payload: object2({
22378
+ message: string2().min(1).optional(),
22379
+ plan_steps: array(chainStepSchema).min(1).max(32).optional(),
22380
+ plan_branch: string2().min(1).optional(),
22381
+ cwd: string2().min(1).optional()
22382
+ }).strict()
22383
+ }).strict(),
22384
+ object2({
22385
+ operation: literal("git.push"),
22386
+ payload: object2({ branch: string2().min(1).optional(), cwd: string2().min(1).optional() }).strict()
22387
+ }).strict(),
22388
+ object2({
22389
+ operation: literal("hosting.pull_request"),
22390
+ payload: object2({
22391
+ title: string2().min(1),
22392
+ body: string2().optional(),
22393
+ draft: boolean2().optional(),
22394
+ target_branch: string2().optional(),
22395
+ babysit: boolean2().optional(),
22396
+ cwd: string2().min(1).optional()
22397
+ }).strict()
22398
+ }).strict(),
22399
+ object2({
22400
+ operation: literal("hosting.merge"),
22401
+ payload: object2({
22402
+ target_branch: string2().min(1).optional(),
22403
+ source_branch: string2().min(1).optional(),
22404
+ cwd: string2().min(1).optional()
22405
+ }).strict()
22406
+ }).strict(),
22407
+ object2({
22408
+ operation: literal("hosting.delete_branch"),
22409
+ payload: object2({ branch: string2().min(1), cwd: string2().min(1).optional() }).strict()
22410
+ }).strict(),
22411
+ object2({
22412
+ operation: literal("youtrack.update"),
22413
+ payload: object2({
22414
+ issueId: string2().min(1),
22415
+ markdown: string2().min(1),
22416
+ minutes: number2().positive().optional()
22417
+ }).strict()
22418
+ }).strict(),
22419
+ object2({
22420
+ operation: literal("youtrack.time"),
22421
+ payload: object2({
22422
+ issueId: string2().min(1),
22423
+ minutes: number2().positive(),
22424
+ text: string2().optional(),
22425
+ dateMs: number2().finite().optional()
22426
+ }).strict()
22427
+ }).strict(),
22428
+ object2({
22429
+ operation: literal("youtrack.meeting"),
22430
+ payload: object2({
22431
+ issueId: string2().min(1),
22432
+ minutes: number2().positive(),
22433
+ text: string2().min(1)
22434
+ }).strict()
22435
+ }).strict(),
22436
+ object2({
22437
+ operation: literal("changelog.apply"),
22438
+ payload: object2({
22439
+ entries: union([
22440
+ record(string2(), array(string2())),
22441
+ array(object2({ category: string2().min(1), text: string2().min(1) }).strict())
22442
+ ]).optional(),
22443
+ path: string2().optional(),
22444
+ normalize_only: boolean2().optional()
22445
+ }).strict()
22446
+ }).strict(),
22447
+ object2({
22448
+ operation: literal("context.read"),
22449
+ payload: object2({
22450
+ kind: _enum([
22451
+ "git",
22452
+ "pr",
22453
+ "youtrack",
22454
+ "github_issue",
22455
+ "gitlab_issue",
22456
+ "changelog",
22457
+ "release",
22458
+ "affected"
22459
+ ]),
22460
+ range: string2().optional(),
22461
+ issueId: string2().optional(),
22462
+ issueUrl: string2().optional(),
22463
+ issueRef: string2().optional(),
22464
+ mode: string2().optional(),
22465
+ specPath: string2().optional(),
22466
+ planPath: string2().optional(),
22467
+ cwd: string2().min(1).optional()
22468
+ }).strict()
22469
+ }).strict()
22470
+ ]);
22471
+ // packages/workit-core/src/core/task-engine.ts
22472
+ import { realpathSync as realpathSync6 } from "node:fs";
22473
+
22474
+ // packages/workit-core/src/core/policy-resolver.ts
22475
+ var resolverInputSchema = object2({
22476
+ intent: intentSchema,
22477
+ assessment: assessmentSchema,
22478
+ constraints: array(constraintSchema),
22479
+ prior: object2({
22480
+ decisions: array(decisionSchema),
22481
+ findings: array(findingSchema),
22482
+ requirements: array(requirementSchema)
22483
+ }).strict(),
22484
+ preferences: object2({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
22485
+ }).strict();
22486
+ var compareCodeUnits3 = (left, right) => {
22487
+ const length = Math.min(left.length, right.length);
22488
+ for (let index = 0;index < length; index += 1) {
22489
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
22490
+ if (difference !== 0)
22491
+ return difference;
22492
+ }
22493
+ return left.length - right.length;
22145
22494
  };
22146
- function selectMethods(policy, capabilities) {
22147
- return Object.entries(METHODS).flatMap(([id, definition]) => {
22148
- const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
22149
- if (requirements.length === 0)
22150
- return [];
22151
- const capability = assuranceFor(id, definition, capabilities);
22152
- const reasons = [
22153
- ...new Set(requirements.map((requirement) => requirement.reason)),
22154
- ...capability.reasons
22155
- ];
22156
- if (capability.assurance === "unavailable")
22157
- reasons.unshift("required capability is unavailable");
22158
- return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
22159
- });
22160
- }
22161
-
22162
- // packages/workit-core/src/core/task-context.ts
22163
- function reconcileResume(view, observations = []) {
22164
- if (observations.length > 0)
22165
- return failure2("permission_denied", "worker observations require native host verification");
22166
- const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
22167
- if (!candidate.ok)
22168
- return candidate;
22169
- const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
22170
- const blockers = [...view.task.progress.blockers];
22171
- if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
22172
- blockers.push({
22173
- reason: "worker state requires reconciliation",
22174
- dependentAction: "resume",
22175
- refs: []
22495
+ var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits3);
22496
+ var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits3(canonicalJson(left), canonicalJson(right)));
22497
+ var normalizedScope = (scope) => ({
22498
+ description: scope.description,
22499
+ paths: stableUnique(scope.paths),
22500
+ exclusions: stableUnique(scope.exclusions)
22501
+ });
22502
+ var normalizedRefList = (values) => stableList(values);
22503
+ var normalizeAssessment = (assessment) => ({
22504
+ facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
22505
+ signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
22506
+ name,
22507
+ { ...signal, refs: normalizedRefList(signal.refs) }
22508
+ ])),
22509
+ consequences: stableList(assessment.consequences.map((consequence) => ({
22510
+ ...consequence,
22511
+ fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
22512
+ }))),
22513
+ verification: stableList(assessment.verification.map((verification) => ({
22514
+ ...verification,
22515
+ scope: normalizedScope(verification.scope),
22516
+ availableChecks: normalizedRefList(verification.availableChecks),
22517
+ gaps: stableUnique(verification.gaps)
22518
+ })))
22519
+ });
22520
+ var normalizeConstraint = (constraint) => ({
22521
+ ...constraint,
22522
+ source: constraint.source,
22523
+ requires: stableList(constraint.requires.map((requirement) => ({
22524
+ ...requirement,
22525
+ scope: normalizedScope(requirement.scope),
22526
+ refs: normalizedRefList(requirement.refs)
22527
+ })))
22528
+ });
22529
+ var normalizeDecision = (decision) => ({
22530
+ ...decision,
22531
+ binding: {
22532
+ ...decision.binding,
22533
+ scope: normalizedScope(decision.binding.scope),
22534
+ contentRefs: normalizedRefList(decision.binding.contentRefs)
22535
+ },
22536
+ requirementIds: stableUnique(decision.requirementIds)
22537
+ });
22538
+ var normalizeIntent = (intent) => ({
22539
+ ...intent,
22540
+ scope: normalizedScope(intent.scope),
22541
+ authorityRefs: normalizedRefList(intent.authorityRefs)
22542
+ });
22543
+ var normalizePrior = (prior) => ({
22544
+ decisions: stableList(prior.decisions.map(normalizeDecision)),
22545
+ findings: stableList(prior.findings.map((finding) => ({
22546
+ ...finding,
22547
+ scope: normalizedScope(finding.scope),
22548
+ refs: normalizedRefList(finding.refs),
22549
+ resolution: finding.resolution ? {
22550
+ ...finding.resolution,
22551
+ evidenceIds: stableUnique(finding.resolution.evidenceIds),
22552
+ decisionIds: stableUnique(finding.resolution.decisionIds)
22553
+ } : null
22554
+ }))),
22555
+ requirements: stableList(prior.requirements.map((requirement) => ({
22556
+ ...requirement,
22557
+ scope: normalizedScope(requirement.scope)
22558
+ })))
22559
+ });
22560
+ var invalidFields = (error) => ({
22561
+ fields: error.issues.map((issue) => ({
22562
+ path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
22563
+ reason: issue.message
22564
+ }))
22565
+ });
22566
+ function normalizeResolverInput(input) {
22567
+ const parsed = resolverInputSchema.safeParse(input);
22568
+ if (!parsed.success)
22569
+ return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
22570
+ const value = parsed.data;
22571
+ const signals = value.assessment.signals;
22572
+ if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
22573
+ return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
22574
+ fields: [
22575
+ {
22576
+ path: "assessment.signals",
22577
+ reason: "contradictory behaviorChange and mechanicalLowRisk"
22578
+ }
22579
+ ]
22176
22580
  });
22177
- return success(null, null, {
22178
- candidate: candidate.data,
22179
- staleEvidenceIds,
22180
- workerUpdates: [],
22181
- blockers,
22182
- reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
22183
- });
22581
+ const ids = value.constraints.map((constraint) => constraint.id);
22582
+ if (new Set(ids).size !== ids.length)
22583
+ return failure2("invalid_input", "constraint IDs must be unique", {
22584
+ fields: [{ path: "constraints", reason: "duplicate constraint id" }]
22585
+ });
22586
+ if (value.preferences?.fast && value.preferences.thorough)
22587
+ return failure2("invalid_input", "fast and thorough preferences contradict", {
22588
+ fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
22589
+ });
22590
+ const normalized = {
22591
+ intent: normalizeIntent(value.intent),
22592
+ assessment: normalizeAssessment(value.assessment),
22593
+ constraints: stableList(value.constraints.map(normalizeConstraint)),
22594
+ prior: normalizePrior(value.prior),
22595
+ ...value.preferences ? { preferences: { ...value.preferences } } : {},
22596
+ policyVersion: POLICY_VERSION
22597
+ };
22598
+ return success(null, null, normalized);
22184
22599
  }
22185
- var COMPACT_MAX_BYTES = 4096;
22186
- var COMPACT_MAX_ITEMS = 8;
22187
- var COMPACT_TEXT_BYTES = 128;
22188
- var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
22189
- var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
22190
- var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
22191
- var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
22192
- const separator = match.search(/[:=]/u);
22193
- if (separator >= 0)
22194
- return `${match.slice(0, separator)}=<redacted>`;
22195
- const space = match.search(/\s/u);
22196
- return `${match.slice(0, space)} <redacted>`;
22197
- }).slice(0, limit);
22198
- var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
22199
- var compactTimestamp = (value) => `${value.slice(0, 19)}.${value.slice(20, -1).padEnd(9, "0")}`;
22200
- var compactDecisionChoice = (entry) => {
22201
- const stated = entry.data.binding.statedChoice?.text;
22202
- const approved = entry.data.response === "approved" ? entry.data.binding.approvedContent : "";
22203
- const source = stated?.trim() || approved.trim() || null;
22204
- return compactText(source, COMPACT_TEXT_BYTES);
22205
- };
22206
- var compactReference = (ref) => {
22207
- if (ref.kind === "file")
22208
- return {
22209
- kind: "file",
22210
- path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
22211
- };
22212
- if (ref.kind === "record")
22213
- return {
22214
- kind: "record",
22215
- collection: ref.collection,
22216
- id: ref.id
22217
- };
22218
- return { kind: ref.kind };
22219
- };
22220
- function compactTaskContext(view) {
22221
- const decisions = view.task.decisions.slice().toSorted((left, right) => {
22222
- const leftAt = compactTimestamp(left.recordedAt);
22223
- const rightAt = compactTimestamp(right.recordedAt);
22224
- if (leftAt !== rightAt)
22225
- return leftAt > rightAt ? -1 : 1;
22226
- return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
22227
- }).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
22228
- id: entry.id,
22229
- purpose: entry.data.purpose,
22230
- status: entry.data.response,
22231
- digest: entry.data.digest,
22232
- choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
22233
- references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
22234
- }));
22235
- const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
22236
- id: method.id,
22237
- assurance: method.assurance,
22238
- reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
22239
- })) : [];
22240
- const gaps = Array.from(new Set([
22241
- ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
22242
- ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
22243
- ...view.task.progress.blockers.map((entry) => entry.reason)
22244
- ])).toSorted().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
22245
- const context = {
22246
- objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
22247
- status: view.task.status,
22248
- decisions,
22249
- methods,
22250
- gaps,
22251
- nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
22600
+ var requirement = (spec) => ({
22601
+ ...spec,
22602
+ id: requirementId({
22603
+ ruleId: spec.ruleId,
22604
+ scope: spec.scope,
22605
+ satisfaction: spec.satisfaction,
22606
+ before: spec.before,
22607
+ dependentAction: spec.dependentAction
22608
+ })
22609
+ });
22610
+ var signalReason = (input, name) => input.assessment.signals[name].reason;
22611
+ function resolveRequirements(input) {
22612
+ const { signals } = input.assessment;
22613
+ const scope = input.intent.scope;
22614
+ const requirements = [];
22615
+ const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
22616
+ const hasUnknownFact = [
22617
+ ...input.assessment.facts,
22618
+ ...input.assessment.consequences.map(({ fact }) => fact)
22619
+ ].some((fact) => fact.basis === "unknown");
22620
+ if (hasUnknownSignal || hasUnknownFact)
22621
+ requirements.push(requirement({
22622
+ ruleId: "consequential-unknown",
22623
+ dimension: "investigation",
22624
+ scope,
22625
+ reason: `Resolve consequential uncertainty before the dependent action: ${[
22626
+ ...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
22627
+ ...hasUnknownFact ? ["unknown fact"] : []
22628
+ ].join(", ")}`,
22629
+ satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
22630
+ before: "dependent_action",
22631
+ dependentAction: null,
22632
+ acceptanceAllowed: false
22633
+ }));
22634
+ if (signals.productChoiceOpen.value === true)
22635
+ requirements.push(requirement({
22636
+ ruleId: "product-decision",
22637
+ dimension: "decisions",
22638
+ scope,
22639
+ reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
22640
+ satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
22641
+ before: "dependent_action",
22642
+ dependentAction: null,
22643
+ acceptanceAllowed: false
22644
+ }));
22645
+ const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
22646
+ if (signals.behaviorChange.value === true)
22647
+ requirements.push(requirement({
22648
+ ruleId: "behavioral-verification",
22649
+ dimension: "testing",
22650
+ scope,
22651
+ reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
22652
+ satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
22653
+ before: "close",
22654
+ dependentAction: null,
22655
+ acceptanceAllowed: false
22656
+ }), requirement({
22657
+ ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
22658
+ dimension: "review",
22659
+ scope,
22660
+ reason: independentBehaviorReview ? "Consequential behavior or the thorough preference requires fresh-context review." : "Bounded behavior changes use self-review unless consequential risks or trusted rules require more.",
22661
+ satisfaction: independentBehaviorReview ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
22662
+ before: "close",
22663
+ dependentAction: null,
22664
+ acceptanceAllowed: !independentBehaviorReview
22665
+ }));
22666
+ else if (signals.mechanicalLowRisk.value === true)
22667
+ requirements.push(requirement({
22668
+ ruleId: "mechanical-existing-checks",
22669
+ dimension: "verification",
22670
+ scope,
22671
+ reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
22672
+ satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
22673
+ before: "close",
22674
+ dependentAction: null,
22675
+ acceptanceAllowed: true
22676
+ }), requirement({
22677
+ ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
22678
+ dimension: "review",
22679
+ scope,
22680
+ reason: input.preferences?.thorough ? "The thorough preference upgrades mechanical self-review to fresh-context review." : "Mechanical low-risk work uses self-review rather than mandatory fresh-context review.",
22681
+ satisfaction: input.preferences?.thorough ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
22682
+ before: "close",
22683
+ dependentAction: null,
22684
+ acceptanceAllowed: !input.preferences?.thorough
22685
+ }));
22686
+ if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
22687
+ requirements.push(requirement({
22688
+ ruleId: "pre-pr-cleanup",
22689
+ dimension: "verification",
22690
+ scope,
22691
+ reason: "Implementation output must be deslopped before it is delivered.",
22692
+ satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
22693
+ before: "dependent_action",
22694
+ dependentAction: "hosting.pull_request",
22695
+ acceptanceAllowed: true
22696
+ }));
22697
+ if (signals.durableAgreementNeeded.value === true)
22698
+ requirements.push(requirement({
22699
+ ruleId: "durable-spec",
22700
+ dimension: "artifacts",
22701
+ scope,
22702
+ reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
22703
+ satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
22704
+ before: "write",
22705
+ dependentAction: null,
22706
+ acceptanceAllowed: true
22707
+ }));
22708
+ if (signals.coordinationPlanNeeded.value === true)
22709
+ requirements.push(requirement({
22710
+ ruleId: "coordination-plan",
22711
+ dimension: "continuity",
22712
+ scope,
22713
+ reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
22714
+ satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
22715
+ before: "dependent_action",
22716
+ dependentAction: null,
22717
+ acceptanceAllowed: true
22718
+ }));
22719
+ if (signals.helperUseful.value === true && !input.preferences?.fast)
22720
+ requirements.push(requirement({
22721
+ ruleId: "helper-usefulness",
22722
+ dimension: "delegation",
22723
+ scope,
22724
+ reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
22725
+ satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
22726
+ before: "dependent_action",
22727
+ dependentAction: null,
22728
+ acceptanceAllowed: true
22729
+ }));
22730
+ for (const constraint of input.constraints)
22731
+ for (const obligation of constraint.requires) {
22732
+ const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
22733
+ requirements.push(requirement({
22734
+ ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
22735
+ kind: obligation.kind,
22736
+ scope: normalizedScope(obligation.scope),
22737
+ refs: normalizedRefList(obligation.refs),
22738
+ method: obligation.method,
22739
+ before: obligation.before,
22740
+ dependentAction: obligation.dependentAction
22741
+ })}`,
22742
+ dimension,
22743
+ scope: obligation.scope,
22744
+ reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
22745
+ satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
22746
+ before: obligation.before,
22747
+ dependentAction: obligation.dependentAction,
22748
+ acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
22749
+ }));
22750
+ }
22751
+ const seen = new Set;
22752
+ return requirements.filter((item) => {
22753
+ if (seen.has(item.id))
22754
+ return false;
22755
+ seen.add(item.id);
22756
+ return true;
22757
+ });
22758
+ }
22759
+ function resolvePolicy(input) {
22760
+ const normalized = normalizeResolverInput(input);
22761
+ if (!normalized.ok)
22762
+ return normalized;
22763
+ const requirements = resolveRequirements(normalized.data);
22764
+ const policy = {
22765
+ policyVersion: POLICY_VERSION,
22766
+ inputDigest: sha256(canonicalJson(normalized.data)),
22767
+ requirements
22252
22768
  };
22253
- let encoded = JSON.stringify(context);
22254
- while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
22255
- if (context.gaps.length)
22256
- context.gaps.pop();
22257
- else if (context.decisions.length)
22258
- context.decisions.pop();
22259
- else
22260
- context.methods.pop();
22261
- encoded = JSON.stringify(context);
22262
- }
22263
- if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
22264
- context.objective = compactText(context.objective, 64) ?? "";
22265
- context.nextAction = compactText(context.nextAction, 64);
22266
- encoded = JSON.stringify(context);
22267
- }
22268
- return encoded;
22769
+ const parsed = policySchema.safeParse(policy);
22770
+ if (!parsed.success)
22771
+ return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
22772
+ return success(null, null, parsed.data);
22773
+ }
22774
+ function diffPolicy(previous, next, reason, now) {
22775
+ if (!policySchema.safeParse(previous).success && previous !== null)
22776
+ throw new TypeError("previous policy is invalid");
22777
+ if (!policySchema.safeParse(next).success)
22778
+ throw new TypeError("next policy is invalid");
22779
+ const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
22780
+ const newIds = new Set(next.requirements.map((item) => item.id));
22781
+ const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits3);
22782
+ const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits3);
22783
+ if (added.length === 0 && retired.length === 0)
22784
+ return null;
22785
+ const normalizedReason = reason.trim();
22786
+ const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
22787
+ const change = {
22788
+ recordedAt: now,
22789
+ fromInputDigest: previous?.inputDigest ?? null,
22790
+ toInputDigest: next.inputDigest,
22791
+ added,
22792
+ retired,
22793
+ reason: changeReason
22794
+ };
22795
+ const parsed = policyChangeSchema.safeParse(change);
22796
+ if (!parsed.success)
22797
+ throw new TypeError("policy change is invalid");
22798
+ return parsed.data;
22269
22799
  }
22270
- var exportDigest = (bundle) => sha256({
22271
- schemaVersion: bundle.schemaVersion,
22272
- exportedAt: bundle.exportedAt,
22273
- sourceWorkspaceId: bundle.sourceWorkspaceId,
22274
- task: bundle.task
22275
- });
22276
22800
 
22277
22801
  // packages/workit-core/src/core/task-engine.ts
22278
22802
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
@@ -22445,6 +22969,29 @@ var applyWorkerLifecycle = (input) => {
22445
22969
  return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
22446
22970
  };
22447
22971
  var refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }));
22972
+ var REVISION_RETRY_ATTEMPTS = 8;
22973
+ var retryPause = new Int32Array(new SharedArrayBuffer(4));
22974
+ var filledRevisions = (request) => {
22975
+ const value = typeof request === "object" && request !== null ? request : {};
22976
+ return {
22977
+ task: value.expectedRevision === undefined,
22978
+ workspace: value.expectedWorkspaceRevision === undefined
22979
+ };
22980
+ };
22981
+ var filledConflict = (result, filled) => !result.ok && result.code === "revision_conflict" && (("actualRevision" in result.details) && filled.task || ("actualWorkspaceRevision" in result.details) && filled.workspace);
22982
+ var retryFilledRevisions = (filled, run) => {
22983
+ if (!filled.task && !filled.workspace)
22984
+ return run();
22985
+ let result = run();
22986
+ const deadline = Date.now() + defaultLockTimeout();
22987
+ for (let attempt = 1;filledConflict(result, filled); attempt += 1) {
22988
+ if (attempt >= REVISION_RETRY_ATTEMPTS || Date.now() >= deadline)
22989
+ return failure2("busy", "records kept changing under concurrent writers; retry the call");
22990
+ Atomics.wait(retryPause, 0, 0, Math.floor(Math.random() * 4 * attempt) + 1);
22991
+ result = run();
22992
+ }
22993
+ return result;
22994
+ };
22448
22995
  var trustedNow = (context) => typeof context.now === "function" ? context.now() : context.now;
22449
22996
  var mapRef = (ref, ids) => {
22450
22997
  if (ref.kind !== "record")
@@ -22692,6 +23239,9 @@ class WorkitCore {
22692
23239
  if (input.expectedWorkspaceRevision === undefined)
22693
23240
  input.expectedWorkspaceRevision = workspace ? workspace.revision : null;
22694
23241
  }
23242
+ retryOmittedRevisions(request, run) {
23243
+ return retryFilledRevisions(filledRevisions(request), run);
23244
+ }
22695
23245
  helperEntry(task, requireSession = false, requireActive = false) {
22696
23246
  const workerId = this.context.workerId ?? null;
22697
23247
  if (workerId === null)
@@ -22723,6 +23273,9 @@ class WorkitCore {
22723
23273
  return null;
22724
23274
  }
22725
23275
  state(request) {
23276
+ return request?.action === "import" ? this.retryOmittedRevisions(request, () => this.stateOnce(request)) : this.stateOnce(request);
23277
+ }
23278
+ stateOnce(request) {
22726
23279
  const root = this.contextRootError();
22727
23280
  if (!root.ok)
22728
23281
  return root;
@@ -22824,6 +23377,9 @@ class WorkitCore {
22824
23377
  });
22825
23378
  }
22826
23379
  task(request) {
23380
+ return this.retryOmittedRevisions(request, () => this.taskOnce(request));
23381
+ }
23382
+ taskOnce(request) {
22827
23383
  const root = this.contextRootError();
22828
23384
  if (!root.ok)
22829
23385
  return root;
@@ -22923,6 +23479,9 @@ class WorkitCore {
22923
23479
  }
22924
23480
  }
22925
23481
  policy(request) {
23482
+ return this.retryOmittedRevisions(request, () => this.policyOnce(request));
23483
+ }
23484
+ policyOnce(request) {
22926
23485
  const root = this.contextRootError();
22927
23486
  if (!root.ok)
22928
23487
  return root;
@@ -22969,6 +23528,9 @@ class WorkitCore {
22969
23528
  return success(changed.data.revision, null, changed.data.policy);
22970
23529
  }
22971
23530
  evidence(request) {
23531
+ return this.retryOmittedRevisions(request, () => this.evidenceOnce(request));
23532
+ }
23533
+ evidenceOnce(request) {
22972
23534
  const root = this.contextRootError();
22973
23535
  if (!root.ok)
22974
23536
  return root;
@@ -23053,6 +23615,23 @@ class WorkitCore {
23053
23615
  observeStandingDecision(request) {
23054
23616
  return this.recordDecision(request, undefined, true, true);
23055
23617
  }
23618
+ commitTask(taskId, expected, filled, recheck, update) {
23619
+ let revision = expected;
23620
+ let first = true;
23621
+ return retryFilledRevisions({ task: filled, workspace: false }, () => {
23622
+ if (!first) {
23623
+ const fresh = this.store.readTask(taskId);
23624
+ if (!fresh.ok)
23625
+ return fresh;
23626
+ const checked = recheck(fresh.data);
23627
+ if (!checked.ok)
23628
+ return checked;
23629
+ revision = fresh.data.revision;
23630
+ }
23631
+ first = false;
23632
+ return this.store.mutateTask(taskId, revision, update, trustedNow(this.context));
23633
+ });
23634
+ }
23056
23635
  recordDecision(request, nativeObservation, nativeRequired = false, standing = false) {
23057
23636
  const root = this.contextRootError();
23058
23637
  if (!root.ok)
@@ -23077,6 +23656,7 @@ class WorkitCore {
23077
23656
  return task;
23078
23657
  if ((this.context.workerId ?? null) !== null)
23079
23658
  return failure2("permission_denied", "helpers cannot record or revoke decisions");
23659
+ const filled = input.expectedRevision === undefined;
23080
23660
  this.fillRevisions(input, task.data);
23081
23661
  if (input.action === "record") {
23082
23662
  if (task.data.status === "closed")
@@ -23125,7 +23705,20 @@ class WorkitCore {
23125
23705
  const nativeProvenance = native?.ok ? retireNativeAuthority(native.data) : null;
23126
23706
  if (native?.ok && !nativeProvenance)
23127
23707
  return failure2("permission_denied", "native decision authority was retired");
23128
- const changed = this.store.mutateTask(task.data.id, input.expectedRevision, (current, mutation) => {
23708
+ let standingProvenance = standingApproval?.ok === true ? standingApproval.data : null;
23709
+ const recheck = (fresh) => {
23710
+ if (fresh.status === "closed")
23711
+ return failure2("invalid_transition", "closed task cannot record a decision");
23712
+ if (!standingProvenance)
23713
+ return success(null, null, null);
23714
+ const again = verifyStandingApproval(this.store.root, fresh, this.context.caller, input.binding);
23715
+ if (again.ok)
23716
+ standingProvenance = again.data;
23717
+ return again;
23718
+ };
23719
+ const changed = this.commitTask(task.data.id, input.expectedRevision, filled, recheck, (current, mutation) => {
23720
+ if (current.status === "closed")
23721
+ return failure2("invalid_transition", "closed task cannot record a decision");
23129
23722
  const latestContent = verifyDecisionContent(this.store, input.binding);
23130
23723
  if (!latestContent.ok)
23131
23724
  return latestContent;
@@ -23137,14 +23730,14 @@ class WorkitCore {
23137
23730
  const entry = {
23138
23731
  id: newId(),
23139
23732
  recordedAt: mutation.now,
23140
- provenance: standingApproval?.ok === true ? standingApproval.data : nativeProvenance ?? provenance(this.context, "agent_reported"),
23733
+ provenance: standingProvenance ?? nativeProvenance ?? provenance(this.context, "agent_reported"),
23141
23734
  data
23142
23735
  };
23143
23736
  return success(mutation.revision, null, {
23144
23737
  ...current,
23145
23738
  decisions: [...current.decisions, entry]
23146
23739
  });
23147
- }, trustedNow(this.context));
23740
+ });
23148
23741
  if (!changed.ok)
23149
23742
  return changed;
23150
23743
  return success(changed.data.revision, null, changed.data.decisions.at(-1));
@@ -23158,7 +23751,7 @@ class WorkitCore {
23158
23751
  return failure2("invalid_transition", "decision is already revoked");
23159
23752
  if (decision.data.consumption)
23160
23753
  return failure2("permission_denied", "consumed decision cannot be revoked");
23161
- const changed = this.store.mutateTask(task.data.id, input.expectedRevision, (current, mutation) => {
23754
+ const changed = this.commitTask(task.data.id, input.expectedRevision, filled, () => success(null, null, null), (current, mutation) => {
23162
23755
  const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
23163
23756
  if (!entry)
23164
23757
  return failure2("not_found", "decision not found");
@@ -23175,13 +23768,16 @@ class WorkitCore {
23175
23768
  ...current,
23176
23769
  decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? updated : candidate)
23177
23770
  });
23178
- }, trustedNow(this.context));
23771
+ });
23179
23772
  if (!changed.ok)
23180
23773
  return changed;
23181
23774
  const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
23182
23775
  return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
23183
23776
  }
23184
23777
  finding(request) {
23778
+ return this.retryOmittedRevisions(request, () => this.findingOnce(request));
23779
+ }
23780
+ findingOnce(request) {
23185
23781
  const root = this.contextRootError();
23186
23782
  if (!root.ok)
23187
23783
  return root;
@@ -23305,6 +23901,9 @@ class WorkitCore {
23305
23901
  return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
23306
23902
  }
23307
23903
  worker(request) {
23904
+ return this.retryOmittedRevisions(request, () => this.workerOnce(request));
23905
+ }
23906
+ workerOnce(request) {
23308
23907
  const root = this.contextRootError();
23309
23908
  if (!root.ok)
23310
23909
  return root;
@@ -23652,6 +24251,9 @@ class WorkitCore {
23652
24251
  return applied;
23653
24252
  }
23654
24253
  writer(request) {
24254
+ return this.retryOmittedRevisions(request, () => this.writerOnce(request));
24255
+ }
24256
+ writerOnce(request) {
23655
24257
  const root = this.contextRootError();
23656
24258
  if (!root.ok)
23657
24259
  return root;
@@ -24254,122 +24856,6 @@ class WorkitCore {
24254
24856
  return this.summary(changed.data.task);
24255
24857
  }
24256
24858
  }
24257
-
24258
- // packages/workit-core/src/core/session-context.ts
24259
- var cache = new Map;
24260
- // packages/workit-core/src/core/external-action.ts
24261
- var chainStepSchema = union([
24262
- string2().min(1),
24263
- object2({ branch: string2().min(1) }).strict(),
24264
- object2({ pr: literal(true) }).strict()
24265
- ]);
24266
- var externalActionSchema = discriminatedUnion("operation", [
24267
- object2({
24268
- operation: literal("git.branch_setup"),
24269
- payload: object2({
24270
- action: _enum(["setup", "reapply_stash"]).optional(),
24271
- sdd_dir: string2().optional(),
24272
- target_branch: string2().optional(),
24273
- stash: _enum(["yes", "no"]).optional(),
24274
- cwd: string2().min(1).optional()
24275
- }).strict()
24276
- }).strict(),
24277
- object2({
24278
- operation: literal("git.commit"),
24279
- payload: object2({
24280
- message: string2().min(1).optional(),
24281
- plan_steps: array(chainStepSchema).min(1).max(32).optional(),
24282
- plan_branch: string2().min(1).optional(),
24283
- cwd: string2().min(1).optional()
24284
- }).strict()
24285
- }).strict(),
24286
- object2({
24287
- operation: literal("git.push"),
24288
- payload: object2({ branch: string2().min(1).optional(), cwd: string2().min(1).optional() }).strict()
24289
- }).strict(),
24290
- object2({
24291
- operation: literal("hosting.pull_request"),
24292
- payload: object2({
24293
- title: string2().min(1),
24294
- body: string2().optional(),
24295
- draft: boolean2().optional(),
24296
- target_branch: string2().optional(),
24297
- babysit: boolean2().optional(),
24298
- cwd: string2().min(1).optional()
24299
- }).strict()
24300
- }).strict(),
24301
- object2({
24302
- operation: literal("hosting.merge"),
24303
- payload: object2({
24304
- target_branch: string2().min(1).optional(),
24305
- source_branch: string2().min(1).optional(),
24306
- cwd: string2().min(1).optional()
24307
- }).strict()
24308
- }).strict(),
24309
- object2({
24310
- operation: literal("hosting.delete_branch"),
24311
- payload: object2({ branch: string2().min(1), cwd: string2().min(1).optional() }).strict()
24312
- }).strict(),
24313
- object2({
24314
- operation: literal("youtrack.update"),
24315
- payload: object2({
24316
- issueId: string2().min(1),
24317
- markdown: string2().min(1),
24318
- minutes: number2().positive().optional()
24319
- }).strict()
24320
- }).strict(),
24321
- object2({
24322
- operation: literal("youtrack.time"),
24323
- payload: object2({
24324
- issueId: string2().min(1),
24325
- minutes: number2().positive(),
24326
- text: string2().optional(),
24327
- dateMs: number2().finite().optional()
24328
- }).strict()
24329
- }).strict(),
24330
- object2({
24331
- operation: literal("youtrack.meeting"),
24332
- payload: object2({
24333
- issueId: string2().min(1),
24334
- minutes: number2().positive(),
24335
- text: string2().min(1)
24336
- }).strict()
24337
- }).strict(),
24338
- object2({
24339
- operation: literal("changelog.apply"),
24340
- payload: object2({
24341
- entries: union([
24342
- record(string2(), array(string2())),
24343
- array(object2({ category: string2().min(1), text: string2().min(1) }).strict())
24344
- ]).optional(),
24345
- path: string2().optional(),
24346
- normalize_only: boolean2().optional()
24347
- }).strict()
24348
- }).strict(),
24349
- object2({
24350
- operation: literal("context.read"),
24351
- payload: object2({
24352
- kind: _enum([
24353
- "git",
24354
- "pr",
24355
- "youtrack",
24356
- "github_issue",
24357
- "gitlab_issue",
24358
- "changelog",
24359
- "release",
24360
- "affected"
24361
- ]),
24362
- range: string2().optional(),
24363
- issueId: string2().optional(),
24364
- issueUrl: string2().optional(),
24365
- issueRef: string2().optional(),
24366
- mode: string2().optional(),
24367
- specPath: string2().optional(),
24368
- planPath: string2().optional(),
24369
- cwd: string2().min(1).optional()
24370
- }).strict()
24371
- }).strict()
24372
- ]);
24373
24859
  // packages/workit-core/src/core.ts
24374
24860
  var revision2 = /^[A-Za-z0-9@][A-Za-z0-9@._/~^{}-]*$/;
24375
24861
  function gitRevisionParts(value) {
@@ -24413,7 +24899,7 @@ function resolveInside(root, candidate) {
24413
24899
  }
24414
24900
  function run2(root, executable, args, env = {}) {
24415
24901
  const cwd = realpathSync7(root);
24416
- const result = spawnSync3(executable, args, {
24902
+ const result = spawnSync4(executable, args, {
24417
24903
  cwd,
24418
24904
  encoding: "utf8",
24419
24905
  env: { ...process.env, ...env }
@@ -24484,9 +24970,9 @@ var applyPatterns = (value) => {
24484
24970
  var redactSecrets = (value) => applyPatterns(value);
24485
24971
 
24486
24972
  // packages/workit-core/src/core/pr-create.ts
24487
- import fs8 from "node:fs";
24973
+ import fs9 from "node:fs";
24488
24974
  import path12 from "node:path";
24489
- import { spawnSync as spawnSync4 } from "node:child_process";
24975
+ import { spawnSync as spawnSync5 } from "node:child_process";
24490
24976
  function parseGhIssue(value) {
24491
24977
  const m = /issues\/(\d+)/.exec(value);
24492
24978
  return m ? m[1] : String(value).trim().replace(/^#/, "");
@@ -24499,7 +24985,7 @@ function whichOnPath(tool) {
24499
24985
  for (const name of names) {
24500
24986
  const candidate = path12.join(dir, name);
24501
24987
  try {
24502
- fs8.accessSync(candidate, fs8.constants.X_OK);
24988
+ fs9.accessSync(candidate, fs9.constants.X_OK);
24503
24989
  return candidate;
24504
24990
  } catch {}
24505
24991
  }
@@ -24508,7 +24994,7 @@ function whichOnPath(tool) {
24508
24994
  }
24509
24995
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
24510
24996
  var pushRemote = (cwd) => {
24511
- const result = spawnSync4("git", ["remote", "get-url", "--push", "--all", "origin"], {
24997
+ const result = spawnSync5("git", ["remote", "get-url", "--push", "--all", "origin"], {
24512
24998
  cwd,
24513
24999
  encoding: "utf8"
24514
25000
  });
@@ -24519,7 +25005,7 @@ var pushRemote = (cwd) => {
24519
25005
  };
24520
25006
 
24521
25007
  // packages/workit-core/src/core/repo-context.ts
24522
- import { spawnSync as spawnSync5 } from "node:child_process";
25008
+ import { spawnSync as spawnSync6 } from "node:child_process";
24523
25009
  import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
24524
25010
  import path13 from "node:path";
24525
25011
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
@@ -24548,7 +25034,7 @@ var invalidRange = (cwd) => ({
24548
25034
  cwd
24549
25035
  });
24550
25036
  var runGit = (cwd, args) => {
24551
- const result = spawnSync5("git", args, { cwd, encoding: "utf8" });
25037
+ const result = spawnSync6("git", args, { cwd, encoding: "utf8" });
24552
25038
  return {
24553
25039
  stdout: (result.stdout ?? "").trimEnd(),
24554
25040
  stderr: (result.stderr ?? "").trimEnd(),
@@ -24721,7 +25207,7 @@ var documentationFiles = (cwd) => {
24721
25207
  walk(cwd, 1);
24722
25208
  return matches.toSorted().slice(0, 200);
24723
25209
  };
24724
- var readTrimmed = (file, maxLines) => {
25210
+ var readTrimmed2 = (file, maxLines) => {
24725
25211
  if (!existsSync6(file))
24726
25212
  return "";
24727
25213
  const lines = readFileSync7(file, "utf8").split(`
@@ -24788,7 +25274,7 @@ ${status.stderr}` : "");
24788
25274
  stdout += `template_path: ${template}
24789
25275
 
24790
25276
  `;
24791
- stdout += readTrimmed(path13.join(cwd, template), 220);
25277
+ stdout += readTrimmed2(path13.join(cwd, template), 220);
24792
25278
  } else {
24793
25279
  stdout += `template_path: none
24794
25280
 
@@ -24860,7 +25346,7 @@ function changelogContext(root, range) {
24860
25346
  stdout += printSection("Existing CHANGELOG.md");
24861
25347
  const changelogPath = path13.join(cwd, "CHANGELOG.md");
24862
25348
  if (existsSync6(changelogPath)) {
24863
- stdout += readTrimmed(changelogPath, 260) + `
25349
+ stdout += readTrimmed2(changelogPath, 260) + `
24864
25350
  `;
24865
25351
  } else {
24866
25352
  stdout += `CHANGELOG.md not found.
@@ -24902,7 +25388,7 @@ function docsRefreshContext(root, range) {
24902
25388
  stdout += printSection("README Preview");
24903
25389
  const readme = path13.join(cwd, "README.md");
24904
25390
  if (existsSync6(readme)) {
24905
- stdout += readTrimmed(readme, 220) + `
25391
+ stdout += readTrimmed2(readme, 220) + `
24906
25392
  `;
24907
25393
  } else {
24908
25394
  stdout += `README.md not found.
@@ -24989,7 +25475,7 @@ function releaseNotesContext(root, rangeOrTag) {
24989
25475
  }
24990
25476
 
24991
25477
  // packages/workit-core/src/core/tracker-issues.ts
24992
- import { spawnSync as spawnSync6 } from "node:child_process";
25478
+ import { spawnSync as spawnSync7 } from "node:child_process";
24993
25479
  var repoPathFromRemote = (remote) => {
24994
25480
  let rest = (remote || "").trim().replace(/\/+$/, "");
24995
25481
  if (!rest)
@@ -25010,7 +25496,7 @@ var defaultCli = (bin, available) => {
25010
25496
  if (!available)
25011
25497
  return null;
25012
25498
  return (args, root) => {
25013
- const out = spawnSync6(bin, args, {
25499
+ const out = spawnSync7(bin, args, {
25014
25500
  cwd: root,
25015
25501
  encoding: "utf8",
25016
25502
  env: { ...process.env, PATH: process.env.PATH ?? "" }
@@ -25088,7 +25574,7 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
25088
25574
  };
25089
25575
 
25090
25576
  // packages/workit-core/src/core/youtrack.ts
25091
- import fs9 from "node:fs";
25577
+ import fs10 from "node:fs";
25092
25578
  import path14 from "node:path";
25093
25579
 
25094
25580
  // packages/workit-core/src/core/templates.ts
@@ -25107,17 +25593,17 @@ var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? p
25107
25593
  var youTrackTokenModeOk = (p) => {
25108
25594
  if (process.platform === "win32")
25109
25595
  return true;
25110
- const mode = fs9.statSync(p).mode & 511;
25596
+ const mode = fs10.statSync(p).mode & 511;
25111
25597
  return mode === 384;
25112
25598
  };
25113
25599
  function readYouTrackConfig(required) {
25114
25600
  const cfgPath = youTrackConfigPath();
25115
- if (!fs9.existsSync(cfgPath)) {
25601
+ if (!fs10.existsSync(cfgPath)) {
25116
25602
  return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
25117
25603
  }
25118
25604
  let parsed;
25119
25605
  try {
25120
- parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
25606
+ parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
25121
25607
  } catch {
25122
25608
  return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
25123
25609
  }
@@ -25128,11 +25614,11 @@ function readYouTrackConfig(required) {
25128
25614
  }
25129
25615
  var readYouTrackContextConfig = () => {
25130
25616
  const cfgPath = youTrackReadOnlyConfigPath();
25131
- if (!fs9.existsSync(cfgPath))
25617
+ if (!fs10.existsSync(cfgPath))
25132
25618
  return { data: {} };
25133
25619
  let parsed;
25134
25620
  try {
25135
- parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
25621
+ parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
25136
25622
  } catch {
25137
25623
  return { error: "YouTrack configuration is unavailable" };
25138
25624
  }
@@ -25172,11 +25658,11 @@ var youTrackToken = () => {
25172
25658
  return loaded;
25173
25659
  const tokenFile = String(loaded.config.tokenFile ?? "");
25174
25660
  const tokenPath = tokenFile ? path14.isAbsolute(tokenFile) ? path14.resolve(tokenFile) : path14.resolve(process.cwd(), tokenFile) : "";
25175
- if (!tokenPath || !fs9.existsSync(tokenPath))
25661
+ if (!tokenPath || !fs10.existsSync(tokenPath))
25176
25662
  return { error: "missing youtrack.token" };
25177
25663
  if (!youTrackTokenModeOk(tokenPath))
25178
25664
  return { error: "youtrack.token mode must be 0600" };
25179
- const token = fs9.readFileSync(tokenPath, "utf8").trim();
25665
+ const token = fs10.readFileSync(tokenPath, "utf8").trim();
25180
25666
  if (!token)
25181
25667
  return { error: "empty token file" };
25182
25668
  if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
@@ -25241,9 +25727,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
25241
25727
  } catch {
25242
25728
  continue;
25243
25729
  }
25244
- if (!fs9.existsSync(full))
25730
+ if (!fs10.existsSync(full))
25245
25731
  continue;
25246
- const text = fs9.readFileSync(full, "utf8");
25732
+ const text = fs10.readFileSync(full, "utf8");
25247
25733
  const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
25248
25734
  if (m)
25249
25735
  return m[1];
@@ -25804,72 +26290,168 @@ var main = async () => {
25804
26290
  var entryPath = process.argv[1] ? path16.resolve(process.argv[1]) : "";
25805
26291
  if (entryPath === path16.resolve(fileURLToPath3(import.meta.url)))
25806
26292
  await main();
26293
+ // packages/workit-core/src/hooks/descriptor.ts
26294
+ var support = (descriptor, axis) => {
26295
+ if (axis.startsWith("event:"))
26296
+ return descriptor.events[axis.slice("event:".length)].support;
26297
+ if (axis === "shellPolicy.deny")
26298
+ return descriptor.shellPolicy.deny;
26299
+ if (axis === "stopControl" || axis === "shellAvailable")
26300
+ return descriptor[axis];
26301
+ const [group, key] = axis.split(".");
26302
+ return descriptor[group][key];
26303
+ };
26304
+ var usable = (value) => value === "native" || value === "partial";
26305
+ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map((rule) => {
26306
+ const levels = rule.requires.map((axis) => support(descriptor, axis));
26307
+ const available = rule.assurance !== "unavailable" && levels.every(usable) && (rule.observed ?? []).every((flag) => observed[flag] === true);
26308
+ const assurance = !available ? "unavailable" : rule.assurance === "enforced" && (levels.length === 0 || levels.some((level) => level !== "native")) ? "agent_guided" : rule.assurance;
26309
+ return {
26310
+ name: rule.name,
26311
+ surface: rule.surface,
26312
+ assurance,
26313
+ reason: assurance === "unavailable" ? rule.unavailableReason ?? rule.reason : rule.reason,
26314
+ refs: rule.refs.map((handle) => ({ kind: "host", host: descriptor.host, handle }))
26315
+ };
26316
+ });
26317
+ // packages/workit-core/src/hooks/context.ts
26318
+ var cache = new Map;
26319
+ // packages/workit-core/src/hooks/handle.ts
26320
+ var offered = new Set;
26321
+ // packages/workit-core/src/hooks/hosts/fields.ts
26322
+ var SHELLS = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
25807
26323
 
25808
- // packages/workit-cursor/hooks/workit-hook.ts
25809
- var hostRef = (handle) => ({ kind: "host", host: "cursor", handle });
25810
- var cursorCapabilities = (availability = {}) => {
25811
- const has = (name) => availability[name] === true;
25812
- return [
26324
+ // packages/workit-core/src/hooks/hosts/claude-code.ts
26325
+ var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
26326
+ var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
26327
+ var CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit", "SubagentStart"]);
26328
+
26329
+ // packages/workit-core/src/hooks/hosts/codex.ts
26330
+ var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
26331
+ var SOURCES2 = new Set(["startup", "resume", "clear", "compact"]);
26332
+
26333
+ // packages/workit-core/src/hooks/hosts/cursor.ts
26334
+ var undocumented = { support: "undocumented", native: null };
26335
+ var none = { support: "none", native: null };
26336
+ var CURSOR_DESCRIPTOR = {
26337
+ host: "cursor",
26338
+ label: "Cursor",
26339
+ verifiedAgainst: "Cursor hooks docs; payloads in test/fixtures/hooks/cursor",
26340
+ docs: ["https://cursor.com/docs/agent/hooks"],
26341
+ transport: "hook-process",
26342
+ events: {
26343
+ "session.start": { support: "native", native: "sessionStart" },
26344
+ "context.turn": none,
26345
+ "shell.pre": { support: "native", native: "beforeShellExecution" },
26346
+ "tool.pre": { support: "native", native: "preToolUse" },
26347
+ "shell.post": undocumented,
26348
+ "subagent.start": { support: "native", native: "subagentStart" },
26349
+ "subagent.stop": { support: "partial", native: "subagentStop" },
26350
+ "prompt.submit": undocumented,
26351
+ "compact.pre": { support: "partial", native: "preCompact" },
26352
+ stop: undocumented
26353
+ },
26354
+ shellPolicy: { deny: "native", channel: "exit2+json", failClosed: true },
26355
+ context: {
26356
+ sessionStart: "native",
26357
+ perTurn: "none",
26358
+ afterCompact: "partial",
26359
+ task: "single-active"
26360
+ },
26361
+ subagents: {
26362
+ identity: "native",
26363
+ parentBinding: "native",
26364
+ blockStart: "native",
26365
+ worktreeIsolation: "undocumented",
26366
+ maxConcurrency: "undocumented"
26367
+ },
26368
+ provenance: {
26369
+ sessionId: "native",
26370
+ agentIdOnTool: "undocumented",
26371
+ postToolObserve: "undocumented"
26372
+ },
26373
+ interaction: { questions: "none", writeBoundary: "partial" },
26374
+ stopControl: "undocumented",
26375
+ shellAvailable: "native",
26376
+ perEventCost: "npx-network",
26377
+ capabilities: [
25813
26378
  {
25814
26379
  name: "interactive_decision",
25815
26380
  surface: "AskQuestion",
26381
+ refs: ["AskQuestion"],
26382
+ requires: [],
25816
26383
  assurance: "agent_guided",
25817
- reason: "Cursor does not expose AskQuestion answers to Workit hooks or MCP",
25818
- refs: [hostRef("AskQuestion")]
26384
+ reason: "Cursor does not expose AskQuestion answers to Workit hooks or MCP"
25819
26385
  },
25820
26386
  {
25821
26387
  name: "known_product_writes",
25822
26388
  surface: "preToolUse",
26389
+ refs: ["preToolUse"],
26390
+ requires: [],
25823
26391
  assurance: "unavailable",
25824
- reason: "file writes are host-policy; the Cursor hook no longer gates write tools or shell commands",
25825
- refs: [hostRef("preToolUse")]
26392
+ reason: "file writes are host-policy; the Cursor hook no longer gates write tools or shell commands"
25826
26393
  },
25827
26394
  {
25828
26395
  name: "native_subagents",
25829
26396
  surface: "subagentStart/subagentStop",
25830
- assurance: has("subagentStart") && has("subagentStop") ? "agent_guided" : "unavailable",
25831
- reason: has("subagentStart") && has("subagentStop") ? "reviewer/investigator starts are bounded; Cursor implementer delegation is unavailable and subagentStop lacks a stable child identity" : "Cursor native subagent lifecycle hooks are incomplete",
25832
- refs: [hostRef("subagentStart"), hostRef("subagentStop")]
26397
+ refs: ["subagentStart", "subagentStop"],
26398
+ requires: ["event:subagent.start", "event:subagent.stop"],
26399
+ observed: ["subagent.start", "subagent.stop"],
26400
+ assurance: "agent_guided",
26401
+ reason: "reviewer/investigator starts are bounded; Cursor implementer delegation is unavailable and subagentStop lacks a stable child identity",
26402
+ unavailableReason: "Cursor native subagent lifecycle hooks are incomplete"
25833
26403
  },
25834
26404
  {
25835
26405
  name: "native_subagent_start",
25836
26406
  surface: "subagentStart",
25837
- assurance: has("subagentStart") ? "enforced" : "unavailable",
25838
- reason: has("subagentStart") ? "Cursor subagentStart enforces explicit reviewer/investigator markers; implementer delegation is unavailable" : "Cursor subagentStart is absent",
25839
- refs: [hostRef("subagentStart")]
26407
+ refs: ["subagentStart"],
26408
+ requires: ["event:subagent.start", "subagents.blockStart"],
26409
+ observed: ["subagent.start"],
26410
+ assurance: "enforced",
26411
+ reason: "Cursor subagentStart enforces explicit reviewer/investigator markers; implementer delegation is unavailable",
26412
+ unavailableReason: "Cursor subagentStart is absent"
25840
26413
  },
25841
26414
  {
25842
26415
  name: "fresh-context-review",
25843
26416
  surface: "subagentStart",
25844
- assurance: has("subagentStart") ? "agent_guided" : "unavailable",
25845
- reason: has("subagentStart") ? "independent review runs as a bounded reviewer subagent; stops lack stable identity, so reviewer exclusivity is evaluated from recorded evidence" : "Cursor subagentStart is unavailable for independent review",
25846
- refs: [hostRef("subagentStart")]
26417
+ refs: ["subagentStart"],
26418
+ requires: ["event:subagent.start"],
26419
+ observed: ["subagent.start"],
26420
+ assurance: "agent_guided",
26421
+ reason: "independent review runs as a bounded reviewer subagent; stops lack stable identity, so reviewer exclusivity is evaluated from recorded evidence",
26422
+ unavailableReason: "Cursor subagentStart is unavailable for independent review"
25847
26423
  },
25848
26424
  {
25849
26425
  name: "arbitrary_shell_write",
25850
26426
  surface: "unobservable_shell",
26427
+ refs: ["beforeShellExecution"],
26428
+ requires: [],
25851
26429
  assurance: "unavailable",
25852
- reason: "Only explicitly parsed shell targets are interceptable; arbitrary shell writes are not provable",
25853
- refs: [hostRef("beforeShellExecution")]
26430
+ reason: "Only explicitly parsed shell targets are interceptable; arbitrary shell writes are not provable"
25854
26431
  },
25855
26432
  {
25856
26433
  name: "compact_context",
25857
26434
  surface: "sessionStart/preCompact",
25858
- assurance: has("sessionStart") ? "agent_guided" : "unavailable",
25859
- reason: "sessionStart injects context; preCompact can only show a bounded user reminder",
25860
- refs: [hostRef("sessionStart"), hostRef("preCompact")]
26435
+ refs: ["sessionStart", "preCompact"],
26436
+ requires: ["context.sessionStart"],
26437
+ observed: ["session.start"],
26438
+ assurance: "agent_guided",
26439
+ reason: "sessionStart injects context; preCompact can only show a bounded user reminder"
25861
26440
  }
25862
- ];
26441
+ ]
25863
26442
  };
25864
- var eventNames = new Set([
25865
- "sessionStart",
25866
- "preToolUse",
25867
- "beforeShellExecution",
25868
- "subagentStart",
25869
- "subagentStop",
25870
- "preCompact"
25871
- ]);
25872
- var historyOfferSessions = new Set;
26443
+ var BLOCKING = new Set(["preToolUse", "beforeShellExecution", "subagentStart"]);
26444
+
26445
+ // packages/workit-core/src/hooks/run.ts
26446
+ if (false) {}
26447
+ // packages/workit-cursor/hooks/workit-hook.ts
26448
+ var cursorCapabilities = (availability = {}) => capabilitiesFor(CURSOR_DESCRIPTOR, {
26449
+ "session.start": availability.sessionStart,
26450
+ "tool.pre": availability.preToolUse,
26451
+ "shell.pre": availability.beforeShellExecution,
26452
+ "subagent.start": availability.subagentStart,
26453
+ "subagent.stop": availability.subagentStop
26454
+ });
25873
26455
  if (false)
25874
26456
  ;
25875
26457
  // packages/workit-cursor/mcp/run-server.ts