@brainervirus/workit-mcp 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.
Files changed (2) hide show
  1. package/dist/index.js +1638 -1152
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -16783,7 +16783,7 @@ class StdioServerTransport {
16783
16783
  import { fileURLToPath as fileURLToPath2 } from "node:url";
16784
16784
 
16785
16785
  // packages/workit-core/src/core.ts
16786
- import { spawnSync as spawnSync3 } from "node:child_process";
16786
+ import { spawnSync as spawnSync4 } from "node:child_process";
16787
16787
  import { existsSync as existsSync5, realpathSync as realpathSync7 } from "node:fs";
16788
16788
  import path11 from "node:path";
16789
16789
 
@@ -16863,7 +16863,8 @@ var hostSchema = _enum([
16863
16863
  "codex_cli",
16864
16864
  "codex_desktop",
16865
16865
  "pi",
16866
- "workit_cli"
16866
+ "workit_cli",
16867
+ "claude_code"
16867
16868
  ]);
16868
16869
  var assuranceSchema = _enum(["enforced", "agent_guided", "unavailable"]);
16869
16870
  var outcomeSchema = _enum(["verified", "accepted_limitations", "stopped"]);
@@ -17313,15 +17314,70 @@ var taskRecordSchema = object2({
17313
17314
  decisions: array(entrySchema(decisionSchema)),
17314
17315
  actionProgress: actionProgressListSchema.optional(),
17315
17316
  findings: array(entrySchema(findingSchema)),
17316
- workers: array(entrySchema(workerSchema))
17317
+ workers: array(entrySchema(workerSchema)),
17318
+ critical: array(nonEmpty).optional()
17317
17319
  }).strict();
17320
+ var stripCount = (strips) => strips.reduce((sum, strip) => sum + strip.keys.length, 0);
17321
+ var strippable = (issues, prefix = []) => {
17322
+ const strips = [];
17323
+ for (const issue of issues) {
17324
+ if (issue.code === "unrecognized_keys") {
17325
+ strips.push({ path: [...prefix, ...issue.path], keys: issue.keys });
17326
+ continue;
17327
+ }
17328
+ if (issue.code !== "invalid_union")
17329
+ return null;
17330
+ 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);
17331
+ if (!branch)
17332
+ return null;
17333
+ strips.push(...branch);
17334
+ }
17335
+ return strips;
17336
+ };
17337
+ var recordPath = (path) => path.map((key) => typeof key === "number" ? "*" : String(key)).join(".");
17338
+ var overlaps = (left, right) => left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
17339
+ var parseStoredRecord = (schema, value) => {
17340
+ let parsed = schema.safeParse(value);
17341
+ if (parsed.success)
17342
+ return { success: true, data: parsed.data, stripped: [] };
17343
+ const first = { success: false, error: parsed.error, critical: [] };
17344
+ 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(".")) : [];
17345
+ const stripped = [];
17346
+ let current = structuredClone(value);
17347
+ for (let round = 0;round < 8 && !parsed.success; round++) {
17348
+ const strips = strippable(parsed.error.issues);
17349
+ if (strips === null || strips.length === 0)
17350
+ return first;
17351
+ for (const strip of strips) {
17352
+ let target = current;
17353
+ for (const key of strip.path)
17354
+ target = typeof target === "object" && target !== null ? target[key] : undefined;
17355
+ if (typeof target !== "object" || target === null)
17356
+ return first;
17357
+ for (const key of strip.keys) {
17358
+ stripped.push(recordPath([...strip.path, key]));
17359
+ delete target[key];
17360
+ }
17361
+ }
17362
+ parsed = schema.safeParse(current);
17363
+ }
17364
+ if (!parsed.success)
17365
+ return first;
17366
+ const critical = [
17367
+ ...new Set(stripped.filter((item) => declared.some((path) => overlaps(item, path))))
17368
+ ];
17369
+ if (critical.length > 0)
17370
+ return { ...first, critical };
17371
+ return { success: true, data: parsed.data, stripped: [...new Set(stripped)] };
17372
+ };
17318
17373
  var workspaceRecordSchema = object2({
17319
17374
  schemaVersion: literal(1),
17320
17375
  id,
17321
17376
  revision,
17322
17377
  root: nonEmpty,
17323
17378
  runtime: runtimeSchema.optional(),
17324
- writer: object2({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
17379
+ writer: object2({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable(),
17380
+ critical: array(nonEmpty).optional()
17325
17381
  }).strict();
17326
17382
  var capabilitySchema = object2({
17327
17383
  name: nonEmpty,
@@ -17559,6 +17615,11 @@ var operationSchemas = {
17559
17615
  writer: discriminatedUnion("action", Object.values(writerOperations)),
17560
17616
  state: discriminatedUnion("action", Object.values(stateOperations))
17561
17617
  };
17618
+ var { recover: _unadvertisedRecover, ...advertisedStateOperations } = stateOperations;
17619
+ var advertisedOperationSchemas = {
17620
+ ...operationSchemas,
17621
+ state: discriminatedUnion("action", Object.values(advertisedStateOperations))
17622
+ };
17562
17623
  var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
17563
17624
  var success = (revisionValue, workspaceRevisionValue, data) => ({
17564
17625
  ok: true,
@@ -17582,7 +17643,7 @@ function parseOperation(family, input) {
17582
17643
  return failure2("invalid_input", "operation input is invalid", { fields });
17583
17644
  }
17584
17645
  function operationJsonSchema(family) {
17585
- return toJSONSchema(operationSchemas[family], { target: "draft-2020-12" });
17646
+ return toJSONSchema(advertisedOperationSchemas[family], { target: "draft-2020-12" });
17586
17647
  }
17587
17648
  var OPERATION_SCHEMA_DEPTH = 1;
17588
17649
  var schemaShapeDescription = (value, depth = 0) => {
@@ -17774,8 +17835,7 @@ function requirementId(input) {
17774
17835
  // packages/workit-core/src/core/task-store.ts
17775
17836
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
17776
17837
  import { AsyncLocalStorage } from "node:async_hooks";
17777
- import * as fs4 from "node:fs";
17778
- import { hostname } from "node:os";
17838
+ import * as fs5 from "node:fs";
17779
17839
  import path4 from "node:path";
17780
17840
 
17781
17841
  // packages/workit-core/src/core/package-root.ts
@@ -18398,13 +18458,10 @@ function acquireFileLockSync(targetPath, options) {
18398
18458
  }
18399
18459
  }
18400
18460
  }
18401
- // packages/workit-core/src/core/task-store.ts
18402
- var processEvidenceSchema = object2({
18403
- state: _enum(["stopped", "accounted_for"]),
18404
- pid: number2().int().nonnegative(),
18405
- processStart: string2().nullable(),
18406
- ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
18407
- }).strict();
18461
+ // packages/workit-core/src/core/store-lock.ts
18462
+ import { spawnSync } from "node:child_process";
18463
+ import fs4 from "node:fs";
18464
+ import { hostname } from "node:os";
18408
18465
  var metadataLockSchema = object2({
18409
18466
  pid: number2().int().nonnegative().safe(),
18410
18467
  processStart: string2().nullable(),
@@ -18412,6 +18469,143 @@ var metadataLockSchema = object2({
18412
18469
  nonce: string2().min(1),
18413
18470
  externalAction: literal(true).optional()
18414
18471
  }).strict();
18472
+ var FOREIGN_LOCK_TTL_MS = 10 * 60000;
18473
+ var UNREADABLE_LOCK_TTL_MS = 30000;
18474
+ var RECLAIM_GUARD_TTL_MS = 30000;
18475
+ var defaultLockTimeoutMs = 250;
18476
+ var defaultLockTimeout = () => defaultLockTimeoutMs;
18477
+ var parseMetadataLock = (raw) => {
18478
+ let value;
18479
+ try {
18480
+ value = JSON.parse(raw);
18481
+ } catch {
18482
+ throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18483
+ }
18484
+ const parsed = metadataLockSchema.safeParse(value);
18485
+ if (!parsed.success)
18486
+ throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18487
+ return parsed.data;
18488
+ };
18489
+ var parseMetadataLockOrNull = (raw) => {
18490
+ try {
18491
+ return parseMetadataLock(raw);
18492
+ } catch {
18493
+ return null;
18494
+ }
18495
+ };
18496
+ var sameMetadataLock = (left, right) => {
18497
+ const parsed = metadataLockSchema.safeParse(left);
18498
+ return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
18499
+ };
18500
+ var parseProcStatStart = (stat) => {
18501
+ const close = stat.lastIndexOf(")");
18502
+ if (close < 0)
18503
+ return null;
18504
+ return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
18505
+ };
18506
+ var processStartOf = (pid) => {
18507
+ if (process.platform === "linux") {
18508
+ try {
18509
+ return parseProcStatStart(fs4.readFileSync(`/proc/${pid}/stat`, "utf8"));
18510
+ } catch {
18511
+ return null;
18512
+ }
18513
+ }
18514
+ if (process.platform === "darwin" || process.platform === "freebsd") {
18515
+ try {
18516
+ const run = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
18517
+ encoding: "utf8",
18518
+ timeout: 1000
18519
+ });
18520
+ const value = run.status === 0 ? run.stdout.trim() : "";
18521
+ return value || null;
18522
+ } catch {
18523
+ return null;
18524
+ }
18525
+ }
18526
+ return null;
18527
+ };
18528
+ var readTrimmed = (file) => {
18529
+ try {
18530
+ return fs4.readFileSync(file, "utf8").trim() || null;
18531
+ } catch {
18532
+ return null;
18533
+ }
18534
+ };
18535
+ var cachedLockHost = null;
18536
+ var localLockHost = () => {
18537
+ if (cachedLockHost !== null)
18538
+ return cachedLockHost;
18539
+ let pidns = null;
18540
+ try {
18541
+ pidns = /\[(\d+)\]/.exec(fs4.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
18542
+ } catch {}
18543
+ const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
18544
+ cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
18545
+ return cachedLockHost;
18546
+ };
18547
+ var pidAlive = (pid) => {
18548
+ if (!Number.isSafeInteger(pid) || pid <= 0)
18549
+ return false;
18550
+ try {
18551
+ process.kill(pid, 0);
18552
+ return true;
18553
+ } catch (error) {
18554
+ return error.code === "EPERM";
18555
+ }
18556
+ };
18557
+ var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
18558
+ const lock = metadataLockSchema.safeParse(payload);
18559
+ if (!lock.success)
18560
+ return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
18561
+ const { pid, processStart, host } = lock.data;
18562
+ const [lockName, lockSpace] = host.split("#");
18563
+ const [localName, localSpace] = localHost.split("#");
18564
+ if (lockSpace && localSpace && lockName === localName) {
18565
+ const [lockNs, lockBoot] = lockSpace.split(":");
18566
+ const [localNs, localBoot] = localSpace.split(":");
18567
+ if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
18568
+ return { state: "stale", reason: "lock was taken before this machine rebooted" };
18569
+ }
18570
+ if (host !== localHost)
18571
+ 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` };
18572
+ if (!pidAlive(pid))
18573
+ return { state: "stale", reason: `pid ${pid} is not running` };
18574
+ const currentStart = processStartOf(pid);
18575
+ if (processStart !== null && currentStart !== null && processStart !== currentStart)
18576
+ return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
18577
+ return { state: "live", reason: `held by running pid ${pid}` };
18578
+ };
18579
+ var ageOf = (file, nowMs) => {
18580
+ try {
18581
+ return nowMs - fs4.lstatSync(file).mtimeMs;
18582
+ } catch {
18583
+ return null;
18584
+ }
18585
+ };
18586
+ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
18587
+ const guard = `${lockPath}.reclaim`;
18588
+ const age = ageOf(guard, nowMs);
18589
+ if (age === null || age <= RECLAIM_GUARD_TTL_MS)
18590
+ return false;
18591
+ try {
18592
+ fs4.rmdirSync(guard);
18593
+ return true;
18594
+ } catch {
18595
+ return false;
18596
+ }
18597
+ };
18598
+
18599
+ // packages/workit-core/src/core/task-store.ts
18600
+ var RECOVERY_COPIES_PER_RECORD = 3;
18601
+ var STALE_TEMPORARY_MS = 60 * 60000;
18602
+ var RECOVERY_NAME = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/;
18603
+ var processEvidenceSchema = object2({
18604
+ state: _enum(["stopped", "accounted_for"]),
18605
+ pid: number2().int().nonnegative(),
18606
+ processStart: string2().nullable(),
18607
+ ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
18608
+ }).strict();
18415
18609
  var INDEX_VERSION = 1;
18416
18610
  var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
18417
18611
  var indexEntry = (task, file) => {
@@ -18439,7 +18633,7 @@ var indexEntry = (task, file) => {
18439
18633
  };
18440
18634
  var fileSignature = (file) => {
18441
18635
  try {
18442
- const stat = fs4.statSync(file, { bigint: true });
18636
+ const stat = fs5.statSync(file, { bigint: true });
18443
18637
  return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
18444
18638
  } catch {
18445
18639
  return null;
@@ -18456,7 +18650,7 @@ var cachedRuntimeVersion = null;
18456
18650
  var runtimeVersion = () => {
18457
18651
  if (cachedRuntimeVersion === null) {
18458
18652
  try {
18459
- const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
18653
+ const pkg = JSON.parse(fs5.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
18460
18654
  cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
18461
18655
  } catch {
18462
18656
  cachedRuntimeVersion = "0.0.0";
@@ -18485,36 +18679,20 @@ var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
18485
18679
  var isObject2 = (value) => typeof value === "object" && value !== null;
18486
18680
  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);
18487
18681
  var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
18488
- var parseMetadataLock = (raw) => {
18489
- let value;
18490
- try {
18491
- value = JSON.parse(raw);
18492
- } catch {
18493
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18494
- }
18495
- const parsed = metadataLockSchema.safeParse(value);
18496
- if (!parsed.success)
18497
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18498
- return parsed.data;
18499
- };
18500
- var sameMetadataLock = (left, right) => {
18501
- const parsed = metadataLockSchema.safeParse(left);
18502
- return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
18503
- };
18504
18682
  var sameDirectoryIdentity = (left, right) => {
18505
18683
  if (!path4.isAbsolute(left) || !path4.isAbsolute(right))
18506
18684
  return false;
18507
18685
  const normalizedLeft = path4.resolve(left);
18508
18686
  const normalizedRight = path4.resolve(right);
18509
18687
  try {
18510
- const a = fs4.statSync(normalizedLeft, { bigint: true });
18511
- const b = fs4.statSync(normalizedRight, { bigint: true });
18688
+ const a = fs5.statSync(normalizedLeft, { bigint: true });
18689
+ const b = fs5.statSync(normalizedRight, { bigint: true });
18512
18690
  if (!a.isDirectory() || !b.isDirectory())
18513
18691
  return false;
18514
18692
  if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
18515
18693
  return true;
18516
- const canonicalLeft = fs4.realpathSync(normalizedLeft);
18517
- const canonicalRight = fs4.realpathSync(normalizedRight);
18694
+ const canonicalLeft = fs5.realpathSync(normalizedLeft);
18695
+ const canonicalRight = fs5.realpathSync(normalizedRight);
18518
18696
  if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
18519
18697
  return true;
18520
18698
  return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
@@ -18522,12 +18700,47 @@ var sameDirectoryIdentity = (left, right) => {
18522
18700
  return false;
18523
18701
  }
18524
18702
  };
18703
+ var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
18704
+ var isTransientWindowsError = (error) => {
18705
+ if (process.platform !== "win32")
18706
+ return false;
18707
+ const code = error?.code;
18708
+ return typeof code === "string" && TRANSIENT_WINDOWS_CODES.has(code);
18709
+ };
18710
+ var retryTransient = (run) => {
18711
+ if (process.platform !== "win32")
18712
+ return run();
18713
+ for (let attempt = 0;; attempt += 1) {
18714
+ try {
18715
+ return run();
18716
+ } catch (error) {
18717
+ if (attempt >= 20 || !isTransientWindowsError(error))
18718
+ throw error;
18719
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 * (attempt + 1));
18720
+ }
18721
+ }
18722
+ };
18525
18723
  var externalActionLockRoots = new AsyncLocalStorage;
18724
+ var heldInProcess = new Map;
18725
+ var holdRoot = (root) => heldInProcess.set(root, (heldInProcess.get(root) ?? 0) + 1);
18726
+ var dropRoot = (root) => {
18727
+ const count = (heldInProcess.get(root) ?? 1) - 1;
18728
+ if (count > 0)
18729
+ heldInProcess.set(root, count);
18730
+ else
18731
+ heldInProcess.delete(root);
18732
+ };
18733
+ var dedupeCandidates = (candidates) => {
18734
+ const last = new Map(candidates.map((candidate, index) => [candidate.id, index]));
18735
+ return candidates.filter((candidate, index) => last.get(candidate.id) === index);
18736
+ };
18526
18737
 
18527
18738
  class TaskStore {
18528
18739
  root;
18529
- constructor(root) {
18530
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
18740
+ lockTimeoutMs;
18741
+ constructor(root, options = {}) {
18742
+ this.root = fs5.existsSync(root) ? fs5.realpathSync(root) : path4.resolve(root);
18743
+ this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
18531
18744
  }
18532
18745
  readTask(taskId) {
18533
18746
  if (!validId(taskId))
@@ -18547,11 +18760,11 @@ class TaskStore {
18547
18760
  return result.result;
18548
18761
  }
18549
18762
  listTasks() {
18550
- if (!fs4.existsSync(this.tasksDir))
18763
+ if (!fs5.existsSync(this.tasksDir))
18551
18764
  return success(null, null, []);
18552
18765
  let names;
18553
18766
  try {
18554
- names = fs4.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18767
+ names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18555
18768
  } catch (error) {
18556
18769
  return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
18557
18770
  path: this.tasksDir
@@ -18580,11 +18793,11 @@ class TaskStore {
18580
18793
  return success(null, null, tasks);
18581
18794
  }
18582
18795
  listTaskIndex() {
18583
- if (!fs4.existsSync(this.tasksDir))
18796
+ if (!fs5.existsSync(this.tasksDir))
18584
18797
  return success(null, null, []);
18585
18798
  let names;
18586
18799
  try {
18587
- names = fs4.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18800
+ names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18588
18801
  } catch (error) {
18589
18802
  return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
18590
18803
  path: this.tasksDir
@@ -18846,7 +19059,7 @@ class TaskStore {
18846
19059
  if (!current.data)
18847
19060
  return failure2("not_found", "workspace not found");
18848
19061
  if (current.data.revision !== expected)
18849
- return this.conflict(expected, current.data.revision);
19062
+ return this.workspaceConflict(expected, current.data.revision);
18850
19063
  const previousBytes = this.snapshotBytes(this.workspacePath);
18851
19064
  if (!previousBytes)
18852
19065
  return failure2("storage_error", "workspace snapshot disappeared during mutation");
@@ -18889,7 +19102,7 @@ class TaskStore {
18889
19102
  if (task.data.revision !== expected)
18890
19103
  return this.conflict(expected, task.data.revision);
18891
19104
  if (workspace.data.revision !== input.expectedWorkspaceRevision)
18892
- return this.conflict(input.expectedWorkspaceRevision, workspace.data.revision);
19105
+ return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
18893
19106
  const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
18894
19107
  const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
18895
19108
  if (!previousWorkspaceBytes || !previousTaskBytes)
@@ -18952,12 +19165,12 @@ class TaskStore {
18952
19165
  });
18953
19166
  }
18954
19167
  recoveryCandidates() {
18955
- if (!fs4.existsSync(this.recoveryDir))
19168
+ if (!fs5.existsSync(this.recoveryDir))
18956
19169
  return success(null, null, []);
18957
19170
  try {
18958
19171
  const candidates = [];
18959
- for (const name of fs4.readdirSync(this.recoveryDir)) {
18960
- const match = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/.exec(name);
19172
+ for (const name of fs5.readdirSync(this.recoveryDir)) {
19173
+ const match = RECOVERY_NAME.exec(name);
18961
19174
  if (match)
18962
19175
  candidates.push({
18963
19176
  target: match[1],
@@ -19005,7 +19218,7 @@ class TaskStore {
19005
19218
  return failure2("recovery_required", "validated recovery snapshot not found");
19006
19219
  selectedBytes = candidate;
19007
19220
  }
19008
- const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema) : this.parseBytes(selectedBytes, taskRecordSchema);
19221
+ const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
19009
19222
  if (!selectedRecord.ok)
19010
19223
  return selectedRecord;
19011
19224
  if (target === "workspace") {
@@ -19051,14 +19264,14 @@ class TaskStore {
19051
19264
  return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
19052
19265
  }
19053
19266
  if (target === "workspace") {
19054
- const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema);
19267
+ const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
19055
19268
  if (!parsed.ok)
19056
19269
  return parsed;
19057
19270
  const value = { ...parsed.data, revision: newRevision(), writer: null };
19058
19271
  const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
19059
19272
  return replaced.ok ? success(value.revision, value.revision, value) : replaced;
19060
19273
  }
19061
- const parsed = this.parseBytes(selectedBytes, taskRecordSchema);
19274
+ const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
19062
19275
  if (!parsed.ok)
19063
19276
  return parsed;
19064
19277
  const value = {
@@ -19069,7 +19282,7 @@ class TaskStore {
19069
19282
  };
19070
19283
  const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
19071
19284
  return replaced.ok ? success(value.revision, null, value) : replaced;
19072
- }, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate));
19285
+ }, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
19073
19286
  return result;
19074
19287
  } catch (error) {
19075
19288
  return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
@@ -19104,12 +19317,12 @@ class TaskStore {
19104
19317
  }
19105
19318
  readLockSnapshot() {
19106
19319
  try {
19107
- const stat = fs4.lstatSync(this.lockPath);
19320
+ const stat = fs5.lstatSync(this.lockPath);
19108
19321
  if (!stat.isFile() || stat.isSymbolicLink())
19109
19322
  return failure2("recovery_required", "metadata lock is not a regular file", {
19110
19323
  path: this.lockPath
19111
19324
  });
19112
- const raw = fs4.readFileSync(this.lockPath, "utf8");
19325
+ const raw = fs5.readFileSync(this.lockPath, "utf8");
19113
19326
  return success(null, null, { raw, data: parseMetadataLock(raw) });
19114
19327
  } catch (error) {
19115
19328
  if (error?.code === "ENOENT")
@@ -19120,22 +19333,50 @@ class TaskStore {
19120
19333
  }
19121
19334
  }
19122
19335
  metadataLockOptions() {
19336
+ const inProcess = heldInProcess.has(this.root);
19123
19337
  return {
19124
19338
  lockPath: this.lockPath,
19125
19339
  staleMs: Number.MAX_SAFE_INTEGER,
19126
- timeoutMs: 0,
19127
- retry: { retries: 0 },
19128
- staleRecovery: "fail-closed",
19129
- shouldReclaim: () => false,
19130
- parsePayload: parseMetadataLock,
19340
+ timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
19341
+ retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
19342
+ staleRecovery: "remove-if-unchanged",
19343
+ shouldReclaim: ({ payload, nowMs }) => {
19344
+ let ageMs = null;
19345
+ try {
19346
+ ageMs = nowMs - fs5.lstatSync(this.lockPath).mtimeMs;
19347
+ } catch {}
19348
+ return classifyLockOwner(payload, ageMs).state === "stale";
19349
+ },
19350
+ shouldRemoveStaleLock: () => true,
19351
+ parsePayload: parseMetadataLockOrNull,
19131
19352
  payload: () => ({
19132
19353
  pid: process.pid,
19133
- processStart: this.processStart(process.pid),
19134
- host: hostname(),
19354
+ processStart: processStartOf(process.pid),
19355
+ host: localLockHost(),
19135
19356
  nonce: randomUUID2()
19136
19357
  })
19137
19358
  };
19138
19359
  }
19360
+ acquireMetadataLock(options) {
19361
+ clearAbandonedReclaimGuard(this.lockPath);
19362
+ const deadline = Date.now() + (options.timeoutMs ?? 0);
19363
+ while (true) {
19364
+ try {
19365
+ return acquireFileLockSync(this.workspacePath, {
19366
+ ...options,
19367
+ timeoutMs: Math.max(0, deadline - Date.now())
19368
+ });
19369
+ } catch (error) {
19370
+ const code = error?.code;
19371
+ if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
19372
+ throw error;
19373
+ if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
19374
+ throw error;
19375
+ if (code !== "file_lock_stale")
19376
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
19377
+ }
19378
+ }
19379
+ }
19139
19380
  externalActionLockOptions() {
19140
19381
  const options = this.metadataLockOptions();
19141
19382
  return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
@@ -19153,10 +19394,11 @@ class TaskStore {
19153
19394
  }
19154
19395
  let handle;
19155
19396
  try {
19156
- handle = acquireFileLockSync(this.workspacePath, this.externalActionLockOptions());
19397
+ handle = this.acquireMetadataLock(this.externalActionLockOptions());
19157
19398
  } catch (error) {
19158
19399
  return this.lockFailure(error);
19159
19400
  }
19401
+ holdRoot(this.root);
19160
19402
  let result;
19161
19403
  try {
19162
19404
  if (!handle.verifyStillHeld())
@@ -19191,8 +19433,9 @@ class TaskStore {
19191
19433
  path: this.lockPath
19192
19434
  });
19193
19435
  }
19436
+ dropRoot(this.root);
19194
19437
  try {
19195
- handle.release();
19438
+ retryTransient(() => handle.release());
19196
19439
  } catch (error) {
19197
19440
  const released = this.lockFailure(error);
19198
19441
  const releaseError = released.ok ? "metadata lock release failed" : released.error;
@@ -19205,6 +19448,9 @@ class TaskStore {
19205
19448
  }
19206
19449
  recoveryLockOptions(observed, evidence, gate) {
19207
19450
  const options = this.metadataLockOptions();
19451
+ options.timeoutMs = 0;
19452
+ options.retry = { retries: 0 };
19453
+ options.parsePayload = parseMetadataLock;
19208
19454
  options.staleRecovery = "remove-if-unchanged";
19209
19455
  options.shouldReclaim = ({ payload }) => Boolean(observed && sameMetadataLock(payload, observed.data) && (evidence.state === "stopped" || evidence.state === "accounted_for"));
19210
19456
  options.shouldRemoveStaleLock = ({ raw, payload }) => {
@@ -19226,7 +19472,7 @@ class TaskStore {
19226
19472
  };
19227
19473
  this.replaceSnapshot(this.workspacePath, value, workspace);
19228
19474
  }
19229
- withLock(operation, options = this.metadataLockOptions()) {
19475
+ withLock(operation, options = this.metadataLockOptions(), contention = "busy") {
19230
19476
  try {
19231
19477
  this.initializeMutationStorage();
19232
19478
  } catch (error) {
@@ -19237,10 +19483,12 @@ class TaskStore {
19237
19483
  let handle;
19238
19484
  let result = failure2("storage_error", "metadata lock operation did not produce a result");
19239
19485
  try {
19240
- handle = acquireFileLockSync(this.workspacePath, options);
19486
+ handle = this.acquireMetadataLock(options);
19241
19487
  } catch (error) {
19242
- result = this.lockFailure(error);
19488
+ result = this.lockFailure(error, contention);
19243
19489
  }
19490
+ if (handle)
19491
+ holdRoot(this.root);
19244
19492
  if (handle) {
19245
19493
  try {
19246
19494
  if (!handle.verifyStillHeld())
@@ -19259,8 +19507,9 @@ class TaskStore {
19259
19507
  }
19260
19508
  }
19261
19509
  if (handle) {
19510
+ dropRoot(this.root);
19262
19511
  try {
19263
- handle.release();
19512
+ retryTransient(() => handle.release());
19264
19513
  } catch (error) {
19265
19514
  const releaseFailure = this.lockFailure(error);
19266
19515
  const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
@@ -19269,25 +19518,65 @@ class TaskStore {
19269
19518
  }
19270
19519
  return result;
19271
19520
  }
19272
- lockFailure(error) {
19521
+ lockDirWritable() {
19522
+ const probe = `${this.lockPath}.${process.pid}.${randomUUID2()}.probe`;
19523
+ try {
19524
+ fs5.closeSync(fs5.openSync(probe, "wx", 384));
19525
+ } catch {
19526
+ return false;
19527
+ }
19528
+ try {
19529
+ fs5.rmSync(probe, { force: true });
19530
+ } catch {}
19531
+ return true;
19532
+ }
19533
+ lockHolderPresent() {
19534
+ try {
19535
+ fs5.lstatSync(this.lockPath);
19536
+ return true;
19537
+ } catch (error) {
19538
+ return error?.code !== "ENOENT";
19539
+ }
19540
+ }
19541
+ lockFailure(error, contention = "busy") {
19273
19542
  const value = error;
19274
19543
  const code = typeof value?.code === "string" ? value.code : "";
19275
- if (code === "EEXIST" || code === "file_lock_timeout") {
19276
- const lock = this.readLockSnapshot();
19277
- if (lock.ok && lock.data?.data.externalAction)
19544
+ if (isTransientWindowsError(error) && !this.lockHolderPresent() && !this.lockDirWritable())
19545
+ return failure2("storage_error", `cannot create the workspace metadata lock (${code}); no other Workit call holds it`, {
19546
+ path: this.lockPath,
19547
+ guidance: `Check permissions and the read-only attribute on ${path4.dirname(this.lockPath)}.`
19548
+ });
19549
+ if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
19550
+ let lock = null;
19551
+ try {
19552
+ lock = parseMetadataLockOrNull(fs5.readFileSync(this.lockPath, "utf8"));
19553
+ } catch {}
19554
+ if (lock?.externalAction)
19278
19555
  return failure2("writer_conflict", "workspace is reserved by a managed external action", {
19279
19556
  outcome: "not_started",
19280
19557
  path: this.lockPath
19281
19558
  });
19559
+ if (contention === "busy")
19560
+ return failure2("busy", `workspace metadata lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
19561
+ outcome: "not_started",
19562
+ path: this.lockPath,
19563
+ guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
19564
+ });
19282
19565
  }
19283
19566
  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));
19284
19567
  return failure2(recovery ? "recovery_required" : "storage_error", `metadata lock operation failed: ${String(error)}`, { path: this.lockPath });
19285
19568
  }
19286
19569
  initializeMutationStorage() {
19287
- fs4.mkdirSync(this.tasksDir, { recursive: true });
19288
- fs4.mkdirSync(this.recoveryDir, { recursive: true });
19289
- fs4.writeFileSync(this.gitignorePath, `*
19290
- `);
19570
+ retryTransient(() => fs5.mkdirSync(this.tasksDir, { recursive: true }));
19571
+ retryTransient(() => fs5.mkdirSync(this.recoveryDir, { recursive: true }));
19572
+ let current = null;
19573
+ try {
19574
+ current = retryTransient(() => fs5.readFileSync(this.gitignorePath, "utf8"));
19575
+ } catch {}
19576
+ if (current !== `*
19577
+ `)
19578
+ retryTransient(() => fs5.writeFileSync(this.gitignorePath, `*
19579
+ `));
19291
19580
  }
19292
19581
  replaceSnapshot(file, value, previous) {
19293
19582
  let temporary;
@@ -19299,14 +19588,14 @@ class TaskStore {
19299
19588
  else if (typeof previous === "string")
19300
19589
  this.saveRecovery(file, previous);
19301
19590
  temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
19302
- const fd = fs4.openSync(temporary, "wx", 384);
19591
+ const fd = fs5.openSync(temporary, "wx", 384);
19303
19592
  try {
19304
- fs4.writeSync(fd, jsonBytes(value));
19305
- fs4.fsyncSync(fd);
19593
+ fs5.writeSync(fd, jsonBytes(value));
19594
+ fs5.fsyncSync(fd);
19306
19595
  } finally {
19307
- fs4.closeSync(fd);
19596
+ fs5.closeSync(fd);
19308
19597
  }
19309
- fs4.renameSync(temporary, file);
19598
+ retryTransient(() => fs5.renameSync(temporary, file));
19310
19599
  temporary = undefined;
19311
19600
  this.fsyncDirectory(path4.dirname(file));
19312
19601
  if (path4.dirname(file) === this.tasksDir)
@@ -19319,13 +19608,13 @@ class TaskStore {
19319
19608
  } finally {
19320
19609
  if (temporary)
19321
19610
  try {
19322
- fs4.unlinkSync(temporary);
19611
+ fs5.unlinkSync(temporary);
19323
19612
  } catch {}
19324
19613
  }
19325
19614
  }
19326
19615
  readIndex(workspaceId) {
19327
19616
  try {
19328
- const value = JSON.parse(fs4.readFileSync(this.indexPath, "utf8"));
19617
+ const value = JSON.parse(fs5.readFileSync(this.indexPath, "utf8"));
19329
19618
  if (!isObject2(value) || value.version !== INDEX_VERSION || value.workspaceId !== workspaceId || !isObject2(value.tasks))
19330
19619
  return {};
19331
19620
  const tasks = {};
@@ -19340,15 +19629,15 @@ class TaskStore {
19340
19629
  writeIndex(workspaceId, entries) {
19341
19630
  const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
19342
19631
  try {
19343
- fs4.writeFileSync(temporary, JSON.stringify({
19632
+ fs5.writeFileSync(temporary, JSON.stringify({
19344
19633
  version: INDEX_VERSION,
19345
19634
  workspaceId,
19346
19635
  tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
19347
19636
  }), { mode: 384, flag: "wx" });
19348
- fs4.renameSync(temporary, this.indexPath);
19637
+ retryTransient(() => fs5.renameSync(temporary, this.indexPath));
19349
19638
  } catch {
19350
19639
  try {
19351
- fs4.unlinkSync(temporary);
19640
+ fs5.unlinkSync(temporary);
19352
19641
  } catch {}
19353
19642
  }
19354
19643
  }
@@ -19366,33 +19655,153 @@ class TaskStore {
19366
19655
  const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
19367
19656
  const id = target === "task" ? path4.basename(file, ".json") : "workspace";
19368
19657
  const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
19369
- if (fs4.existsSync(destination)) {
19370
- if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
19371
- return;
19372
- throw new Error("recovery copy already exists with different bytes");
19658
+ if (fs5.existsSync(destination)) {
19659
+ if (digestBytes(retryTransient(() => fs5.readFileSync(destination))) !== digestBytes(bytes))
19660
+ throw new Error("recovery copy already exists with different bytes");
19661
+ const stamp = new Date;
19662
+ retryTransient(() => fs5.utimesSync(destination, stamp, stamp));
19663
+ this.pruneRecovery(`${target}.${id}.`, destination);
19664
+ return;
19373
19665
  }
19374
19666
  let temporary;
19375
19667
  try {
19376
19668
  temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
19377
- const fd = fs4.openSync(temporary, "wx", 384);
19669
+ const fd = fs5.openSync(temporary, "wx", 384);
19378
19670
  try {
19379
- fs4.writeSync(fd, bytes);
19380
- fs4.fsyncSync(fd);
19671
+ fs5.writeSync(fd, bytes);
19672
+ fs5.fsyncSync(fd);
19381
19673
  } finally {
19382
- fs4.closeSync(fd);
19674
+ fs5.closeSync(fd);
19383
19675
  }
19384
- fs4.renameSync(temporary, destination);
19676
+ retryTransient(() => fs5.renameSync(temporary, destination));
19385
19677
  temporary = undefined;
19386
19678
  this.fsyncDirectory(this.recoveryDir);
19679
+ this.pruneRecovery(`${target}.${id}.`, destination);
19387
19680
  } finally {
19388
19681
  if (temporary)
19389
19682
  try {
19390
- fs4.unlinkSync(temporary);
19683
+ fs5.unlinkSync(temporary);
19391
19684
  } catch {}
19392
19685
  }
19393
19686
  }
19687
+ pruneRecovery(prefix, keep) {
19688
+ try {
19689
+ const copies = this.recoveryCopies(prefix);
19690
+ const surplus = this.surplusCopies(copies, path4.basename(keep));
19691
+ for (const copy of surplus)
19692
+ fs5.rmSync(copy.path, { force: true });
19693
+ } catch {}
19694
+ }
19695
+ recoveryCopies(prefix = "") {
19696
+ if (!fs5.existsSync(this.recoveryDir))
19697
+ return [];
19698
+ const copies = [];
19699
+ for (const name of fs5.readdirSync(this.recoveryDir)) {
19700
+ if (!name.startsWith(prefix))
19701
+ continue;
19702
+ const match = RECOVERY_NAME.exec(name);
19703
+ if (!match)
19704
+ continue;
19705
+ const file = path4.join(this.recoveryDir, name);
19706
+ try {
19707
+ const stat = fs5.lstatSync(file, { bigint: true });
19708
+ if (!stat.isFile())
19709
+ continue;
19710
+ copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
19711
+ } catch {}
19712
+ }
19713
+ return copies;
19714
+ }
19715
+ surplusCopies(copies, keep) {
19716
+ 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);
19717
+ return ordered.slice(RECOVERY_COPIES_PER_RECORD);
19718
+ }
19719
+ collectGarbage(options = {}) {
19720
+ const dryRun = options.dryRun === true;
19721
+ const report = {
19722
+ dryRun,
19723
+ recovery: { removed: 0, removedBytes: 0, kept: 0 },
19724
+ temporary: { removed: 0 },
19725
+ candidates: { removed: 0, tasks: [], skippedActive: [], skippedClosed: [], failed: [] }
19726
+ };
19727
+ if (!fs5.existsSync(this.workitDir))
19728
+ return success(null, null, report);
19729
+ const sweep = () => {
19730
+ try {
19731
+ const groups = new Map;
19732
+ for (const copy of this.recoveryCopies())
19733
+ groups.set(copy.group, [...groups.get(copy.group) ?? [], copy]);
19734
+ for (const copies of groups.values()) {
19735
+ const surplus = this.surplusCopies(copies);
19736
+ report.recovery.kept += copies.length - surplus.length;
19737
+ for (const copy of surplus) {
19738
+ report.recovery.removed += 1;
19739
+ try {
19740
+ report.recovery.removedBytes += fs5.statSync(copy.path).size;
19741
+ } catch {}
19742
+ if (!dryRun)
19743
+ fs5.rmSync(copy.path, { force: true });
19744
+ }
19745
+ }
19746
+ const nowMs = Date.now();
19747
+ for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
19748
+ if (!fs5.existsSync(directory))
19749
+ continue;
19750
+ for (const name of fs5.readdirSync(directory)) {
19751
+ if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
19752
+ continue;
19753
+ const file = path4.join(directory, name);
19754
+ const stat = fs5.lstatSync(file);
19755
+ if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
19756
+ continue;
19757
+ report.temporary.removed += 1;
19758
+ if (!dryRun)
19759
+ fs5.rmSync(file, { force: true });
19760
+ }
19761
+ }
19762
+ return success(null, null, null);
19763
+ } catch (error) {
19764
+ return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
19765
+ path: this.recoveryDir
19766
+ });
19767
+ }
19768
+ };
19769
+ const pruned = dryRun ? sweep() : this.withLock(sweep);
19770
+ if (!pruned.ok)
19771
+ return pruned;
19772
+ const tasks = this.listTasks();
19773
+ if (!tasks.ok)
19774
+ return tasks;
19775
+ for (const task of tasks.data) {
19776
+ const deduped = dedupeCandidates(task.candidates);
19777
+ const removed = task.candidates.length - deduped.length;
19778
+ if (removed === 0)
19779
+ continue;
19780
+ if (task.status === "active") {
19781
+ report.candidates.skippedActive.push(task.id);
19782
+ continue;
19783
+ }
19784
+ if (task.status === "closed") {
19785
+ report.candidates.skippedClosed.push(task.id);
19786
+ continue;
19787
+ }
19788
+ if (!dryRun) {
19789
+ const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
19790
+ ...current,
19791
+ candidates: dedupeCandidates(current.candidates)
19792
+ }));
19793
+ if (!written.ok) {
19794
+ report.candidates.failed.push(task.id);
19795
+ continue;
19796
+ }
19797
+ }
19798
+ report.candidates.removed += removed;
19799
+ report.candidates.tasks.push(task.id);
19800
+ }
19801
+ return success(null, null, report);
19802
+ }
19394
19803
  findRecovery(target, taskId, digest, workspace) {
19395
- if (!fs4.existsSync(this.recoveryDir))
19804
+ if (!fs5.existsSync(this.recoveryDir))
19396
19805
  return null;
19397
19806
  const workspaceId = workspace?.id ?? this.trustedWorkspaceId();
19398
19807
  if (target === "workspace" && !workspaceId)
@@ -19404,9 +19813,9 @@ class TaskStore {
19404
19813
  if (candidate.target !== target || candidate.digest !== digest)
19405
19814
  continue;
19406
19815
  try {
19407
- if (!fs4.lstatSync(candidate.path).isFile())
19816
+ if (!fs5.lstatSync(candidate.path).isFile())
19408
19817
  continue;
19409
- const bytes = fs4.readFileSync(candidate.path);
19818
+ const bytes = fs5.readFileSync(candidate.path);
19410
19819
  if (digestBytes(bytes) !== digest)
19411
19820
  continue;
19412
19821
  if (target === "workspace") {
@@ -19424,11 +19833,11 @@ class TaskStore {
19424
19833
  }
19425
19834
  fsyncDirectory(directory) {
19426
19835
  try {
19427
- const fd = fs4.openSync(directory, "r");
19836
+ const fd = fs5.openSync(directory, "r");
19428
19837
  try {
19429
- fs4.fsyncSync(fd);
19838
+ fs5.fsyncSync(fd);
19430
19839
  } finally {
19431
- fs4.closeSync(fd);
19840
+ fs5.closeSync(fd);
19432
19841
  }
19433
19842
  } catch (error) {
19434
19843
  if (process.platform !== "win32")
@@ -19436,14 +19845,14 @@ class TaskStore {
19436
19845
  }
19437
19846
  }
19438
19847
  trustedWorkspaceId() {
19439
- if (!fs4.existsSync(this.tasksDir))
19848
+ if (!fs5.existsSync(this.tasksDir))
19440
19849
  return null;
19441
19850
  const ids = new Set;
19442
19851
  try {
19443
- for (const name of fs4.readdirSync(this.tasksDir)) {
19852
+ for (const name of fs5.readdirSync(this.tasksDir)) {
19444
19853
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
19445
19854
  return null;
19446
- const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
19855
+ const parsed = this.parseBytes(fs5.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
19447
19856
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
19448
19857
  return null;
19449
19858
  ids.add(parsed.data.workspaceId);
@@ -19455,14 +19864,14 @@ class TaskStore {
19455
19864
  }
19456
19865
  snapshotBytes(file) {
19457
19866
  try {
19458
- return fs4.readFileSync(file);
19867
+ return retryTransient(() => fs5.readFileSync(file));
19459
19868
  } catch {
19460
19869
  return null;
19461
19870
  }
19462
19871
  }
19463
19872
  readRecord(file, schema) {
19464
19873
  try {
19465
- const bytes = fs4.readFileSync(file, "utf8");
19874
+ const bytes = retryTransient(() => fs5.readFileSync(file, "utf8"));
19466
19875
  return { exists: true, result: this.parseBytes(bytes, schema) };
19467
19876
  } catch (error) {
19468
19877
  if (error?.code === "ENOENT")
@@ -19475,7 +19884,7 @@ class TaskStore {
19475
19884
  };
19476
19885
  }
19477
19886
  }
19478
- parseBytes(bytes, schema) {
19887
+ parseBytes(bytes, schema, mode = "read") {
19479
19888
  let value;
19480
19889
  try {
19481
19890
  value = JSON.parse(typeof bytes === "string" ? bytes : bytes.toString("utf8"));
@@ -19484,27 +19893,28 @@ class TaskStore {
19484
19893
  }
19485
19894
  if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
19486
19895
  return failure2("unsupported_version", "unsupported snapshot schema version");
19487
- const parsed = schema.safeParse(value);
19488
- if (parsed.success)
19896
+ const parsed = parseStoredRecord(schema, value);
19897
+ if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
19489
19898
  return success(null, null, parsed.data);
19490
19899
  const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
19900
+ const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
19901
+ if (parsed.success)
19902
+ return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
19903
+ if (parsed.critical.length > 0)
19904
+ return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
19491
19905
  if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
19492
19906
  return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
19493
19907
  return failure2("recovery_required", "snapshot does not satisfy its schema");
19494
19908
  }
19495
- processStart(pid) {
19496
- try {
19497
- return fs4.readFileSync(`/proc/${pid}/stat`, "utf8").split(" ")[21] ?? null;
19498
- } catch {
19499
- return null;
19500
- }
19501
- }
19502
19909
  conflict(expected, actual) {
19503
19910
  return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
19504
19911
  expectedRevision: expected,
19505
19912
  actualRevision: actual
19506
19913
  });
19507
19914
  }
19915
+ workspaceConflict(expected, actual) {
19916
+ return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
19917
+ }
19508
19918
  taskPath(taskId) {
19509
19919
  return path4.join(this.tasksDir, `${taskId}.json`);
19510
19920
  }
@@ -19530,53 +19940,116 @@ class TaskStore {
19530
19940
  return path4.join(this.workitDir, ".gitignore");
19531
19941
  }
19532
19942
  }
19533
- // packages/workit-core/src/core/task-engine.ts
19534
- import { realpathSync as realpathSync6 } from "node:fs";
19535
-
19536
- // packages/workit-core/src/core/authority.ts
19537
- import { createHash as createHash5 } from "node:crypto";
19538
- import * as fs6 from "node:fs";
19539
- import path9 from "node:path";
19540
-
19541
- // packages/workit-core/src/core/auto-approval.ts
19542
- import { createHash as createHash4 } from "node:crypto";
19543
- import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
19544
- import path8 from "node:path";
19545
-
19546
- // packages/workit-core/src/core/workspaces.ts
19547
- import { createHash as createHash3 } from "node:crypto";
19548
- import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
19549
- import path6 from "node:path";
19550
-
19551
- // packages/workit-core/src/core/config.ts
19552
- import {
19553
- copyFileSync,
19554
- cpSync,
19555
- existsSync as existsSync3,
19556
- mkdirSync as mkdirSync2,
19557
- readFileSync as readFileSync2,
19558
- readdirSync as readdirSync2,
19559
- writeFileSync as writeFileSync2
19560
- } from "node:fs";
19561
- import os from "node:os";
19562
- import path5 from "node:path";
19563
-
19564
- // packages/workit-core/src/core/boundary.ts
19565
- import { statSync as statSync2 } from "node:fs";
19566
- var EVENT = {
19567
- initialization: "initialization",
19568
- provenance: "provenance",
19569
- assets: "assets",
19570
- configurationSource: "configuration_source",
19571
- hooks: "hooks",
19572
- mcpConnection: "mcp_connection",
19573
- migration: "migration",
19574
- installSteps: "install_steps",
19575
- uncaughtFailure: "uncaught_failure",
19576
- toolsFailed: "tools_failed",
19577
- doctor: "doctor"
19943
+ // packages/workit-core/src/core/workers.ts
19944
+ var same = (left, right) => {
19945
+ try {
19946
+ return canonicalJson(left) === canonicalJson(right);
19947
+ } catch {
19948
+ return false;
19949
+ }
19578
19950
  };
19579
- var errorDetail = (err) => {
19951
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
19952
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
19953
+ var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
19954
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
19955
+ var validPath = (value) => typeof value === "string" && value.length > 0 && !Array.from(value).some((char) => {
19956
+ const code = char.charCodeAt(0);
19957
+ return code < 32 || code === 127;
19958
+ }) && !value.split(/[\\/]/).includes("..");
19959
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
19960
+ var workerIdOf = (value) => value.workerId ?? null;
19961
+ var currentWriterOwnsTask = (task, workspace, caller) => {
19962
+ const owner = workspace.writer?.owner;
19963
+ return task.status === "active" && task.workspaceId === workspace.id && owner?.taskId === task.id && owner.session.host === caller.host && owner.session.handle === caller.actor;
19964
+ };
19965
+ function assertProductWriteAllowed(input) {
19966
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
19967
+ return failure2("invalid_input", "invalid product write path");
19968
+ if (!input.store)
19969
+ return failure2("permission_denied", "write authorization requires core state");
19970
+ const task = input.store.readTask(input.task.id);
19971
+ if (!task.ok)
19972
+ return task;
19973
+ const workspace = input.store.readWorkspace();
19974
+ if (!workspace.ok)
19975
+ return workspace;
19976
+ if (!workspace.data)
19977
+ return failure2("not_found", "workspace not found");
19978
+ const workspaceRecord = workspace.data;
19979
+ if (task.data.status !== "active")
19980
+ return failure2("invalid_transition", "paused or closed tasks cannot own product writes");
19981
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
19982
+ return failure2("recovery_required", "task and workspace bindings are invalid");
19983
+ const writer = workspaceRecord.writer;
19984
+ if (!writer)
19985
+ return failure2("permission_denied", "checkout has no writer owner");
19986
+ if (writer.state === "uncertain")
19987
+ return failure2("recovery_required", "checkout writer requires recovery", {
19988
+ owner: writer.owner
19989
+ });
19990
+ const caller = callerValue(input.caller);
19991
+ const workerId = workerIdOf(input.caller);
19992
+ const owner = writer.owner;
19993
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
19994
+ return failure2("writer_conflict", "checkout is owned by another validated actor", { owner });
19995
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
19996
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
19997
+ return failure2("permission_denied", "only implementers can own product writes");
19998
+ 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))))
19999
+ return failure2("recovery_required", "worker session is not an active writer");
20000
+ return success(null, workspaceRecord.revision, owner);
20001
+ }
20002
+
20003
+ // packages/workit-core/src/core/task-evaluation.ts
20004
+ import { createHash as createHash6 } from "node:crypto";
20005
+ import * as fs8 from "node:fs";
20006
+ import { spawnSync as spawnSync3 } from "node:child_process";
20007
+ import path10 from "node:path";
20008
+
20009
+ // packages/workit-core/src/core/authority.ts
20010
+ import { createHash as createHash5 } from "node:crypto";
20011
+ import * as fs7 from "node:fs";
20012
+ import path9 from "node:path";
20013
+
20014
+ // packages/workit-core/src/core/auto-approval.ts
20015
+ import { createHash as createHash4 } from "node:crypto";
20016
+ import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
20017
+ import path8 from "node:path";
20018
+
20019
+ // packages/workit-core/src/core/workspaces.ts
20020
+ import { createHash as createHash3 } from "node:crypto";
20021
+ import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
20022
+ import path6 from "node:path";
20023
+
20024
+ // packages/workit-core/src/core/config.ts
20025
+ import {
20026
+ copyFileSync,
20027
+ cpSync,
20028
+ existsSync as existsSync3,
20029
+ mkdirSync as mkdirSync2,
20030
+ readFileSync as readFileSync2,
20031
+ readdirSync as readdirSync2,
20032
+ writeFileSync as writeFileSync2
20033
+ } from "node:fs";
20034
+ import os from "node:os";
20035
+ import path5 from "node:path";
20036
+
20037
+ // packages/workit-core/src/core/boundary.ts
20038
+ import { statSync as statSync2 } from "node:fs";
20039
+ var EVENT = {
20040
+ initialization: "initialization",
20041
+ provenance: "provenance",
20042
+ assets: "assets",
20043
+ configurationSource: "configuration_source",
20044
+ hooks: "hooks",
20045
+ mcpConnection: "mcp_connection",
20046
+ migration: "migration",
20047
+ installSteps: "install_steps",
20048
+ uncaughtFailure: "uncaught_failure",
20049
+ toolsFailed: "tools_failed",
20050
+ doctor: "doctor"
20051
+ };
20052
+ var errorDetail = (err) => {
19580
20053
  if (err instanceof Error) {
19581
20054
  return { error_name: err.name, error: err.message };
19582
20055
  }
@@ -20205,13 +20678,13 @@ var gitContext = (workspaceRoot, paths = []) => {
20205
20678
  };
20206
20679
 
20207
20680
  // packages/workit-core/src/core/vcs-config.ts
20208
- import fs5 from "node:fs";
20681
+ import fs6 from "node:fs";
20209
20682
  import path7 from "node:path";
20210
- import { spawnSync } from "node:child_process";
20683
+ import { spawnSync as spawnSync2 } from "node:child_process";
20211
20684
  var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
20212
20685
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
20213
20686
  var remoteProvider = (cwd) => {
20214
- const r = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
20687
+ const r = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
20215
20688
  if (r.status !== 0)
20216
20689
  return null;
20217
20690
  const url = (r.stdout ?? "").trim();
@@ -20248,7 +20721,7 @@ var remoteEndpoint = (remote) => {
20248
20721
  var configuredSshEndpoint = (host) => {
20249
20722
  if (host.startsWith("-"))
20250
20723
  return null;
20251
- const resolved = spawnSync("ssh", ["-G", host], {
20724
+ const resolved = spawnSync2("ssh", ["-G", host], {
20252
20725
  encoding: "utf8",
20253
20726
  timeout: 5000,
20254
20727
  maxBuffer: 1024 * 1024,
@@ -20298,7 +20771,7 @@ var readVcsConfig = () => {
20298
20771
  const file = vcsConfigPath();
20299
20772
  let raw;
20300
20773
  try {
20301
- raw = fs5.readFileSync(file, "utf8");
20774
+ raw = fs6.readFileSync(file, "utf8");
20302
20775
  } catch {
20303
20776
  return { status: "missing", path: file, config: {} };
20304
20777
  }
@@ -20399,7 +20872,7 @@ function vcsCliIdentity(cwd) {
20399
20872
  PATH: process.env.PATH ?? "",
20400
20873
  ...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
20401
20874
  };
20402
- const result = spawnSync(bin, ["api", "user"], {
20875
+ const result = spawnSync2(bin, ["api", "user"], {
20403
20876
  cwd: root,
20404
20877
  encoding: "utf8",
20405
20878
  env
@@ -20441,7 +20914,7 @@ function mergedPrStyle(limit = 6, cwd) {
20441
20914
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
20442
20915
  descriptionPreview: desc.slice(0, 600)
20443
20916
  });
20444
- const remoteResult = spawnSync("git", ["remote", "get-url", "--push", "--all", "origin"], {
20917
+ const remoteResult = spawnSync2("git", ["remote", "get-url", "--push", "--all", "origin"], {
20445
20918
  cwd,
20446
20919
  encoding: "utf8"
20447
20920
  });
@@ -20457,7 +20930,7 @@ function mergedPrStyle(limit = 6, cwd) {
20457
20930
  if (!hostingApiHostMatches(remote, host))
20458
20931
  return { ok: false, error: "PR style target does not match the provider host" };
20459
20932
  if (provider === "gitlab") {
20460
- const run = (args) => spawnSync("glab", ["api", ...args], {
20933
+ const run = (args) => spawnSync2("glab", ["api", ...args], {
20461
20934
  cwd,
20462
20935
  encoding: "utf8",
20463
20936
  env
@@ -20477,7 +20950,7 @@ function mergedPrStyle(limit = 6, cwd) {
20477
20950
  });
20478
20951
  }
20479
20952
  } else if (provider === "github") {
20480
- const r = spawnSync("gh", [
20953
+ const r = spawnSync2("gh", [
20481
20954
  "pr",
20482
20955
  "list",
20483
20956
  "--repo",
@@ -20536,66 +21009,6 @@ var verifyPushIdentity = (root, provider, account) => {
20536
21009
  return { ok: true };
20537
21010
  };
20538
21011
 
20539
- // packages/workit-core/src/core/workers.ts
20540
- var same = (left, right) => {
20541
- try {
20542
- return canonicalJson(left) === canonicalJson(right);
20543
- } catch {
20544
- return false;
20545
- }
20546
- };
20547
- var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
20548
- var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
20549
- var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
20550
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
20551
- var validPath = (value) => typeof value === "string" && value.length > 0 && !Array.from(value).some((char) => {
20552
- const code = char.charCodeAt(0);
20553
- return code < 32 || code === 127;
20554
- }) && !value.split(/[\\/]/).includes("..");
20555
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
20556
- var workerIdOf = (value) => value.workerId ?? null;
20557
- var currentWriterOwnsTask = (task, workspace, caller) => {
20558
- const owner = workspace.writer?.owner;
20559
- return task.status === "active" && task.workspaceId === workspace.id && owner?.taskId === task.id && owner.session.host === caller.host && owner.session.handle === caller.actor;
20560
- };
20561
- function assertProductWriteAllowed(input) {
20562
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
20563
- return failure2("invalid_input", "invalid product write path");
20564
- if (!input.store)
20565
- return failure2("permission_denied", "write authorization requires core state");
20566
- const task = input.store.readTask(input.task.id);
20567
- if (!task.ok)
20568
- return task;
20569
- const workspace = input.store.readWorkspace();
20570
- if (!workspace.ok)
20571
- return workspace;
20572
- if (!workspace.data)
20573
- return failure2("not_found", "workspace not found");
20574
- const workspaceRecord = workspace.data;
20575
- if (task.data.status !== "active")
20576
- return failure2("invalid_transition", "paused or closed tasks cannot own product writes");
20577
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
20578
- return failure2("recovery_required", "task and workspace bindings are invalid");
20579
- const writer = workspaceRecord.writer;
20580
- if (!writer)
20581
- return failure2("permission_denied", "checkout has no writer owner");
20582
- if (writer.state === "uncertain")
20583
- return failure2("recovery_required", "checkout writer requires recovery", {
20584
- owner: writer.owner
20585
- });
20586
- const caller = callerValue(input.caller);
20587
- const workerId = workerIdOf(input.caller);
20588
- const owner = writer.owner;
20589
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
20590
- return failure2("writer_conflict", "checkout is owned by another validated actor", { owner });
20591
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
20592
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
20593
- return failure2("permission_denied", "only implementers can own product writes");
20594
- 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))))
20595
- return failure2("recovery_required", "worker session is not an active writer");
20596
- return success(null, workspaceRecord.revision, owner);
20597
- }
20598
-
20599
21012
  // packages/workit-core/src/core/auto-approval.ts
20600
21013
  var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
20601
21014
  var OPERATION_CLASS = {
@@ -20882,7 +21295,7 @@ var digestBytes2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
20882
21295
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
20883
21296
  let root;
20884
21297
  try {
20885
- root = fs6.realpathSync(checkoutRoot);
21298
+ root = fs7.realpathSync(checkoutRoot);
20886
21299
  } catch {
20887
21300
  return failure2("permission_denied", "checkout root is unavailable");
20888
21301
  }
@@ -20905,8 +21318,8 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
20905
21318
  if (!segment)
20906
21319
  continue;
20907
21320
  current = path9.join(current, segment);
20908
- const stat = fs6.lstatSync(current);
20909
- const real = fs6.realpathSync(current);
21321
+ const stat = fs7.lstatSync(current);
21322
+ const real = fs7.realpathSync(current);
20910
21323
  if (real !== root && !real.startsWith(`${root}${path9.sep}`))
20911
21324
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
20912
21325
  fields: [
@@ -20923,7 +21336,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
20923
21336
  if (current !== target && !stat.isDirectory())
20924
21337
  return failure2("invalid_input", "document reference ancestor is not a directory");
20925
21338
  }
20926
- if (digestBytes2(fs6.readFileSync(target)) !== reference.digest)
21339
+ if (digestBytes2(fs7.readFileSync(target)) !== reference.digest)
20927
21340
  return failure2("permission_denied", "approved document bytes have changed");
20928
21341
  } catch {
20929
21342
  return failure2("invalid_input", "approved document is unavailable");
@@ -21286,18 +21699,41 @@ function reconcileAction(input) {
21286
21699
  var bindingCovers = scopeCovers;
21287
21700
  var verifyDecisionContent = verifyContentRefs;
21288
21701
 
21289
- // packages/workit-core/src/core/policy-resolver.ts
21290
- var resolverInputSchema = object2({
21291
- intent: intentSchema,
21292
- assessment: assessmentSchema,
21293
- constraints: array(constraintSchema),
21294
- prior: object2({
21295
- decisions: array(decisionSchema),
21296
- findings: array(findingSchema),
21297
- requirements: array(requirementSchema)
21298
- }).strict(),
21299
- preferences: object2({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
21300
- }).strict();
21702
+ // packages/workit-core/src/core/task-evaluation.ts
21703
+ var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
21704
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
21705
+ var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
21706
+ var canonicalPath = (value) => {
21707
+ if (typeof value !== "string" || !value)
21708
+ return null;
21709
+ if (value.includes("\\") || value.split("/").includes(".."))
21710
+ return null;
21711
+ const normalized = path10.posix.normalize(value);
21712
+ if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
21713
+ return null;
21714
+ return normalized === "." ? "." : normalized.replace(/\/$/, "");
21715
+ };
21716
+ var canonicalScope = (scope) => {
21717
+ const paths = scope.paths.length ? scope.paths : ["."];
21718
+ const normalizedPaths = paths.map(canonicalPath);
21719
+ const normalizedExclusions = scope.exclusions.map(canonicalPath);
21720
+ if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
21721
+ return null;
21722
+ return {
21723
+ description: scope.description,
21724
+ paths: [...new Set(normalizedPaths)],
21725
+ exclusions: [...new Set(normalizedExclusions)]
21726
+ };
21727
+ };
21728
+ var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
21729
+ var scopeMatches = (value, scope) => {
21730
+ const normalized = canonicalScope(scope);
21731
+ if (!normalized)
21732
+ return false;
21733
+ const paths = normalized.paths;
21734
+ return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
21735
+ };
21736
+ var pathRelevant = (value, scope) => scopeMatches(value, scope);
21301
21737
  var compareCodeUnits2 = (left, right) => {
21302
21738
  const length = Math.min(left.length, right.length);
21303
21739
  for (let index = 0;index < length; index += 1) {
@@ -21307,362 +21743,8 @@ var compareCodeUnits2 = (left, right) => {
21307
21743
  }
21308
21744
  return left.length - right.length;
21309
21745
  };
21310
- var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits2);
21311
- var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits2(canonicalJson(left), canonicalJson(right)));
21312
- var normalizedScope = (scope) => ({
21313
- description: scope.description,
21314
- paths: stableUnique(scope.paths),
21315
- exclusions: stableUnique(scope.exclusions)
21316
- });
21317
- var normalizedRefList = (values) => stableList(values);
21318
- var normalizeAssessment = (assessment) => ({
21319
- facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
21320
- signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
21321
- name,
21322
- { ...signal, refs: normalizedRefList(signal.refs) }
21323
- ])),
21324
- consequences: stableList(assessment.consequences.map((consequence) => ({
21325
- ...consequence,
21326
- fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
21327
- }))),
21328
- verification: stableList(assessment.verification.map((verification) => ({
21329
- ...verification,
21330
- scope: normalizedScope(verification.scope),
21331
- availableChecks: normalizedRefList(verification.availableChecks),
21332
- gaps: stableUnique(verification.gaps)
21333
- })))
21334
- });
21335
- var normalizeConstraint = (constraint) => ({
21336
- ...constraint,
21337
- source: constraint.source,
21338
- requires: stableList(constraint.requires.map((requirement) => ({
21339
- ...requirement,
21340
- scope: normalizedScope(requirement.scope),
21341
- refs: normalizedRefList(requirement.refs)
21342
- })))
21343
- });
21344
- var normalizeDecision = (decision) => ({
21345
- ...decision,
21346
- binding: {
21347
- ...decision.binding,
21348
- scope: normalizedScope(decision.binding.scope),
21349
- contentRefs: normalizedRefList(decision.binding.contentRefs)
21350
- },
21351
- requirementIds: stableUnique(decision.requirementIds)
21352
- });
21353
- var normalizeIntent = (intent) => ({
21354
- ...intent,
21355
- scope: normalizedScope(intent.scope),
21356
- authorityRefs: normalizedRefList(intent.authorityRefs)
21357
- });
21358
- var normalizePrior = (prior) => ({
21359
- decisions: stableList(prior.decisions.map(normalizeDecision)),
21360
- findings: stableList(prior.findings.map((finding) => ({
21361
- ...finding,
21362
- scope: normalizedScope(finding.scope),
21363
- refs: normalizedRefList(finding.refs),
21364
- resolution: finding.resolution ? {
21365
- ...finding.resolution,
21366
- evidenceIds: stableUnique(finding.resolution.evidenceIds),
21367
- decisionIds: stableUnique(finding.resolution.decisionIds)
21368
- } : null
21369
- }))),
21370
- requirements: stableList(prior.requirements.map((requirement) => ({
21371
- ...requirement,
21372
- scope: normalizedScope(requirement.scope)
21373
- })))
21374
- });
21375
- var invalidFields = (error) => ({
21376
- fields: error.issues.map((issue) => ({
21377
- path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
21378
- reason: issue.message
21379
- }))
21380
- });
21381
- function normalizeResolverInput(input) {
21382
- const parsed = resolverInputSchema.safeParse(input);
21383
- if (!parsed.success)
21384
- return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
21385
- const value = parsed.data;
21386
- const signals = value.assessment.signals;
21387
- if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
21388
- return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
21389
- fields: [
21390
- {
21391
- path: "assessment.signals",
21392
- reason: "contradictory behaviorChange and mechanicalLowRisk"
21393
- }
21394
- ]
21395
- });
21396
- const ids = value.constraints.map((constraint) => constraint.id);
21397
- if (new Set(ids).size !== ids.length)
21398
- return failure2("invalid_input", "constraint IDs must be unique", {
21399
- fields: [{ path: "constraints", reason: "duplicate constraint id" }]
21400
- });
21401
- if (value.preferences?.fast && value.preferences.thorough)
21402
- return failure2("invalid_input", "fast and thorough preferences contradict", {
21403
- fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
21404
- });
21405
- const normalized = {
21406
- intent: normalizeIntent(value.intent),
21407
- assessment: normalizeAssessment(value.assessment),
21408
- constraints: stableList(value.constraints.map(normalizeConstraint)),
21409
- prior: normalizePrior(value.prior),
21410
- ...value.preferences ? { preferences: { ...value.preferences } } : {},
21411
- policyVersion: POLICY_VERSION
21412
- };
21413
- return success(null, null, normalized);
21414
- }
21415
- var requirement = (spec) => ({
21416
- ...spec,
21417
- id: requirementId({
21418
- ruleId: spec.ruleId,
21419
- scope: spec.scope,
21420
- satisfaction: spec.satisfaction,
21421
- before: spec.before,
21422
- dependentAction: spec.dependentAction
21423
- })
21424
- });
21425
- var signalReason = (input, name) => input.assessment.signals[name].reason;
21426
- function resolveRequirements(input) {
21427
- const { signals } = input.assessment;
21428
- const scope = input.intent.scope;
21429
- const requirements = [];
21430
- const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
21431
- const hasUnknownFact = [
21432
- ...input.assessment.facts,
21433
- ...input.assessment.consequences.map(({ fact }) => fact)
21434
- ].some((fact) => fact.basis === "unknown");
21435
- if (hasUnknownSignal || hasUnknownFact)
21436
- requirements.push(requirement({
21437
- ruleId: "consequential-unknown",
21438
- dimension: "investigation",
21439
- scope,
21440
- reason: `Resolve consequential uncertainty before the dependent action: ${[
21441
- ...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
21442
- ...hasUnknownFact ? ["unknown fact"] : []
21443
- ].join(", ")}`,
21444
- satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
21445
- before: "dependent_action",
21446
- dependentAction: null,
21447
- acceptanceAllowed: false
21448
- }));
21449
- if (signals.productChoiceOpen.value === true)
21450
- requirements.push(requirement({
21451
- ruleId: "product-decision",
21452
- dimension: "decisions",
21453
- scope,
21454
- reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
21455
- satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
21456
- before: "dependent_action",
21457
- dependentAction: null,
21458
- acceptanceAllowed: false
21459
- }));
21460
- const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
21461
- if (signals.behaviorChange.value === true)
21462
- requirements.push(requirement({
21463
- ruleId: "behavioral-verification",
21464
- dimension: "testing",
21465
- scope,
21466
- reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
21467
- satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
21468
- before: "close",
21469
- dependentAction: null,
21470
- acceptanceAllowed: false
21471
- }), requirement({
21472
- ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
21473
- dimension: "review",
21474
- scope,
21475
- 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.",
21476
- 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.",
21477
- before: "close",
21478
- dependentAction: null,
21479
- acceptanceAllowed: !independentBehaviorReview
21480
- }));
21481
- else if (signals.mechanicalLowRisk.value === true)
21482
- requirements.push(requirement({
21483
- ruleId: "mechanical-existing-checks",
21484
- dimension: "verification",
21485
- scope,
21486
- reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
21487
- satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
21488
- before: "close",
21489
- dependentAction: null,
21490
- acceptanceAllowed: true
21491
- }), requirement({
21492
- ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
21493
- dimension: "review",
21494
- scope,
21495
- 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.",
21496
- 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.",
21497
- before: "close",
21498
- dependentAction: null,
21499
- acceptanceAllowed: !input.preferences?.thorough
21500
- }));
21501
- if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
21502
- requirements.push(requirement({
21503
- ruleId: "pre-pr-cleanup",
21504
- dimension: "verification",
21505
- scope,
21506
- reason: "Implementation output must be deslopped before it is delivered.",
21507
- satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
21508
- before: "dependent_action",
21509
- dependentAction: "hosting.pull_request",
21510
- acceptanceAllowed: true
21511
- }));
21512
- if (signals.durableAgreementNeeded.value === true)
21513
- requirements.push(requirement({
21514
- ruleId: "durable-spec",
21515
- dimension: "artifacts",
21516
- scope,
21517
- reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
21518
- satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
21519
- before: "write",
21520
- dependentAction: null,
21521
- acceptanceAllowed: true
21522
- }));
21523
- if (signals.coordinationPlanNeeded.value === true)
21524
- requirements.push(requirement({
21525
- ruleId: "coordination-plan",
21526
- dimension: "continuity",
21527
- scope,
21528
- reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
21529
- satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
21530
- before: "dependent_action",
21531
- dependentAction: null,
21532
- acceptanceAllowed: true
21533
- }));
21534
- if (signals.helperUseful.value === true && !input.preferences?.fast)
21535
- requirements.push(requirement({
21536
- ruleId: "helper-usefulness",
21537
- dimension: "delegation",
21538
- scope,
21539
- reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
21540
- satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
21541
- before: "dependent_action",
21542
- dependentAction: null,
21543
- acceptanceAllowed: true
21544
- }));
21545
- for (const constraint of input.constraints)
21546
- for (const obligation of constraint.requires) {
21547
- const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
21548
- requirements.push(requirement({
21549
- ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
21550
- kind: obligation.kind,
21551
- scope: normalizedScope(obligation.scope),
21552
- refs: normalizedRefList(obligation.refs),
21553
- method: obligation.method,
21554
- before: obligation.before,
21555
- dependentAction: obligation.dependentAction
21556
- })}`,
21557
- dimension,
21558
- scope: obligation.scope,
21559
- reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
21560
- satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
21561
- before: obligation.before,
21562
- dependentAction: obligation.dependentAction,
21563
- acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
21564
- }));
21565
- }
21566
- const seen = new Set;
21567
- return requirements.filter((item) => {
21568
- if (seen.has(item.id))
21569
- return false;
21570
- seen.add(item.id);
21571
- return true;
21572
- });
21573
- }
21574
- function resolvePolicy(input) {
21575
- const normalized = normalizeResolverInput(input);
21576
- if (!normalized.ok)
21577
- return normalized;
21578
- const requirements = resolveRequirements(normalized.data);
21579
- const policy = {
21580
- policyVersion: POLICY_VERSION,
21581
- inputDigest: sha256(canonicalJson(normalized.data)),
21582
- requirements
21583
- };
21584
- const parsed = policySchema.safeParse(policy);
21585
- if (!parsed.success)
21586
- return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
21587
- return success(null, null, parsed.data);
21588
- }
21589
- function diffPolicy(previous, next, reason, now) {
21590
- if (!policySchema.safeParse(previous).success && previous !== null)
21591
- throw new TypeError("previous policy is invalid");
21592
- if (!policySchema.safeParse(next).success)
21593
- throw new TypeError("next policy is invalid");
21594
- const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
21595
- const newIds = new Set(next.requirements.map((item) => item.id));
21596
- const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits2);
21597
- const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits2);
21598
- if (added.length === 0 && retired.length === 0)
21599
- return null;
21600
- const normalizedReason = reason.trim();
21601
- const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
21602
- const change = {
21603
- recordedAt: now,
21604
- fromInputDigest: previous?.inputDigest ?? null,
21605
- toInputDigest: next.inputDigest,
21606
- added,
21607
- retired,
21608
- reason: changeReason
21609
- };
21610
- const parsed = policyChangeSchema.safeParse(change);
21611
- if (!parsed.success)
21612
- throw new TypeError("policy change is invalid");
21613
- return parsed.data;
21614
- }
21615
-
21616
- // packages/workit-core/src/core/task-evaluation.ts
21617
- import { createHash as createHash6 } from "node:crypto";
21618
- import * as fs7 from "node:fs";
21619
- import { spawnSync as spawnSync2 } from "node:child_process";
21620
- import path10 from "node:path";
21621
- var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
21622
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
21623
- var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
21624
- var canonicalPath = (value) => {
21625
- if (typeof value !== "string" || !value)
21626
- return null;
21627
- if (value.includes("\\") || value.split("/").includes(".."))
21628
- return null;
21629
- const normalized = path10.posix.normalize(value);
21630
- if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
21631
- return null;
21632
- return normalized === "." ? "." : normalized.replace(/\/$/, "");
21633
- };
21634
- var canonicalScope = (scope) => {
21635
- const paths = scope.paths.length ? scope.paths : ["."];
21636
- const normalizedPaths = paths.map(canonicalPath);
21637
- const normalizedExclusions = scope.exclusions.map(canonicalPath);
21638
- if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
21639
- return null;
21640
- return {
21641
- description: scope.description,
21642
- paths: [...new Set(normalizedPaths)],
21643
- exclusions: [...new Set(normalizedExclusions)]
21644
- };
21645
- };
21646
- var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
21647
- var scopeMatches = (value, scope) => {
21648
- const normalized = canonicalScope(scope);
21649
- if (!normalized)
21650
- return false;
21651
- const paths = normalized.paths;
21652
- return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
21653
- };
21654
- var pathRelevant = (value, scope) => scopeMatches(value, scope);
21655
- var compareCodeUnits3 = (left, right) => {
21656
- const length = Math.min(left.length, right.length);
21657
- for (let index = 0;index < length; index += 1) {
21658
- const difference = left.charCodeAt(index) - right.charCodeAt(index);
21659
- if (difference !== 0)
21660
- return difference;
21661
- }
21662
- return left.length - right.length;
21663
- };
21664
21746
  var gitPaths = (root) => {
21665
- const result = spawnSync2("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
21747
+ const result = spawnSync3("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
21666
21748
  cwd: root,
21667
21749
  encoding: "buffer"
21668
21750
  });
@@ -21675,7 +21757,7 @@ var gitPaths = (root) => {
21675
21757
  };
21676
21758
  }
21677
21759
  const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")) : [];
21678
- const stagedDeleted = spawnSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
21760
+ const stagedDeleted = spawnSync3("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
21679
21761
  if (stagedDeleted.status !== 0) {
21680
21762
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
21681
21763
  if (!/not a git repository/i.test(stderr))
@@ -21688,7 +21770,7 @@ var gitPaths = (root) => {
21688
21770
  var walk = (root, directory, output) => {
21689
21771
  let names;
21690
21772
  try {
21691
- names = fs7.readdirSync(directory);
21773
+ names = fs8.readdirSync(directory);
21692
21774
  } catch {
21693
21775
  return true;
21694
21776
  }
@@ -21699,7 +21781,7 @@ var walk = (root, directory, output) => {
21699
21781
  const target = path10.join(directory, name);
21700
21782
  let stat;
21701
21783
  try {
21702
- stat = fs7.lstatSync(target);
21784
+ stat = fs8.lstatSync(target);
21703
21785
  } catch {
21704
21786
  uncertain = true;
21705
21787
  continue;
@@ -21718,10 +21800,10 @@ var scopeRoots = (root, scope) => {
21718
21800
  if (!inside(root, target))
21719
21801
  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` }] });
21720
21802
  let ancestor = target;
21721
- while (!fs7.existsSync(ancestor) && ancestor !== root)
21803
+ while (!fs8.existsSync(ancestor) && ancestor !== root)
21722
21804
  ancestor = path10.dirname(ancestor);
21723
21805
  try {
21724
- if (!inside(root, fs7.realpathSync(ancestor)))
21806
+ if (!inside(root, fs8.realpathSync(ancestor)))
21725
21807
  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` }] });
21726
21808
  } catch {
21727
21809
  return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
@@ -21733,7 +21815,7 @@ var scopeRoots = (root, scope) => {
21733
21815
  function captureCandidate(root, scope, environment = []) {
21734
21816
  let checkout;
21735
21817
  try {
21736
- checkout = fs7.realpathSync(root);
21818
+ checkout = fs8.realpathSync(root);
21737
21819
  } catch {
21738
21820
  return failure2("invalid_input", "candidate root does not exist");
21739
21821
  }
@@ -21751,11 +21833,11 @@ function captureCandidate(root, scope, environment = []) {
21751
21833
  if (item === ".git" || item === ".workit")
21752
21834
  continue;
21753
21835
  names.add(item);
21754
- if (!fs7.existsSync(target))
21836
+ if (!fs8.existsSync(target))
21755
21837
  continue;
21756
21838
  let stat;
21757
21839
  try {
21758
- stat = fs7.lstatSync(target);
21840
+ stat = fs8.lstatSync(target);
21759
21841
  } catch {
21760
21842
  uncertain = true;
21761
21843
  continue;
@@ -21773,15 +21855,15 @@ function captureCandidate(root, scope, environment = []) {
21773
21855
  continue;
21774
21856
  const target = path10.join(checkout, item);
21775
21857
  try {
21776
- const stat = fs7.lstatSync(target);
21858
+ const stat = fs8.lstatSync(target);
21777
21859
  if (stat.isSymbolicLink()) {
21778
- const link = fs7.readlinkSync(target);
21860
+ const link = fs8.readlinkSync(target);
21779
21861
  files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
21780
21862
  } else if (stat.isFile()) {
21781
21863
  files.push({
21782
21864
  path: item,
21783
21865
  kind: "file",
21784
- digest: digestBytes3(fs7.readFileSync(target)),
21866
+ digest: digestBytes3(fs8.readFileSync(target)),
21785
21867
  executable: (stat.mode & 73) !== 0
21786
21868
  });
21787
21869
  } else if (!stat.isDirectory()) {
@@ -21803,10 +21885,10 @@ function captureCandidate(root, scope, environment = []) {
21803
21885
  const namesByEnvironment = supplied.map(({ name }) => name);
21804
21886
  if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
21805
21887
  return failure2("invalid_input", "candidate environment names must be unique and non-empty");
21806
- const values = supplied.toSorted((left, right) => compareCodeUnits3(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
21888
+ const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
21807
21889
  if (values.some(({ value }) => value === null))
21808
21890
  completeness = "uncertain";
21809
- const headResult = spawnSync2("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
21891
+ const headResult = spawnSync3("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
21810
21892
  if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
21811
21893
  completeness = "uncertain";
21812
21894
  const head = headResult.status === 0 ? headResult.stdout.trim() : null;
@@ -21857,420 +21939,862 @@ function evaluateEvidence(task, candidate) {
21857
21939
  if (evidence.kind === "check" || evidence.kind === "review") {
21858
21940
  if (!evidence.beforeCandidateId || !evidence.candidateId)
21859
21941
  return {
21860
- evidenceId: entry.id,
21861
- status: "stale",
21862
- reason: "check or review evidence lacks start and end candidates"
21942
+ evidenceId: entry.id,
21943
+ status: "stale",
21944
+ reason: "check or review evidence lacks start and end candidates"
21945
+ };
21946
+ if (evidence.beforeCandidateId !== evidence.candidateId)
21947
+ return {
21948
+ evidenceId: entry.id,
21949
+ status: "stale",
21950
+ reason: "candidate changed during the check or review"
21951
+ };
21952
+ }
21953
+ if (!evidence.candidateId || !current)
21954
+ return {
21955
+ evidenceId: entry.id,
21956
+ status: evidence.result,
21957
+ reason: "evidence has no candidate binding"
21958
+ };
21959
+ const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
21960
+ if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
21961
+ return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
21962
+ if (observed.id === current.id)
21963
+ return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
21964
+ const scopes = evidenceScopes(task, evidence.requirementIds);
21965
+ const changed = changedPaths(observed, current);
21966
+ const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
21967
+ if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
21968
+ return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
21969
+ return {
21970
+ evidenceId: entry.id,
21971
+ status: evidence.result,
21972
+ reason: "candidate changed outside evidence scope"
21973
+ };
21974
+ });
21975
+ }
21976
+ var scopeCovers2 = scopeCovers;
21977
+ var checkPin = (assignment, beforeCandidateId, candidateId) => {
21978
+ const pin = assignment.candidateId ?? null;
21979
+ if (pin === null)
21980
+ return true;
21981
+ return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
21982
+ };
21983
+ var resolveBinding = (evidence, pin, currentCandidateId) => {
21984
+ const bound = evidence.kind === "check" || evidence.kind === "review";
21985
+ const fallback = bound ? pin ?? currentCandidateId : null;
21986
+ return {
21987
+ beforeCandidateId: evidence.beforeCandidateId ?? fallback,
21988
+ candidateId: evidence.candidateId ?? fallback
21989
+ };
21990
+ };
21991
+ var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId != null);
21992
+ var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
21993
+ var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
21994
+ var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
21995
+ if (kind !== "passed")
21996
+ return false;
21997
+ if (dimension === "testing" || dimension === "verification")
21998
+ return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
21999
+ if (dimension === "review")
22000
+ return evidenceKind === "review";
22001
+ if (dimension === "investigation" || dimension === "challenge")
22002
+ return evidenceKind === "investigation";
22003
+ if (dimension === "artifacts" || dimension === "continuity")
22004
+ return evidenceKind === "artifact";
22005
+ return false;
22006
+ };
22007
+ var expectedKinds = (dimension, ruleId) => {
22008
+ if (dimension === "testing")
22009
+ return "check";
22010
+ if (dimension === "verification")
22011
+ return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
22012
+ if (dimension === "review")
22013
+ return "review";
22014
+ if (dimension === "investigation" || dimension === "challenge")
22015
+ return "investigation";
22016
+ if (dimension === "artifacts" || dimension === "continuity")
22017
+ return "artifact";
22018
+ if (dimension === "decisions")
22019
+ return "decision";
22020
+ return "worker report";
22021
+ };
22022
+ function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
22023
+ const evidence = evaluateEvidence(task, candidate);
22024
+ if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
22025
+ return task.policy.requirements.map((requirement) => ({
22026
+ requirementId: requirement.id,
22027
+ status: "unsatisfied",
22028
+ evidenceIds: [],
22029
+ decisionIds: [],
22030
+ reason: "stored policy version is unsupported; reassessment is required"
22031
+ }));
22032
+ return (task.policy?.requirements ?? []).map((requirement) => {
22033
+ const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
22034
+ const passed = related.filter(({ entry, evaluation }) => {
22035
+ if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
22036
+ return false;
22037
+ if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
22038
+ return false;
22039
+ if (requirement.dimension !== "review")
22040
+ return true;
22041
+ const reviewSession = sessionFromRef(entry.data.reviewContext);
22042
+ const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
22043
+ const sameEvidenceSession = related.some(({ entry: other }) => other.id !== entry.id && other.data.kind === "review" && other.data.candidateId !== null && other.data.candidateId === entry.data.candidateId && sameSession(reviewSession, other.provenance.session));
22044
+ return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
22045
+ });
22046
+ if (passed.length) {
22047
+ const currentId = candidate?.id ?? null;
22048
+ const withRed = requirement.dimension === "testing" ? passed.filter(({ entry }) => related.some((other) => other.entry.data.kind === "check" && other.evaluation.status === "failed" && other.entry.data.requirementIds.includes(requirement.id) && other.entry.recordedAt <= entry.recordedAt) || related.some((baseline) => baseline.entry.id !== entry.id && baseline.entry.data.kind === "check" && baseline.entry.data.result === "passed" && baseline.entry.data.candidateId !== null && currentId !== null && baseline.entry.data.candidateId !== currentId)) : passed;
22049
+ if (withRed.length)
22050
+ return {
22051
+ requirementId: requirement.id,
22052
+ status: "satisfied",
22053
+ evidenceIds: withRed.map(({ entry }) => entry.id),
22054
+ decisionIds: [],
22055
+ reason: "fresh applicable evidence passed"
21863
22056
  };
21864
- if (evidence.beforeCandidateId !== evidence.candidateId)
22057
+ if (requirement.dimension === "testing")
21865
22058
  return {
21866
- evidenceId: entry.id,
21867
- status: "stale",
21868
- reason: "candidate changed during the check or review"
22059
+ requirementId: requirement.id,
22060
+ status: "unsatisfied",
22061
+ evidenceIds: [],
22062
+ decisionIds: [],
22063
+ reason: "testing requirement has GREEN evidence but no preceding RED"
21869
22064
  };
21870
22065
  }
21871
- if (!evidence.candidateId || !current)
22066
+ if (requirement.dimension === "delegation") {
22067
+ const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
22068
+ const reconciled = task.workers.some((worker) => worker.data.report?.outcome === "completed" && (worker.data.assignment.requirementIds.includes(requirement.id) || scopeCovers2(dot(requirement.scope), dot(worker.data.assignment.scope))));
22069
+ if (reconciled)
22070
+ return {
22071
+ requirementId: requirement.id,
22072
+ status: "satisfied",
22073
+ evidenceIds: [],
22074
+ decisionIds: [],
22075
+ reason: "bounded helper completed and its result is recorded"
22076
+ };
22077
+ }
22078
+ const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
22079
+ if (decisions.length)
21872
22080
  return {
21873
- evidenceId: entry.id,
21874
- status: evidence.result,
21875
- reason: "evidence has no candidate binding"
22081
+ requirementId: requirement.id,
22082
+ status: "satisfied",
22083
+ evidenceIds: [],
22084
+ decisionIds: decisions.map((decision) => decision.id),
22085
+ reason: "an applicable approved or stated decision satisfies the requirement"
21876
22086
  };
21877
- const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
21878
- if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
21879
- return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
21880
- if (observed.id === current.id)
21881
- return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
21882
- const scopes = evidenceScopes(task, evidence.requirementIds);
21883
- const changed = changedPaths(observed, current);
21884
- const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
21885
- if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
21886
- return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
22087
+ const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
22088
+ if (limitations.length)
22089
+ return {
22090
+ requirementId: requirement.id,
22091
+ status: "accepted_limitation",
22092
+ evidenceIds: [],
22093
+ decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
22094
+ reason: "an applicable approved limitation permits the missing evidence"
22095
+ };
22096
+ const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
22097
+ if (!unavailable && related.length > 0 && !passed.length)
22098
+ return {
22099
+ requirementId: requirement.id,
22100
+ status: "unsatisfied",
22101
+ evidenceIds: related.map(({ entry }) => entry.id),
22102
+ decisionIds: [],
22103
+ reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
22104
+ };
22105
+ return {
22106
+ requirementId: requirement.id,
22107
+ status: unavailable ? "unavailable" : "unsatisfied",
22108
+ evidenceIds: related.map(({ entry }) => entry.id),
22109
+ decisionIds: [],
22110
+ reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
22111
+ };
22112
+ });
22113
+ }
22114
+ function evaluateClosure(requestedOutcome, view) {
22115
+ if (requestedOutcome !== "stopped" && !view.task.policy)
22116
+ return failure2("requirements_unsatisfied", "task has not been assessed");
22117
+ if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
22118
+ return failure2("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
22119
+ const evaluations = view.requirements;
22120
+ const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
22121
+ if (requestedOutcome !== "stopped" && openFindings.length)
22122
+ return failure2("requirements_unsatisfied", "open findings must be resolved before closure");
22123
+ if (requestedOutcome !== "stopped") {
22124
+ const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
22125
+ for (const entry of view.task.findings) {
22126
+ if (entry.data.disposition === "fixed") {
22127
+ const verified = entry.data.resolution?.evidenceIds.some((id) => {
22128
+ const evidence = view.task.evidence.find((item) => item.id === id);
22129
+ if (!evidence)
22130
+ return false;
22131
+ return findingVerificationPasses(entry.data.candidateId, {
22132
+ kind: evidence.data.kind,
22133
+ candidateId: evidence.data.candidateId,
22134
+ status: evidenceById.get(id) ?? "missing"
22135
+ });
22136
+ });
22137
+ if (!verified)
22138
+ return failure2("requirements_unsatisfied", "fixed findings require current verification");
22139
+ }
22140
+ if (entry.data.disposition === "deferred") {
22141
+ const valid = entry.data.resolution?.decisionIds.some((id) => {
22142
+ const decisionEntry = view.task.decisions.find((item) => item.id === id);
22143
+ const decision = decisionEntry?.data;
22144
+ return Boolean(decision && decisionEntry?.provenance.kind !== "imported" && decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.digest === decisionDigest(decision) && verifyDecisionContentAtRoot(view.workspace.root, decision.binding).ok && decision.binding.taskId === view.task.id && decision.binding.workspaceId === view.workspace.id && scopeCovers2(decision.binding.scope, entry.data.scope) && decision.requirementIds.some((requirementId) => view.task.policy?.requirements.some((requirement) => requirement.id === requirementId && requirement.acceptanceAllowed)));
22145
+ });
22146
+ if (!valid)
22147
+ return failure2("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
22148
+ }
22149
+ }
22150
+ }
22151
+ const blocking = evaluations.filter((item) => {
22152
+ if (item.status !== "unsatisfied" && item.status !== "unavailable")
22153
+ return false;
22154
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
22155
+ return !requirement || requirement.before === "close";
22156
+ });
22157
+ if (requestedOutcome !== "stopped" && blocking.length)
22158
+ return failure2("requirements_unsatisfied", "applicable requirements are unsatisfied", {
22159
+ requirementIds: blocking.map((item) => item.requirementId),
22160
+ requirements: blocking.map((item) => {
22161
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
22162
+ return {
22163
+ requirementId: item.requirementId,
22164
+ ruleId: requirement?.ruleId ?? "unknown",
22165
+ reason: item.reason,
22166
+ satisfaction: requirement?.satisfaction ?? "",
22167
+ dependentAction: requirement?.dependentAction ?? null
22168
+ };
22169
+ })
22170
+ });
22171
+ const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
22172
+ if (requestedOutcome === "verified" && accepted.length)
22173
+ return failure2("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
22174
+ requirementIds: accepted.map((item) => item.requirementId)
22175
+ });
22176
+ const evidenceIds = [
22177
+ ...new Set([
22178
+ ...evaluations.flatMap((item) => item.evidenceIds),
22179
+ ...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
22180
+ ])
22181
+ ];
22182
+ const decisionIds = [
22183
+ ...new Set([
22184
+ ...evaluations.flatMap((item) => item.decisionIds),
22185
+ ...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
22186
+ ])
22187
+ ];
22188
+ return success(null, null, {
22189
+ outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
22190
+ evidenceIds,
22191
+ decisionIds,
22192
+ requirementIds: evaluations.map((item) => item.requirementId)
22193
+ });
22194
+ }
22195
+
22196
+ // packages/workit-core/src/core/methods.ts
22197
+ var METHODS = {
22198
+ "workit-challenge": { dimensions: ["challenge", "decisions"] },
22199
+ "workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
22200
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
22201
+ "workit-plan": { dimensions: ["artifacts", "continuity"] },
22202
+ "workit-implement": { dimensions: ["delegation"] },
22203
+ "workit-debug": {},
22204
+ "workit-handoff": {},
22205
+ "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
22206
+ };
22207
+ var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
22208
+ var assuranceFor = (id, definition, capabilities) => {
22209
+ const keys = new Set([
22210
+ id,
22211
+ ...definition.dimensions ?? [],
22212
+ ...definition.ruleIds ?? []
22213
+ ]);
22214
+ const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
22215
+ if (id === "workit-review" && matches.length === 0)
22216
+ return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
22217
+ if (matches.some((capability) => capability.assurance === "unavailable"))
22218
+ return {
22219
+ assurance: "unavailable",
22220
+ reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
22221
+ };
22222
+ if (matches.some((capability) => capability.assurance === "enforced"))
22223
+ return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
22224
+ return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
22225
+ };
22226
+ function selectMethods(policy, capabilities) {
22227
+ return Object.entries(METHODS).flatMap(([id, definition]) => {
22228
+ const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
22229
+ if (requirements.length === 0)
22230
+ return [];
22231
+ const capability = assuranceFor(id, definition, capabilities);
22232
+ const reasons = [
22233
+ ...new Set(requirements.map((requirement) => requirement.reason)),
22234
+ ...capability.reasons
22235
+ ];
22236
+ if (capability.assurance === "unavailable")
22237
+ reasons.unshift("required capability is unavailable");
22238
+ return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
22239
+ });
22240
+ }
22241
+
22242
+ // packages/workit-core/src/core/task-context.ts
22243
+ function reconcileResume(view, observations = []) {
22244
+ if (observations.length > 0)
22245
+ return failure2("permission_denied", "worker observations require native host verification");
22246
+ const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
22247
+ if (!candidate.ok)
22248
+ return candidate;
22249
+ const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
22250
+ const blockers = [...view.task.progress.blockers];
22251
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
22252
+ blockers.push({
22253
+ reason: "worker state requires reconciliation",
22254
+ dependentAction: "resume",
22255
+ refs: []
22256
+ });
22257
+ return success(null, null, {
22258
+ candidate: candidate.data,
22259
+ staleEvidenceIds,
22260
+ workerUpdates: [],
22261
+ blockers,
22262
+ reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
22263
+ });
22264
+ }
22265
+ var COMPACT_MAX_BYTES = 4096;
22266
+ var COMPACT_MAX_ITEMS = 8;
22267
+ var COMPACT_TEXT_BYTES = 128;
22268
+ var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
22269
+ var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
22270
+ var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
22271
+ var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
22272
+ const separator = match.search(/[:=]/u);
22273
+ if (separator >= 0)
22274
+ return `${match.slice(0, separator)}=<redacted>`;
22275
+ const space = match.search(/\s/u);
22276
+ return `${match.slice(0, space)} <redacted>`;
22277
+ }).slice(0, limit);
22278
+ var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
22279
+ var compactTimestamp = (value) => `${value.slice(0, 19)}.${value.slice(20, -1).padEnd(9, "0")}`;
22280
+ var compactDecisionChoice = (entry) => {
22281
+ const stated = entry.data.binding.statedChoice?.text;
22282
+ const approved = entry.data.response === "approved" ? entry.data.binding.approvedContent : "";
22283
+ const source = stated?.trim() || approved.trim() || null;
22284
+ return compactText(source, COMPACT_TEXT_BYTES);
22285
+ };
22286
+ var compactReference = (ref) => {
22287
+ if (ref.kind === "file")
21887
22288
  return {
21888
- evidenceId: entry.id,
21889
- status: evidence.result,
21890
- reason: "candidate changed outside evidence scope"
22289
+ kind: "file",
22290
+ path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
21891
22291
  };
21892
- });
21893
- }
21894
- var scopeCovers2 = scopeCovers;
21895
- var checkPin = (assignment, beforeCandidateId, candidateId) => {
21896
- const pin = assignment.candidateId ?? null;
21897
- if (pin === null)
21898
- return true;
21899
- return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
22292
+ if (ref.kind === "record")
22293
+ return {
22294
+ kind: "record",
22295
+ collection: ref.collection,
22296
+ id: ref.id
22297
+ };
22298
+ return { kind: ref.kind };
21900
22299
  };
21901
- var resolveBinding = (evidence, pin, currentCandidateId) => {
21902
- const bound = evidence.kind === "check" || evidence.kind === "review";
21903
- const fallback = bound ? pin ?? currentCandidateId : null;
21904
- return {
21905
- beforeCandidateId: evidence.beforeCandidateId ?? fallback,
21906
- candidateId: evidence.candidateId ?? fallback
22300
+ function compactTaskContext(view) {
22301
+ const decisions = view.task.decisions.slice().toSorted((left, right) => {
22302
+ const leftAt = compactTimestamp(left.recordedAt);
22303
+ const rightAt = compactTimestamp(right.recordedAt);
22304
+ if (leftAt !== rightAt)
22305
+ return leftAt > rightAt ? -1 : 1;
22306
+ return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
22307
+ }).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
22308
+ id: entry.id,
22309
+ purpose: entry.data.purpose,
22310
+ status: entry.data.response,
22311
+ digest: entry.data.digest,
22312
+ choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
22313
+ references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
22314
+ }));
22315
+ const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
22316
+ id: method.id,
22317
+ assurance: method.assurance,
22318
+ reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
22319
+ })) : [];
22320
+ const gaps = Array.from(new Set([
22321
+ ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
22322
+ ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
22323
+ ...view.task.progress.blockers.map((entry) => entry.reason)
22324
+ ])).toSorted().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
22325
+ const context = {
22326
+ objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
22327
+ status: view.task.status,
22328
+ decisions,
22329
+ methods,
22330
+ gaps,
22331
+ nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
21907
22332
  };
22333
+ let encoded = JSON.stringify(context);
22334
+ while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
22335
+ if (context.gaps.length)
22336
+ context.gaps.pop();
22337
+ else if (context.decisions.length)
22338
+ context.decisions.pop();
22339
+ else
22340
+ context.methods.pop();
22341
+ encoded = JSON.stringify(context);
22342
+ }
22343
+ if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
22344
+ context.objective = compactText(context.objective, 64) ?? "";
22345
+ context.nextAction = compactText(context.nextAction, 64);
22346
+ encoded = JSON.stringify(context);
22347
+ }
22348
+ return encoded;
22349
+ }
22350
+ var exportDigest = (bundle) => sha256({
22351
+ schemaVersion: bundle.schemaVersion,
22352
+ exportedAt: bundle.exportedAt,
22353
+ sourceWorkspaceId: bundle.sourceWorkspaceId,
22354
+ task: bundle.task
22355
+ });
22356
+ // packages/workit-core/src/core/external-action.ts
22357
+ var chainStepSchema = union([
22358
+ string2().min(1),
22359
+ object2({ branch: string2().min(1) }).strict(),
22360
+ object2({ pr: literal(true) }).strict()
22361
+ ]);
22362
+ var externalActionSchema = discriminatedUnion("operation", [
22363
+ object2({
22364
+ operation: literal("git.branch_setup"),
22365
+ payload: object2({
22366
+ action: _enum(["setup", "reapply_stash"]).optional(),
22367
+ sdd_dir: string2().optional(),
22368
+ target_branch: string2().optional(),
22369
+ stash: _enum(["yes", "no"]).optional(),
22370
+ cwd: string2().min(1).optional()
22371
+ }).strict()
22372
+ }).strict(),
22373
+ object2({
22374
+ operation: literal("git.commit"),
22375
+ payload: object2({
22376
+ message: string2().min(1).optional(),
22377
+ plan_steps: array(chainStepSchema).min(1).max(32).optional(),
22378
+ plan_branch: string2().min(1).optional(),
22379
+ cwd: string2().min(1).optional()
22380
+ }).strict()
22381
+ }).strict(),
22382
+ object2({
22383
+ operation: literal("git.push"),
22384
+ payload: object2({ branch: string2().min(1).optional(), cwd: string2().min(1).optional() }).strict()
22385
+ }).strict(),
22386
+ object2({
22387
+ operation: literal("hosting.pull_request"),
22388
+ payload: object2({
22389
+ title: string2().min(1),
22390
+ body: string2().optional(),
22391
+ draft: boolean2().optional(),
22392
+ target_branch: string2().optional(),
22393
+ babysit: boolean2().optional(),
22394
+ cwd: string2().min(1).optional()
22395
+ }).strict()
22396
+ }).strict(),
22397
+ object2({
22398
+ operation: literal("hosting.merge"),
22399
+ payload: object2({
22400
+ target_branch: string2().min(1).optional(),
22401
+ source_branch: string2().min(1).optional(),
22402
+ cwd: string2().min(1).optional()
22403
+ }).strict()
22404
+ }).strict(),
22405
+ object2({
22406
+ operation: literal("hosting.delete_branch"),
22407
+ payload: object2({ branch: string2().min(1), cwd: string2().min(1).optional() }).strict()
22408
+ }).strict(),
22409
+ object2({
22410
+ operation: literal("youtrack.update"),
22411
+ payload: object2({
22412
+ issueId: string2().min(1),
22413
+ markdown: string2().min(1),
22414
+ minutes: number2().positive().optional()
22415
+ }).strict()
22416
+ }).strict(),
22417
+ object2({
22418
+ operation: literal("youtrack.time"),
22419
+ payload: object2({
22420
+ issueId: string2().min(1),
22421
+ minutes: number2().positive(),
22422
+ text: string2().optional(),
22423
+ dateMs: number2().finite().optional()
22424
+ }).strict()
22425
+ }).strict(),
22426
+ object2({
22427
+ operation: literal("youtrack.meeting"),
22428
+ payload: object2({
22429
+ issueId: string2().min(1),
22430
+ minutes: number2().positive(),
22431
+ text: string2().min(1)
22432
+ }).strict()
22433
+ }).strict(),
22434
+ object2({
22435
+ operation: literal("changelog.apply"),
22436
+ payload: object2({
22437
+ entries: union([
22438
+ record(string2(), array(string2())),
22439
+ array(object2({ category: string2().min(1), text: string2().min(1) }).strict())
22440
+ ]).optional(),
22441
+ path: string2().optional(),
22442
+ normalize_only: boolean2().optional()
22443
+ }).strict()
22444
+ }).strict(),
22445
+ object2({
22446
+ operation: literal("context.read"),
22447
+ payload: object2({
22448
+ kind: _enum([
22449
+ "git",
22450
+ "pr",
22451
+ "youtrack",
22452
+ "github_issue",
22453
+ "gitlab_issue",
22454
+ "changelog",
22455
+ "release",
22456
+ "affected"
22457
+ ]),
22458
+ range: string2().optional(),
22459
+ issueId: string2().optional(),
22460
+ issueUrl: string2().optional(),
22461
+ issueRef: string2().optional(),
22462
+ mode: string2().optional(),
22463
+ specPath: string2().optional(),
22464
+ planPath: string2().optional(),
22465
+ cwd: string2().min(1).optional()
22466
+ }).strict()
22467
+ }).strict()
22468
+ ]);
22469
+ // packages/workit-core/src/core/task-engine.ts
22470
+ import { realpathSync as realpathSync6 } from "node:fs";
22471
+
22472
+ // packages/workit-core/src/core/policy-resolver.ts
22473
+ var resolverInputSchema = object2({
22474
+ intent: intentSchema,
22475
+ assessment: assessmentSchema,
22476
+ constraints: array(constraintSchema),
22477
+ prior: object2({
22478
+ decisions: array(decisionSchema),
22479
+ findings: array(findingSchema),
22480
+ requirements: array(requirementSchema)
22481
+ }).strict(),
22482
+ preferences: object2({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
22483
+ }).strict();
22484
+ var compareCodeUnits3 = (left, right) => {
22485
+ const length = Math.min(left.length, right.length);
22486
+ for (let index = 0;index < length; index += 1) {
22487
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
22488
+ if (difference !== 0)
22489
+ return difference;
22490
+ }
22491
+ return left.length - right.length;
21908
22492
  };
21909
- var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId != null);
21910
- var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
21911
- var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
21912
- var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
21913
- if (kind !== "passed")
21914
- return false;
21915
- if (dimension === "testing" || dimension === "verification")
21916
- return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
21917
- if (dimension === "review")
21918
- return evidenceKind === "review";
21919
- if (dimension === "investigation" || dimension === "challenge")
21920
- return evidenceKind === "investigation";
21921
- if (dimension === "artifacts" || dimension === "continuity")
21922
- return evidenceKind === "artifact";
21923
- return false;
21924
- };
21925
- var expectedKinds = (dimension, ruleId) => {
21926
- if (dimension === "testing")
21927
- return "check";
21928
- if (dimension === "verification")
21929
- return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
21930
- if (dimension === "review")
21931
- return "review";
21932
- if (dimension === "investigation" || dimension === "challenge")
21933
- return "investigation";
21934
- if (dimension === "artifacts" || dimension === "continuity")
21935
- return "artifact";
21936
- if (dimension === "decisions")
21937
- return "decision";
21938
- return "worker report";
21939
- };
21940
- function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
21941
- const evidence = evaluateEvidence(task, candidate);
21942
- if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
21943
- return task.policy.requirements.map((requirement) => ({
21944
- requirementId: requirement.id,
21945
- status: "unsatisfied",
21946
- evidenceIds: [],
21947
- decisionIds: [],
21948
- reason: "stored policy version is unsupported; reassessment is required"
21949
- }));
21950
- return (task.policy?.requirements ?? []).map((requirement) => {
21951
- const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
21952
- const passed = related.filter(({ entry, evaluation }) => {
21953
- if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
21954
- return false;
21955
- if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
21956
- return false;
21957
- if (requirement.dimension !== "review")
21958
- return true;
21959
- const reviewSession = sessionFromRef(entry.data.reviewContext);
21960
- const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
21961
- const sameEvidenceSession = related.some(({ entry: other }) => other.id !== entry.id && other.data.kind === "review" && other.data.candidateId !== null && other.data.candidateId === entry.data.candidateId && sameSession(reviewSession, other.provenance.session));
21962
- return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
22493
+ var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits3);
22494
+ var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits3(canonicalJson(left), canonicalJson(right)));
22495
+ var normalizedScope = (scope) => ({
22496
+ description: scope.description,
22497
+ paths: stableUnique(scope.paths),
22498
+ exclusions: stableUnique(scope.exclusions)
22499
+ });
22500
+ var normalizedRefList = (values) => stableList(values);
22501
+ var normalizeAssessment = (assessment) => ({
22502
+ facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
22503
+ signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
22504
+ name,
22505
+ { ...signal, refs: normalizedRefList(signal.refs) }
22506
+ ])),
22507
+ consequences: stableList(assessment.consequences.map((consequence) => ({
22508
+ ...consequence,
22509
+ fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
22510
+ }))),
22511
+ verification: stableList(assessment.verification.map((verification) => ({
22512
+ ...verification,
22513
+ scope: normalizedScope(verification.scope),
22514
+ availableChecks: normalizedRefList(verification.availableChecks),
22515
+ gaps: stableUnique(verification.gaps)
22516
+ })))
22517
+ });
22518
+ var normalizeConstraint = (constraint) => ({
22519
+ ...constraint,
22520
+ source: constraint.source,
22521
+ requires: stableList(constraint.requires.map((requirement) => ({
22522
+ ...requirement,
22523
+ scope: normalizedScope(requirement.scope),
22524
+ refs: normalizedRefList(requirement.refs)
22525
+ })))
22526
+ });
22527
+ var normalizeDecision = (decision) => ({
22528
+ ...decision,
22529
+ binding: {
22530
+ ...decision.binding,
22531
+ scope: normalizedScope(decision.binding.scope),
22532
+ contentRefs: normalizedRefList(decision.binding.contentRefs)
22533
+ },
22534
+ requirementIds: stableUnique(decision.requirementIds)
22535
+ });
22536
+ var normalizeIntent = (intent) => ({
22537
+ ...intent,
22538
+ scope: normalizedScope(intent.scope),
22539
+ authorityRefs: normalizedRefList(intent.authorityRefs)
22540
+ });
22541
+ var normalizePrior = (prior) => ({
22542
+ decisions: stableList(prior.decisions.map(normalizeDecision)),
22543
+ findings: stableList(prior.findings.map((finding) => ({
22544
+ ...finding,
22545
+ scope: normalizedScope(finding.scope),
22546
+ refs: normalizedRefList(finding.refs),
22547
+ resolution: finding.resolution ? {
22548
+ ...finding.resolution,
22549
+ evidenceIds: stableUnique(finding.resolution.evidenceIds),
22550
+ decisionIds: stableUnique(finding.resolution.decisionIds)
22551
+ } : null
22552
+ }))),
22553
+ requirements: stableList(prior.requirements.map((requirement) => ({
22554
+ ...requirement,
22555
+ scope: normalizedScope(requirement.scope)
22556
+ })))
22557
+ });
22558
+ var invalidFields = (error) => ({
22559
+ fields: error.issues.map((issue) => ({
22560
+ path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
22561
+ reason: issue.message
22562
+ }))
22563
+ });
22564
+ function normalizeResolverInput(input) {
22565
+ const parsed = resolverInputSchema.safeParse(input);
22566
+ if (!parsed.success)
22567
+ return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
22568
+ const value = parsed.data;
22569
+ const signals = value.assessment.signals;
22570
+ if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
22571
+ return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
22572
+ fields: [
22573
+ {
22574
+ path: "assessment.signals",
22575
+ reason: "contradictory behaviorChange and mechanicalLowRisk"
22576
+ }
22577
+ ]
21963
22578
  });
21964
- if (passed.length) {
21965
- const currentId = candidate?.id ?? null;
21966
- const withRed = requirement.dimension === "testing" ? passed.filter(({ entry }) => related.some((other) => other.entry.data.kind === "check" && other.evaluation.status === "failed" && other.entry.data.requirementIds.includes(requirement.id) && other.entry.recordedAt <= entry.recordedAt) || related.some((baseline) => baseline.entry.id !== entry.id && baseline.entry.data.kind === "check" && baseline.entry.data.result === "passed" && baseline.entry.data.candidateId !== null && currentId !== null && baseline.entry.data.candidateId !== currentId)) : passed;
21967
- if (withRed.length)
21968
- return {
21969
- requirementId: requirement.id,
21970
- status: "satisfied",
21971
- evidenceIds: withRed.map(({ entry }) => entry.id),
21972
- decisionIds: [],
21973
- reason: "fresh applicable evidence passed"
21974
- };
21975
- if (requirement.dimension === "testing")
21976
- return {
21977
- requirementId: requirement.id,
21978
- status: "unsatisfied",
21979
- evidenceIds: [],
21980
- decisionIds: [],
21981
- reason: "testing requirement has GREEN evidence but no preceding RED"
21982
- };
21983
- }
21984
- if (requirement.dimension === "delegation") {
21985
- const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
21986
- const reconciled = task.workers.some((worker) => worker.data.report?.outcome === "completed" && (worker.data.assignment.requirementIds.includes(requirement.id) || scopeCovers2(dot(requirement.scope), dot(worker.data.assignment.scope))));
21987
- if (reconciled)
21988
- return {
21989
- requirementId: requirement.id,
21990
- status: "satisfied",
21991
- evidenceIds: [],
21992
- decisionIds: [],
21993
- reason: "bounded helper completed and its result is recorded"
21994
- };
21995
- }
21996
- const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
21997
- if (decisions.length)
21998
- return {
21999
- requirementId: requirement.id,
22000
- status: "satisfied",
22001
- evidenceIds: [],
22002
- decisionIds: decisions.map((decision) => decision.id),
22003
- reason: "an applicable approved or stated decision satisfies the requirement"
22004
- };
22005
- const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
22006
- if (limitations.length)
22007
- return {
22008
- requirementId: requirement.id,
22009
- status: "accepted_limitation",
22010
- evidenceIds: [],
22011
- decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
22012
- reason: "an applicable approved limitation permits the missing evidence"
22013
- };
22014
- const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
22015
- if (!unavailable && related.length > 0 && !passed.length)
22016
- return {
22017
- requirementId: requirement.id,
22018
- status: "unsatisfied",
22019
- evidenceIds: related.map(({ entry }) => entry.id),
22020
- decisionIds: [],
22021
- reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
22022
- };
22023
- return {
22024
- requirementId: requirement.id,
22025
- status: unavailable ? "unavailable" : "unsatisfied",
22026
- evidenceIds: related.map(({ entry }) => entry.id),
22027
- decisionIds: [],
22028
- reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
22029
- };
22030
- });
22579
+ const ids = value.constraints.map((constraint) => constraint.id);
22580
+ if (new Set(ids).size !== ids.length)
22581
+ return failure2("invalid_input", "constraint IDs must be unique", {
22582
+ fields: [{ path: "constraints", reason: "duplicate constraint id" }]
22583
+ });
22584
+ if (value.preferences?.fast && value.preferences.thorough)
22585
+ return failure2("invalid_input", "fast and thorough preferences contradict", {
22586
+ fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
22587
+ });
22588
+ const normalized = {
22589
+ intent: normalizeIntent(value.intent),
22590
+ assessment: normalizeAssessment(value.assessment),
22591
+ constraints: stableList(value.constraints.map(normalizeConstraint)),
22592
+ prior: normalizePrior(value.prior),
22593
+ ...value.preferences ? { preferences: { ...value.preferences } } : {},
22594
+ policyVersion: POLICY_VERSION
22595
+ };
22596
+ return success(null, null, normalized);
22031
22597
  }
22032
- function evaluateClosure(requestedOutcome, view) {
22033
- if (requestedOutcome !== "stopped" && !view.task.policy)
22034
- return failure2("requirements_unsatisfied", "task has not been assessed");
22035
- if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
22036
- return failure2("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
22037
- const evaluations = view.requirements;
22038
- const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
22039
- if (requestedOutcome !== "stopped" && openFindings.length)
22040
- return failure2("requirements_unsatisfied", "open findings must be resolved before closure");
22041
- if (requestedOutcome !== "stopped") {
22042
- const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
22043
- for (const entry of view.task.findings) {
22044
- if (entry.data.disposition === "fixed") {
22045
- const verified = entry.data.resolution?.evidenceIds.some((id) => {
22046
- const evidence = view.task.evidence.find((item) => item.id === id);
22047
- if (!evidence)
22048
- return false;
22049
- return findingVerificationPasses(entry.data.candidateId, {
22050
- kind: evidence.data.kind,
22051
- candidateId: evidence.data.candidateId,
22052
- status: evidenceById.get(id) ?? "missing"
22053
- });
22054
- });
22055
- if (!verified)
22056
- return failure2("requirements_unsatisfied", "fixed findings require current verification");
22057
- }
22058
- if (entry.data.disposition === "deferred") {
22059
- const valid = entry.data.resolution?.decisionIds.some((id) => {
22060
- const decisionEntry = view.task.decisions.find((item) => item.id === id);
22061
- const decision = decisionEntry?.data;
22062
- return Boolean(decision && decisionEntry?.provenance.kind !== "imported" && decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.digest === decisionDigest(decision) && verifyDecisionContentAtRoot(view.workspace.root, decision.binding).ok && decision.binding.taskId === view.task.id && decision.binding.workspaceId === view.workspace.id && scopeCovers2(decision.binding.scope, entry.data.scope) && decision.requirementIds.some((requirementId) => view.task.policy?.requirements.some((requirement) => requirement.id === requirementId && requirement.acceptanceAllowed)));
22063
- });
22064
- if (!valid)
22065
- return failure2("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
22066
- }
22598
+ var requirement = (spec) => ({
22599
+ ...spec,
22600
+ id: requirementId({
22601
+ ruleId: spec.ruleId,
22602
+ scope: spec.scope,
22603
+ satisfaction: spec.satisfaction,
22604
+ before: spec.before,
22605
+ dependentAction: spec.dependentAction
22606
+ })
22607
+ });
22608
+ var signalReason = (input, name) => input.assessment.signals[name].reason;
22609
+ function resolveRequirements(input) {
22610
+ const { signals } = input.assessment;
22611
+ const scope = input.intent.scope;
22612
+ const requirements = [];
22613
+ const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
22614
+ const hasUnknownFact = [
22615
+ ...input.assessment.facts,
22616
+ ...input.assessment.consequences.map(({ fact }) => fact)
22617
+ ].some((fact) => fact.basis === "unknown");
22618
+ if (hasUnknownSignal || hasUnknownFact)
22619
+ requirements.push(requirement({
22620
+ ruleId: "consequential-unknown",
22621
+ dimension: "investigation",
22622
+ scope,
22623
+ reason: `Resolve consequential uncertainty before the dependent action: ${[
22624
+ ...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
22625
+ ...hasUnknownFact ? ["unknown fact"] : []
22626
+ ].join(", ")}`,
22627
+ satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
22628
+ before: "dependent_action",
22629
+ dependentAction: null,
22630
+ acceptanceAllowed: false
22631
+ }));
22632
+ if (signals.productChoiceOpen.value === true)
22633
+ requirements.push(requirement({
22634
+ ruleId: "product-decision",
22635
+ dimension: "decisions",
22636
+ scope,
22637
+ reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
22638
+ satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
22639
+ before: "dependent_action",
22640
+ dependentAction: null,
22641
+ acceptanceAllowed: false
22642
+ }));
22643
+ const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
22644
+ if (signals.behaviorChange.value === true)
22645
+ requirements.push(requirement({
22646
+ ruleId: "behavioral-verification",
22647
+ dimension: "testing",
22648
+ scope,
22649
+ reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
22650
+ satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
22651
+ before: "close",
22652
+ dependentAction: null,
22653
+ acceptanceAllowed: false
22654
+ }), requirement({
22655
+ ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
22656
+ dimension: "review",
22657
+ scope,
22658
+ 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.",
22659
+ 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.",
22660
+ before: "close",
22661
+ dependentAction: null,
22662
+ acceptanceAllowed: !independentBehaviorReview
22663
+ }));
22664
+ else if (signals.mechanicalLowRisk.value === true)
22665
+ requirements.push(requirement({
22666
+ ruleId: "mechanical-existing-checks",
22667
+ dimension: "verification",
22668
+ scope,
22669
+ reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
22670
+ satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
22671
+ before: "close",
22672
+ dependentAction: null,
22673
+ acceptanceAllowed: true
22674
+ }), requirement({
22675
+ ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
22676
+ dimension: "review",
22677
+ scope,
22678
+ 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.",
22679
+ 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.",
22680
+ before: "close",
22681
+ dependentAction: null,
22682
+ acceptanceAllowed: !input.preferences?.thorough
22683
+ }));
22684
+ if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
22685
+ requirements.push(requirement({
22686
+ ruleId: "pre-pr-cleanup",
22687
+ dimension: "verification",
22688
+ scope,
22689
+ reason: "Implementation output must be deslopped before it is delivered.",
22690
+ satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
22691
+ before: "dependent_action",
22692
+ dependentAction: "hosting.pull_request",
22693
+ acceptanceAllowed: true
22694
+ }));
22695
+ if (signals.durableAgreementNeeded.value === true)
22696
+ requirements.push(requirement({
22697
+ ruleId: "durable-spec",
22698
+ dimension: "artifacts",
22699
+ scope,
22700
+ reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
22701
+ satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
22702
+ before: "write",
22703
+ dependentAction: null,
22704
+ acceptanceAllowed: true
22705
+ }));
22706
+ if (signals.coordinationPlanNeeded.value === true)
22707
+ requirements.push(requirement({
22708
+ ruleId: "coordination-plan",
22709
+ dimension: "continuity",
22710
+ scope,
22711
+ reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
22712
+ satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
22713
+ before: "dependent_action",
22714
+ dependentAction: null,
22715
+ acceptanceAllowed: true
22716
+ }));
22717
+ if (signals.helperUseful.value === true && !input.preferences?.fast)
22718
+ requirements.push(requirement({
22719
+ ruleId: "helper-usefulness",
22720
+ dimension: "delegation",
22721
+ scope,
22722
+ reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
22723
+ satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
22724
+ before: "dependent_action",
22725
+ dependentAction: null,
22726
+ acceptanceAllowed: true
22727
+ }));
22728
+ for (const constraint of input.constraints)
22729
+ for (const obligation of constraint.requires) {
22730
+ const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
22731
+ requirements.push(requirement({
22732
+ ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
22733
+ kind: obligation.kind,
22734
+ scope: normalizedScope(obligation.scope),
22735
+ refs: normalizedRefList(obligation.refs),
22736
+ method: obligation.method,
22737
+ before: obligation.before,
22738
+ dependentAction: obligation.dependentAction
22739
+ })}`,
22740
+ dimension,
22741
+ scope: obligation.scope,
22742
+ reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
22743
+ satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
22744
+ before: obligation.before,
22745
+ dependentAction: obligation.dependentAction,
22746
+ acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
22747
+ }));
22067
22748
  }
22068
- }
22069
- const blocking = evaluations.filter((item) => {
22070
- if (item.status !== "unsatisfied" && item.status !== "unavailable")
22749
+ const seen = new Set;
22750
+ return requirements.filter((item) => {
22751
+ if (seen.has(item.id))
22071
22752
  return false;
22072
- const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
22073
- return !requirement || requirement.before === "close";
22074
- });
22075
- if (requestedOutcome !== "stopped" && blocking.length)
22076
- return failure2("requirements_unsatisfied", "applicable requirements are unsatisfied", {
22077
- requirementIds: blocking.map((item) => item.requirementId),
22078
- requirements: blocking.map((item) => {
22079
- const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
22080
- return {
22081
- requirementId: item.requirementId,
22082
- ruleId: requirement?.ruleId ?? "unknown",
22083
- reason: item.reason,
22084
- satisfaction: requirement?.satisfaction ?? "",
22085
- dependentAction: requirement?.dependentAction ?? null
22086
- };
22087
- })
22088
- });
22089
- const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
22090
- if (requestedOutcome === "verified" && accepted.length)
22091
- return failure2("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
22092
- requirementIds: accepted.map((item) => item.requirementId)
22093
- });
22094
- const evidenceIds = [
22095
- ...new Set([
22096
- ...evaluations.flatMap((item) => item.evidenceIds),
22097
- ...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
22098
- ])
22099
- ];
22100
- const decisionIds = [
22101
- ...new Set([
22102
- ...evaluations.flatMap((item) => item.decisionIds),
22103
- ...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
22104
- ])
22105
- ];
22106
- return success(null, null, {
22107
- outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
22108
- evidenceIds,
22109
- decisionIds,
22110
- requirementIds: evaluations.map((item) => item.requirementId)
22111
- });
22112
- }
22113
-
22114
- // packages/workit-core/src/core/methods.ts
22115
- var METHODS = {
22116
- "workit-challenge": { dimensions: ["challenge", "decisions"] },
22117
- "workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
22118
- "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
22119
- "workit-plan": { dimensions: ["artifacts", "continuity"] },
22120
- "workit-implement": { dimensions: ["delegation"] },
22121
- "workit-debug": {},
22122
- "workit-handoff": {},
22123
- "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
22124
- };
22125
- var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
22126
- var assuranceFor = (id, definition, capabilities) => {
22127
- const keys = new Set([
22128
- id,
22129
- ...definition.dimensions ?? [],
22130
- ...definition.ruleIds ?? []
22131
- ]);
22132
- const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
22133
- if (id === "workit-review" && matches.length === 0)
22134
- return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
22135
- if (matches.some((capability) => capability.assurance === "unavailable"))
22136
- return {
22137
- assurance: "unavailable",
22138
- reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
22139
- };
22140
- if (matches.some((capability) => capability.assurance === "enforced"))
22141
- return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
22142
- return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
22143
- };
22144
- function selectMethods(policy, capabilities) {
22145
- return Object.entries(METHODS).flatMap(([id, definition]) => {
22146
- const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
22147
- if (requirements.length === 0)
22148
- return [];
22149
- const capability = assuranceFor(id, definition, capabilities);
22150
- const reasons = [
22151
- ...new Set(requirements.map((requirement) => requirement.reason)),
22152
- ...capability.reasons
22153
- ];
22154
- if (capability.assurance === "unavailable")
22155
- reasons.unshift("required capability is unavailable");
22156
- return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
22753
+ seen.add(item.id);
22754
+ return true;
22157
22755
  });
22158
22756
  }
22159
-
22160
- // packages/workit-core/src/core/task-context.ts
22161
- function reconcileResume(view, observations = []) {
22162
- if (observations.length > 0)
22163
- return failure2("permission_denied", "worker observations require native host verification");
22164
- const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
22165
- if (!candidate.ok)
22166
- return candidate;
22167
- const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
22168
- const blockers = [...view.task.progress.blockers];
22169
- if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
22170
- blockers.push({
22171
- reason: "worker state requires reconciliation",
22172
- dependentAction: "resume",
22173
- refs: []
22174
- });
22175
- return success(null, null, {
22176
- candidate: candidate.data,
22177
- staleEvidenceIds,
22178
- workerUpdates: [],
22179
- blockers,
22180
- reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
22181
- });
22757
+ function resolvePolicy(input) {
22758
+ const normalized = normalizeResolverInput(input);
22759
+ if (!normalized.ok)
22760
+ return normalized;
22761
+ const requirements = resolveRequirements(normalized.data);
22762
+ const policy = {
22763
+ policyVersion: POLICY_VERSION,
22764
+ inputDigest: sha256(canonicalJson(normalized.data)),
22765
+ requirements
22766
+ };
22767
+ const parsed = policySchema.safeParse(policy);
22768
+ if (!parsed.success)
22769
+ return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
22770
+ return success(null, null, parsed.data);
22182
22771
  }
22183
- var COMPACT_MAX_BYTES = 4096;
22184
- var COMPACT_MAX_ITEMS = 8;
22185
- var COMPACT_TEXT_BYTES = 128;
22186
- var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
22187
- var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
22188
- var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
22189
- var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
22190
- const separator = match.search(/[:=]/u);
22191
- if (separator >= 0)
22192
- return `${match.slice(0, separator)}=<redacted>`;
22193
- const space = match.search(/\s/u);
22194
- return `${match.slice(0, space)} <redacted>`;
22195
- }).slice(0, limit);
22196
- var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
22197
- var compactTimestamp = (value) => `${value.slice(0, 19)}.${value.slice(20, -1).padEnd(9, "0")}`;
22198
- var compactDecisionChoice = (entry) => {
22199
- const stated = entry.data.binding.statedChoice?.text;
22200
- const approved = entry.data.response === "approved" ? entry.data.binding.approvedContent : "";
22201
- const source = stated?.trim() || approved.trim() || null;
22202
- return compactText(source, COMPACT_TEXT_BYTES);
22203
- };
22204
- var compactReference = (ref) => {
22205
- if (ref.kind === "file")
22206
- return {
22207
- kind: "file",
22208
- path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
22209
- };
22210
- if (ref.kind === "record")
22211
- return {
22212
- kind: "record",
22213
- collection: ref.collection,
22214
- id: ref.id
22215
- };
22216
- return { kind: ref.kind };
22217
- };
22218
- function compactTaskContext(view) {
22219
- const decisions = view.task.decisions.slice().toSorted((left, right) => {
22220
- const leftAt = compactTimestamp(left.recordedAt);
22221
- const rightAt = compactTimestamp(right.recordedAt);
22222
- if (leftAt !== rightAt)
22223
- return leftAt > rightAt ? -1 : 1;
22224
- return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
22225
- }).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
22226
- id: entry.id,
22227
- purpose: entry.data.purpose,
22228
- status: entry.data.response,
22229
- digest: entry.data.digest,
22230
- choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
22231
- references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
22232
- }));
22233
- const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
22234
- id: method.id,
22235
- assurance: method.assurance,
22236
- reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
22237
- })) : [];
22238
- const gaps = Array.from(new Set([
22239
- ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
22240
- ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
22241
- ...view.task.progress.blockers.map((entry) => entry.reason)
22242
- ])).toSorted().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
22243
- const context = {
22244
- objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
22245
- status: view.task.status,
22246
- decisions,
22247
- methods,
22248
- gaps,
22249
- nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
22772
+ function diffPolicy(previous, next, reason, now) {
22773
+ if (!policySchema.safeParse(previous).success && previous !== null)
22774
+ throw new TypeError("previous policy is invalid");
22775
+ if (!policySchema.safeParse(next).success)
22776
+ throw new TypeError("next policy is invalid");
22777
+ const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
22778
+ const newIds = new Set(next.requirements.map((item) => item.id));
22779
+ const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits3);
22780
+ const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits3);
22781
+ if (added.length === 0 && retired.length === 0)
22782
+ return null;
22783
+ const normalizedReason = reason.trim();
22784
+ const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
22785
+ const change = {
22786
+ recordedAt: now,
22787
+ fromInputDigest: previous?.inputDigest ?? null,
22788
+ toInputDigest: next.inputDigest,
22789
+ added,
22790
+ retired,
22791
+ reason: changeReason
22250
22792
  };
22251
- let encoded = JSON.stringify(context);
22252
- while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
22253
- if (context.gaps.length)
22254
- context.gaps.pop();
22255
- else if (context.decisions.length)
22256
- context.decisions.pop();
22257
- else
22258
- context.methods.pop();
22259
- encoded = JSON.stringify(context);
22260
- }
22261
- if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
22262
- context.objective = compactText(context.objective, 64) ?? "";
22263
- context.nextAction = compactText(context.nextAction, 64);
22264
- encoded = JSON.stringify(context);
22265
- }
22266
- return encoded;
22793
+ const parsed = policyChangeSchema.safeParse(change);
22794
+ if (!parsed.success)
22795
+ throw new TypeError("policy change is invalid");
22796
+ return parsed.data;
22267
22797
  }
22268
- var exportDigest = (bundle) => sha256({
22269
- schemaVersion: bundle.schemaVersion,
22270
- exportedAt: bundle.exportedAt,
22271
- sourceWorkspaceId: bundle.sourceWorkspaceId,
22272
- task: bundle.task
22273
- });
22274
22798
 
22275
22799
  // packages/workit-core/src/core/task-engine.ts
22276
22800
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
@@ -22443,6 +22967,29 @@ var applyWorkerLifecycle = (input) => {
22443
22967
  return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
22444
22968
  };
22445
22969
  var refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }));
22970
+ var REVISION_RETRY_ATTEMPTS = 8;
22971
+ var retryPause = new Int32Array(new SharedArrayBuffer(4));
22972
+ var filledRevisions = (request) => {
22973
+ const value = typeof request === "object" && request !== null ? request : {};
22974
+ return {
22975
+ task: value.expectedRevision === undefined,
22976
+ workspace: value.expectedWorkspaceRevision === undefined
22977
+ };
22978
+ };
22979
+ var filledConflict = (result, filled) => !result.ok && result.code === "revision_conflict" && (("actualRevision" in result.details) && filled.task || ("actualWorkspaceRevision" in result.details) && filled.workspace);
22980
+ var retryFilledRevisions = (filled, run) => {
22981
+ if (!filled.task && !filled.workspace)
22982
+ return run();
22983
+ let result = run();
22984
+ const deadline = Date.now() + defaultLockTimeout();
22985
+ for (let attempt = 1;filledConflict(result, filled); attempt += 1) {
22986
+ if (attempt >= REVISION_RETRY_ATTEMPTS || Date.now() >= deadline)
22987
+ return failure2("busy", "records kept changing under concurrent writers; retry the call");
22988
+ Atomics.wait(retryPause, 0, 0, Math.floor(Math.random() * 4 * attempt) + 1);
22989
+ result = run();
22990
+ }
22991
+ return result;
22992
+ };
22446
22993
  var trustedNow = (context) => typeof context.now === "function" ? context.now() : context.now;
22447
22994
  var mapRef = (ref, ids) => {
22448
22995
  if (ref.kind !== "record")
@@ -22690,6 +23237,9 @@ class WorkitCore {
22690
23237
  if (input.expectedWorkspaceRevision === undefined)
22691
23238
  input.expectedWorkspaceRevision = workspace ? workspace.revision : null;
22692
23239
  }
23240
+ retryOmittedRevisions(request, run) {
23241
+ return retryFilledRevisions(filledRevisions(request), run);
23242
+ }
22693
23243
  helperEntry(task, requireSession = false, requireActive = false) {
22694
23244
  const workerId = this.context.workerId ?? null;
22695
23245
  if (workerId === null)
@@ -22721,6 +23271,9 @@ class WorkitCore {
22721
23271
  return null;
22722
23272
  }
22723
23273
  state(request) {
23274
+ return request?.action === "import" ? this.retryOmittedRevisions(request, () => this.stateOnce(request)) : this.stateOnce(request);
23275
+ }
23276
+ stateOnce(request) {
22724
23277
  const root = this.contextRootError();
22725
23278
  if (!root.ok)
22726
23279
  return root;
@@ -22822,6 +23375,9 @@ class WorkitCore {
22822
23375
  });
22823
23376
  }
22824
23377
  task(request) {
23378
+ return this.retryOmittedRevisions(request, () => this.taskOnce(request));
23379
+ }
23380
+ taskOnce(request) {
22825
23381
  const root = this.contextRootError();
22826
23382
  if (!root.ok)
22827
23383
  return root;
@@ -22921,6 +23477,9 @@ class WorkitCore {
22921
23477
  }
22922
23478
  }
22923
23479
  policy(request) {
23480
+ return this.retryOmittedRevisions(request, () => this.policyOnce(request));
23481
+ }
23482
+ policyOnce(request) {
22924
23483
  const root = this.contextRootError();
22925
23484
  if (!root.ok)
22926
23485
  return root;
@@ -22967,6 +23526,9 @@ class WorkitCore {
22967
23526
  return success(changed.data.revision, null, changed.data.policy);
22968
23527
  }
22969
23528
  evidence(request) {
23529
+ return this.retryOmittedRevisions(request, () => this.evidenceOnce(request));
23530
+ }
23531
+ evidenceOnce(request) {
22970
23532
  const root = this.contextRootError();
22971
23533
  if (!root.ok)
22972
23534
  return root;
@@ -23051,6 +23613,23 @@ class WorkitCore {
23051
23613
  observeStandingDecision(request) {
23052
23614
  return this.recordDecision(request, undefined, true, true);
23053
23615
  }
23616
+ commitTask(taskId, expected, filled, recheck, update) {
23617
+ let revision = expected;
23618
+ let first = true;
23619
+ return retryFilledRevisions({ task: filled, workspace: false }, () => {
23620
+ if (!first) {
23621
+ const fresh = this.store.readTask(taskId);
23622
+ if (!fresh.ok)
23623
+ return fresh;
23624
+ const checked = recheck(fresh.data);
23625
+ if (!checked.ok)
23626
+ return checked;
23627
+ revision = fresh.data.revision;
23628
+ }
23629
+ first = false;
23630
+ return this.store.mutateTask(taskId, revision, update, trustedNow(this.context));
23631
+ });
23632
+ }
23054
23633
  recordDecision(request, nativeObservation, nativeRequired = false, standing = false) {
23055
23634
  const root = this.contextRootError();
23056
23635
  if (!root.ok)
@@ -23075,6 +23654,7 @@ class WorkitCore {
23075
23654
  return task;
23076
23655
  if ((this.context.workerId ?? null) !== null)
23077
23656
  return failure2("permission_denied", "helpers cannot record or revoke decisions");
23657
+ const filled = input.expectedRevision === undefined;
23078
23658
  this.fillRevisions(input, task.data);
23079
23659
  if (input.action === "record") {
23080
23660
  if (task.data.status === "closed")
@@ -23123,7 +23703,20 @@ class WorkitCore {
23123
23703
  const nativeProvenance = native?.ok ? retireNativeAuthority(native.data) : null;
23124
23704
  if (native?.ok && !nativeProvenance)
23125
23705
  return failure2("permission_denied", "native decision authority was retired");
23126
- const changed = this.store.mutateTask(task.data.id, input.expectedRevision, (current, mutation) => {
23706
+ let standingProvenance = standingApproval?.ok === true ? standingApproval.data : null;
23707
+ const recheck = (fresh) => {
23708
+ if (fresh.status === "closed")
23709
+ return failure2("invalid_transition", "closed task cannot record a decision");
23710
+ if (!standingProvenance)
23711
+ return success(null, null, null);
23712
+ const again = verifyStandingApproval(this.store.root, fresh, this.context.caller, input.binding);
23713
+ if (again.ok)
23714
+ standingProvenance = again.data;
23715
+ return again;
23716
+ };
23717
+ const changed = this.commitTask(task.data.id, input.expectedRevision, filled, recheck, (current, mutation) => {
23718
+ if (current.status === "closed")
23719
+ return failure2("invalid_transition", "closed task cannot record a decision");
23127
23720
  const latestContent = verifyDecisionContent(this.store, input.binding);
23128
23721
  if (!latestContent.ok)
23129
23722
  return latestContent;
@@ -23135,14 +23728,14 @@ class WorkitCore {
23135
23728
  const entry = {
23136
23729
  id: newId(),
23137
23730
  recordedAt: mutation.now,
23138
- provenance: standingApproval?.ok === true ? standingApproval.data : nativeProvenance ?? provenance(this.context, "agent_reported"),
23731
+ provenance: standingProvenance ?? nativeProvenance ?? provenance(this.context, "agent_reported"),
23139
23732
  data
23140
23733
  };
23141
23734
  return success(mutation.revision, null, {
23142
23735
  ...current,
23143
23736
  decisions: [...current.decisions, entry]
23144
23737
  });
23145
- }, trustedNow(this.context));
23738
+ });
23146
23739
  if (!changed.ok)
23147
23740
  return changed;
23148
23741
  return success(changed.data.revision, null, changed.data.decisions.at(-1));
@@ -23156,7 +23749,7 @@ class WorkitCore {
23156
23749
  return failure2("invalid_transition", "decision is already revoked");
23157
23750
  if (decision.data.consumption)
23158
23751
  return failure2("permission_denied", "consumed decision cannot be revoked");
23159
- const changed = this.store.mutateTask(task.data.id, input.expectedRevision, (current, mutation) => {
23752
+ const changed = this.commitTask(task.data.id, input.expectedRevision, filled, () => success(null, null, null), (current, mutation) => {
23160
23753
  const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
23161
23754
  if (!entry)
23162
23755
  return failure2("not_found", "decision not found");
@@ -23173,13 +23766,16 @@ class WorkitCore {
23173
23766
  ...current,
23174
23767
  decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? updated : candidate)
23175
23768
  });
23176
- }, trustedNow(this.context));
23769
+ });
23177
23770
  if (!changed.ok)
23178
23771
  return changed;
23179
23772
  const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
23180
23773
  return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
23181
23774
  }
23182
23775
  finding(request) {
23776
+ return this.retryOmittedRevisions(request, () => this.findingOnce(request));
23777
+ }
23778
+ findingOnce(request) {
23183
23779
  const root = this.contextRootError();
23184
23780
  if (!root.ok)
23185
23781
  return root;
@@ -23303,6 +23899,9 @@ class WorkitCore {
23303
23899
  return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
23304
23900
  }
23305
23901
  worker(request) {
23902
+ return this.retryOmittedRevisions(request, () => this.workerOnce(request));
23903
+ }
23904
+ workerOnce(request) {
23306
23905
  const root = this.contextRootError();
23307
23906
  if (!root.ok)
23308
23907
  return root;
@@ -23650,6 +24249,9 @@ class WorkitCore {
23650
24249
  return applied;
23651
24250
  }
23652
24251
  writer(request) {
24252
+ return this.retryOmittedRevisions(request, () => this.writerOnce(request));
24253
+ }
24254
+ writerOnce(request) {
23653
24255
  const root = this.contextRootError();
23654
24256
  if (!root.ok)
23655
24257
  return root;
@@ -24252,122 +24854,6 @@ class WorkitCore {
24252
24854
  return this.summary(changed.data.task);
24253
24855
  }
24254
24856
  }
24255
-
24256
- // packages/workit-core/src/core/session-context.ts
24257
- var cache = new Map;
24258
- // packages/workit-core/src/core/external-action.ts
24259
- var chainStepSchema = union([
24260
- string2().min(1),
24261
- object2({ branch: string2().min(1) }).strict(),
24262
- object2({ pr: literal(true) }).strict()
24263
- ]);
24264
- var externalActionSchema = discriminatedUnion("operation", [
24265
- object2({
24266
- operation: literal("git.branch_setup"),
24267
- payload: object2({
24268
- action: _enum(["setup", "reapply_stash"]).optional(),
24269
- sdd_dir: string2().optional(),
24270
- target_branch: string2().optional(),
24271
- stash: _enum(["yes", "no"]).optional(),
24272
- cwd: string2().min(1).optional()
24273
- }).strict()
24274
- }).strict(),
24275
- object2({
24276
- operation: literal("git.commit"),
24277
- payload: object2({
24278
- message: string2().min(1).optional(),
24279
- plan_steps: array(chainStepSchema).min(1).max(32).optional(),
24280
- plan_branch: string2().min(1).optional(),
24281
- cwd: string2().min(1).optional()
24282
- }).strict()
24283
- }).strict(),
24284
- object2({
24285
- operation: literal("git.push"),
24286
- payload: object2({ branch: string2().min(1).optional(), cwd: string2().min(1).optional() }).strict()
24287
- }).strict(),
24288
- object2({
24289
- operation: literal("hosting.pull_request"),
24290
- payload: object2({
24291
- title: string2().min(1),
24292
- body: string2().optional(),
24293
- draft: boolean2().optional(),
24294
- target_branch: string2().optional(),
24295
- babysit: boolean2().optional(),
24296
- cwd: string2().min(1).optional()
24297
- }).strict()
24298
- }).strict(),
24299
- object2({
24300
- operation: literal("hosting.merge"),
24301
- payload: object2({
24302
- target_branch: string2().min(1).optional(),
24303
- source_branch: string2().min(1).optional(),
24304
- cwd: string2().min(1).optional()
24305
- }).strict()
24306
- }).strict(),
24307
- object2({
24308
- operation: literal("hosting.delete_branch"),
24309
- payload: object2({ branch: string2().min(1), cwd: string2().min(1).optional() }).strict()
24310
- }).strict(),
24311
- object2({
24312
- operation: literal("youtrack.update"),
24313
- payload: object2({
24314
- issueId: string2().min(1),
24315
- markdown: string2().min(1),
24316
- minutes: number2().positive().optional()
24317
- }).strict()
24318
- }).strict(),
24319
- object2({
24320
- operation: literal("youtrack.time"),
24321
- payload: object2({
24322
- issueId: string2().min(1),
24323
- minutes: number2().positive(),
24324
- text: string2().optional(),
24325
- dateMs: number2().finite().optional()
24326
- }).strict()
24327
- }).strict(),
24328
- object2({
24329
- operation: literal("youtrack.meeting"),
24330
- payload: object2({
24331
- issueId: string2().min(1),
24332
- minutes: number2().positive(),
24333
- text: string2().min(1)
24334
- }).strict()
24335
- }).strict(),
24336
- object2({
24337
- operation: literal("changelog.apply"),
24338
- payload: object2({
24339
- entries: union([
24340
- record(string2(), array(string2())),
24341
- array(object2({ category: string2().min(1), text: string2().min(1) }).strict())
24342
- ]).optional(),
24343
- path: string2().optional(),
24344
- normalize_only: boolean2().optional()
24345
- }).strict()
24346
- }).strict(),
24347
- object2({
24348
- operation: literal("context.read"),
24349
- payload: object2({
24350
- kind: _enum([
24351
- "git",
24352
- "pr",
24353
- "youtrack",
24354
- "github_issue",
24355
- "gitlab_issue",
24356
- "changelog",
24357
- "release",
24358
- "affected"
24359
- ]),
24360
- range: string2().optional(),
24361
- issueId: string2().optional(),
24362
- issueUrl: string2().optional(),
24363
- issueRef: string2().optional(),
24364
- mode: string2().optional(),
24365
- specPath: string2().optional(),
24366
- planPath: string2().optional(),
24367
- cwd: string2().min(1).optional()
24368
- }).strict()
24369
- }).strict()
24370
- ]);
24371
24857
  // packages/workit-core/src/core.ts
24372
24858
  var revision2 = /^[A-Za-z0-9@][A-Za-z0-9@._/~^{}-]*$/;
24373
24859
  function gitRevisionParts(value) {
@@ -24411,7 +24897,7 @@ function resolveInside(root, candidate) {
24411
24897
  }
24412
24898
  function run2(root, executable, args, env = {}) {
24413
24899
  const cwd = realpathSync7(root);
24414
- const result = spawnSync3(executable, args, {
24900
+ const result = spawnSync4(executable, args, {
24415
24901
  cwd,
24416
24902
  encoding: "utf8",
24417
24903
  env: { ...process.env, ...env }
@@ -24482,9 +24968,9 @@ var applyPatterns = (value) => {
24482
24968
  var redactSecrets = (value) => applyPatterns(value);
24483
24969
 
24484
24970
  // packages/workit-core/src/core/pr-create.ts
24485
- import fs8 from "node:fs";
24971
+ import fs9 from "node:fs";
24486
24972
  import path12 from "node:path";
24487
- import { spawnSync as spawnSync4 } from "node:child_process";
24973
+ import { spawnSync as spawnSync5 } from "node:child_process";
24488
24974
  function parseGhIssue(value) {
24489
24975
  const m = /issues\/(\d+)/.exec(value);
24490
24976
  return m ? m[1] : String(value).trim().replace(/^#/, "");
@@ -24497,7 +24983,7 @@ function whichOnPath(tool) {
24497
24983
  for (const name of names) {
24498
24984
  const candidate = path12.join(dir, name);
24499
24985
  try {
24500
- fs8.accessSync(candidate, fs8.constants.X_OK);
24986
+ fs9.accessSync(candidate, fs9.constants.X_OK);
24501
24987
  return candidate;
24502
24988
  } catch {}
24503
24989
  }
@@ -24506,7 +24992,7 @@ function whichOnPath(tool) {
24506
24992
  }
24507
24993
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
24508
24994
  var pushRemote = (cwd) => {
24509
- const result = spawnSync4("git", ["remote", "get-url", "--push", "--all", "origin"], {
24995
+ const result = spawnSync5("git", ["remote", "get-url", "--push", "--all", "origin"], {
24510
24996
  cwd,
24511
24997
  encoding: "utf8"
24512
24998
  });
@@ -24517,7 +25003,7 @@ var pushRemote = (cwd) => {
24517
25003
  };
24518
25004
 
24519
25005
  // packages/workit-core/src/core/repo-context.ts
24520
- import { spawnSync as spawnSync5 } from "node:child_process";
25006
+ import { spawnSync as spawnSync6 } from "node:child_process";
24521
25007
  import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
24522
25008
  import path13 from "node:path";
24523
25009
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
@@ -24546,7 +25032,7 @@ var invalidRange = (cwd) => ({
24546
25032
  cwd
24547
25033
  });
24548
25034
  var runGit = (cwd, args) => {
24549
- const result = spawnSync5("git", args, { cwd, encoding: "utf8" });
25035
+ const result = spawnSync6("git", args, { cwd, encoding: "utf8" });
24550
25036
  return {
24551
25037
  stdout: (result.stdout ?? "").trimEnd(),
24552
25038
  stderr: (result.stderr ?? "").trimEnd(),
@@ -24719,7 +25205,7 @@ var documentationFiles = (cwd) => {
24719
25205
  walk(cwd, 1);
24720
25206
  return matches.toSorted().slice(0, 200);
24721
25207
  };
24722
- var readTrimmed = (file, maxLines) => {
25208
+ var readTrimmed2 = (file, maxLines) => {
24723
25209
  if (!existsSync6(file))
24724
25210
  return "";
24725
25211
  const lines = readFileSync7(file, "utf8").split(`
@@ -24786,7 +25272,7 @@ ${status.stderr}` : "");
24786
25272
  stdout += `template_path: ${template}
24787
25273
 
24788
25274
  `;
24789
- stdout += readTrimmed(path13.join(cwd, template), 220);
25275
+ stdout += readTrimmed2(path13.join(cwd, template), 220);
24790
25276
  } else {
24791
25277
  stdout += `template_path: none
24792
25278
 
@@ -24858,7 +25344,7 @@ function changelogContext(root, range) {
24858
25344
  stdout += printSection("Existing CHANGELOG.md");
24859
25345
  const changelogPath = path13.join(cwd, "CHANGELOG.md");
24860
25346
  if (existsSync6(changelogPath)) {
24861
- stdout += readTrimmed(changelogPath, 260) + `
25347
+ stdout += readTrimmed2(changelogPath, 260) + `
24862
25348
  `;
24863
25349
  } else {
24864
25350
  stdout += `CHANGELOG.md not found.
@@ -24900,7 +25386,7 @@ function docsRefreshContext(root, range) {
24900
25386
  stdout += printSection("README Preview");
24901
25387
  const readme = path13.join(cwd, "README.md");
24902
25388
  if (existsSync6(readme)) {
24903
- stdout += readTrimmed(readme, 220) + `
25389
+ stdout += readTrimmed2(readme, 220) + `
24904
25390
  `;
24905
25391
  } else {
24906
25392
  stdout += `README.md not found.
@@ -24987,7 +25473,7 @@ function releaseNotesContext(root, rangeOrTag) {
24987
25473
  }
24988
25474
 
24989
25475
  // packages/workit-core/src/core/tracker-issues.ts
24990
- import { spawnSync as spawnSync6 } from "node:child_process";
25476
+ import { spawnSync as spawnSync7 } from "node:child_process";
24991
25477
  var repoPathFromRemote = (remote) => {
24992
25478
  let rest = (remote || "").trim().replace(/\/+$/, "");
24993
25479
  if (!rest)
@@ -25008,7 +25494,7 @@ var defaultCli = (bin, available) => {
25008
25494
  if (!available)
25009
25495
  return null;
25010
25496
  return (args, root) => {
25011
- const out = spawnSync6(bin, args, {
25497
+ const out = spawnSync7(bin, args, {
25012
25498
  cwd: root,
25013
25499
  encoding: "utf8",
25014
25500
  env: { ...process.env, PATH: process.env.PATH ?? "" }
@@ -25086,7 +25572,7 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
25086
25572
  };
25087
25573
 
25088
25574
  // packages/workit-core/src/core/youtrack.ts
25089
- import fs9 from "node:fs";
25575
+ import fs10 from "node:fs";
25090
25576
  import path14 from "node:path";
25091
25577
 
25092
25578
  // packages/workit-core/src/core/templates.ts
@@ -25105,17 +25591,17 @@ var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? p
25105
25591
  var youTrackTokenModeOk = (p) => {
25106
25592
  if (process.platform === "win32")
25107
25593
  return true;
25108
- const mode = fs9.statSync(p).mode & 511;
25594
+ const mode = fs10.statSync(p).mode & 511;
25109
25595
  return mode === 384;
25110
25596
  };
25111
25597
  function readYouTrackConfig(required) {
25112
25598
  const cfgPath = youTrackConfigPath();
25113
- if (!fs9.existsSync(cfgPath)) {
25599
+ if (!fs10.existsSync(cfgPath)) {
25114
25600
  return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
25115
25601
  }
25116
25602
  let parsed;
25117
25603
  try {
25118
- parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
25604
+ parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
25119
25605
  } catch {
25120
25606
  return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
25121
25607
  }
@@ -25126,11 +25612,11 @@ function readYouTrackConfig(required) {
25126
25612
  }
25127
25613
  var readYouTrackContextConfig = () => {
25128
25614
  const cfgPath = youTrackReadOnlyConfigPath();
25129
- if (!fs9.existsSync(cfgPath))
25615
+ if (!fs10.existsSync(cfgPath))
25130
25616
  return { data: {} };
25131
25617
  let parsed;
25132
25618
  try {
25133
- parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
25619
+ parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
25134
25620
  } catch {
25135
25621
  return { error: "YouTrack configuration is unavailable" };
25136
25622
  }
@@ -25170,11 +25656,11 @@ var youTrackToken = () => {
25170
25656
  return loaded;
25171
25657
  const tokenFile = String(loaded.config.tokenFile ?? "");
25172
25658
  const tokenPath = tokenFile ? path14.isAbsolute(tokenFile) ? path14.resolve(tokenFile) : path14.resolve(process.cwd(), tokenFile) : "";
25173
- if (!tokenPath || !fs9.existsSync(tokenPath))
25659
+ if (!tokenPath || !fs10.existsSync(tokenPath))
25174
25660
  return { error: "missing youtrack.token" };
25175
25661
  if (!youTrackTokenModeOk(tokenPath))
25176
25662
  return { error: "youtrack.token mode must be 0600" };
25177
- const token = fs9.readFileSync(tokenPath, "utf8").trim();
25663
+ const token = fs10.readFileSync(tokenPath, "utf8").trim();
25178
25664
  if (!token)
25179
25665
  return { error: "empty token file" };
25180
25666
  if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
@@ -25239,9 +25725,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
25239
25725
  } catch {
25240
25726
  continue;
25241
25727
  }
25242
- if (!fs9.existsSync(full))
25728
+ if (!fs10.existsSync(full))
25243
25729
  continue;
25244
- const text = fs9.readFileSync(full, "utf8");
25730
+ const text = fs10.readFileSync(full, "utf8");
25245
25731
  const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
25246
25732
  if (m)
25247
25733
  return m[1];