@brainervirus/workit-cursor 2.1.5 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16785,7 +16785,7 @@ class StdioServerTransport {
16785
16785
  import { fileURLToPath as fileURLToPath2 } from "node:url";
16786
16786
 
16787
16787
  // packages/workit-core/src/core.ts
16788
- import { spawnSync as spawnSync3 } from "node:child_process";
16788
+ import { spawnSync as spawnSync4 } from "node:child_process";
16789
16789
  import { existsSync as existsSync5, realpathSync as realpathSync7 } from "node:fs";
16790
16790
  import path11 from "node:path";
16791
16791
 
@@ -16865,7 +16865,8 @@ var hostSchema = _enum([
16865
16865
  "codex_cli",
16866
16866
  "codex_desktop",
16867
16867
  "pi",
16868
- "workit_cli"
16868
+ "workit_cli",
16869
+ "claude_code"
16869
16870
  ]);
16870
16871
  var assuranceSchema = _enum(["enforced", "agent_guided", "unavailable"]);
16871
16872
  var outcomeSchema = _enum(["verified", "accepted_limitations", "stopped"]);
@@ -17315,15 +17316,70 @@ var taskRecordSchema = object2({
17315
17316
  decisions: array(entrySchema(decisionSchema)),
17316
17317
  actionProgress: actionProgressListSchema.optional(),
17317
17318
  findings: array(entrySchema(findingSchema)),
17318
- workers: array(entrySchema(workerSchema))
17319
+ workers: array(entrySchema(workerSchema)),
17320
+ critical: array(nonEmpty).optional()
17319
17321
  }).strict();
17322
+ var stripCount = (strips) => strips.reduce((sum, strip) => sum + strip.keys.length, 0);
17323
+ var strippable = (issues, prefix = []) => {
17324
+ const strips = [];
17325
+ for (const issue of issues) {
17326
+ if (issue.code === "unrecognized_keys") {
17327
+ strips.push({ path: [...prefix, ...issue.path], keys: issue.keys });
17328
+ continue;
17329
+ }
17330
+ if (issue.code !== "invalid_union")
17331
+ return null;
17332
+ const branch = issue.errors.map((errors) => strippable(errors, [...prefix, ...issue.path])).filter((found) => found !== null && found.length > 0).reduce((best, found) => best === null || stripCount(found) < stripCount(best) ? found : best, null);
17333
+ if (!branch)
17334
+ return null;
17335
+ strips.push(...branch);
17336
+ }
17337
+ return strips;
17338
+ };
17339
+ var recordPath = (path) => path.map((key) => typeof key === "number" ? "*" : String(key)).join(".");
17340
+ var overlaps = (left, right) => left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
17341
+ var parseStoredRecord = (schema, value) => {
17342
+ let parsed = schema.safeParse(value);
17343
+ if (parsed.success)
17344
+ return { success: true, data: parsed.data, stripped: [] };
17345
+ const first = { success: false, error: parsed.error, critical: [] };
17346
+ const declared = typeof value === "object" && value !== null && Array.isArray(value.critical) ? value.critical.filter((item) => typeof item === "string").map((item) => item.split(".").map((key) => /^\d+$/.test(key) ? "*" : key).join(".")) : [];
17347
+ const stripped = [];
17348
+ let current = structuredClone(value);
17349
+ for (let round = 0;round < 8 && !parsed.success; round++) {
17350
+ const strips = strippable(parsed.error.issues);
17351
+ if (strips === null || strips.length === 0)
17352
+ return first;
17353
+ for (const strip of strips) {
17354
+ let target = current;
17355
+ for (const key of strip.path)
17356
+ target = typeof target === "object" && target !== null ? target[key] : undefined;
17357
+ if (typeof target !== "object" || target === null)
17358
+ return first;
17359
+ for (const key of strip.keys) {
17360
+ stripped.push(recordPath([...strip.path, key]));
17361
+ delete target[key];
17362
+ }
17363
+ }
17364
+ parsed = schema.safeParse(current);
17365
+ }
17366
+ if (!parsed.success)
17367
+ return first;
17368
+ const critical = [
17369
+ ...new Set(stripped.filter((item) => declared.some((path) => overlaps(item, path))))
17370
+ ];
17371
+ if (critical.length > 0)
17372
+ return { ...first, critical };
17373
+ return { success: true, data: parsed.data, stripped: [...new Set(stripped)] };
17374
+ };
17320
17375
  var workspaceRecordSchema = object2({
17321
17376
  schemaVersion: literal(1),
17322
17377
  id,
17323
17378
  revision,
17324
17379
  root: nonEmpty,
17325
17380
  runtime: runtimeSchema.optional(),
17326
- writer: object2({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
17381
+ writer: object2({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable(),
17382
+ critical: array(nonEmpty).optional()
17327
17383
  }).strict();
17328
17384
  var capabilitySchema = object2({
17329
17385
  name: nonEmpty,
@@ -17561,6 +17617,11 @@ var operationSchemas = {
17561
17617
  writer: discriminatedUnion("action", Object.values(writerOperations)),
17562
17618
  state: discriminatedUnion("action", Object.values(stateOperations))
17563
17619
  };
17620
+ var { recover: _unadvertisedRecover, ...advertisedStateOperations } = stateOperations;
17621
+ var advertisedOperationSchemas = {
17622
+ ...operationSchemas,
17623
+ state: discriminatedUnion("action", Object.values(advertisedStateOperations))
17624
+ };
17564
17625
  var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
17565
17626
  var success = (revisionValue, workspaceRevisionValue, data) => ({
17566
17627
  ok: true,
@@ -17584,7 +17645,7 @@ function parseOperation(family, input) {
17584
17645
  return failure2("invalid_input", "operation input is invalid", { fields });
17585
17646
  }
17586
17647
  function operationJsonSchema(family) {
17587
- return toJSONSchema(operationSchemas[family], { target: "draft-2020-12" });
17648
+ return toJSONSchema(advertisedOperationSchemas[family], { target: "draft-2020-12" });
17588
17649
  }
17589
17650
  var OPERATION_SCHEMA_DEPTH = 1;
17590
17651
  var schemaShapeDescription = (value, depth = 0) => {
@@ -17776,8 +17837,7 @@ function requirementId(input) {
17776
17837
  // packages/workit-core/src/core/task-store.ts
17777
17838
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
17778
17839
  import { AsyncLocalStorage } from "node:async_hooks";
17779
- import * as fs4 from "node:fs";
17780
- import { hostname } from "node:os";
17840
+ import * as fs5 from "node:fs";
17781
17841
  import path4 from "node:path";
17782
17842
 
17783
17843
  // packages/workit-core/src/core/package-root.ts
@@ -18400,13 +18460,10 @@ function acquireFileLockSync(targetPath, options) {
18400
18460
  }
18401
18461
  }
18402
18462
  }
18403
- // packages/workit-core/src/core/task-store.ts
18404
- var processEvidenceSchema = object2({
18405
- state: _enum(["stopped", "accounted_for"]),
18406
- pid: number2().int().nonnegative(),
18407
- processStart: string2().nullable(),
18408
- ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
18409
- }).strict();
18463
+ // packages/workit-core/src/core/store-lock.ts
18464
+ import { spawnSync } from "node:child_process";
18465
+ import fs4 from "node:fs";
18466
+ import { hostname } from "node:os";
18410
18467
  var metadataLockSchema = object2({
18411
18468
  pid: number2().int().nonnegative().safe(),
18412
18469
  processStart: string2().nullable(),
@@ -18414,6 +18471,143 @@ var metadataLockSchema = object2({
18414
18471
  nonce: string2().min(1),
18415
18472
  externalAction: literal(true).optional()
18416
18473
  }).strict();
18474
+ var FOREIGN_LOCK_TTL_MS = 10 * 60000;
18475
+ var UNREADABLE_LOCK_TTL_MS = 30000;
18476
+ var RECLAIM_GUARD_TTL_MS = 30000;
18477
+ var defaultLockTimeoutMs = 250;
18478
+ var defaultLockTimeout = () => defaultLockTimeoutMs;
18479
+ var parseMetadataLock = (raw) => {
18480
+ let value;
18481
+ try {
18482
+ value = JSON.parse(raw);
18483
+ } catch {
18484
+ throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18485
+ }
18486
+ const parsed = metadataLockSchema.safeParse(value);
18487
+ if (!parsed.success)
18488
+ throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18489
+ return parsed.data;
18490
+ };
18491
+ var parseMetadataLockOrNull = (raw) => {
18492
+ try {
18493
+ return parseMetadataLock(raw);
18494
+ } catch {
18495
+ return null;
18496
+ }
18497
+ };
18498
+ var sameMetadataLock = (left, right) => {
18499
+ const parsed = metadataLockSchema.safeParse(left);
18500
+ return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
18501
+ };
18502
+ var parseProcStatStart = (stat) => {
18503
+ const close = stat.lastIndexOf(")");
18504
+ if (close < 0)
18505
+ return null;
18506
+ return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
18507
+ };
18508
+ var processStartOf = (pid) => {
18509
+ if (process.platform === "linux") {
18510
+ try {
18511
+ return parseProcStatStart(fs4.readFileSync(`/proc/${pid}/stat`, "utf8"));
18512
+ } catch {
18513
+ return null;
18514
+ }
18515
+ }
18516
+ if (process.platform === "darwin" || process.platform === "freebsd") {
18517
+ try {
18518
+ const run = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
18519
+ encoding: "utf8",
18520
+ timeout: 1000
18521
+ });
18522
+ const value = run.status === 0 ? run.stdout.trim() : "";
18523
+ return value || null;
18524
+ } catch {
18525
+ return null;
18526
+ }
18527
+ }
18528
+ return null;
18529
+ };
18530
+ var readTrimmed = (file) => {
18531
+ try {
18532
+ return fs4.readFileSync(file, "utf8").trim() || null;
18533
+ } catch {
18534
+ return null;
18535
+ }
18536
+ };
18537
+ var cachedLockHost = null;
18538
+ var localLockHost = () => {
18539
+ if (cachedLockHost !== null)
18540
+ return cachedLockHost;
18541
+ let pidns = null;
18542
+ try {
18543
+ pidns = /\[(\d+)\]/.exec(fs4.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
18544
+ } catch {}
18545
+ const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
18546
+ cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
18547
+ return cachedLockHost;
18548
+ };
18549
+ var pidAlive = (pid) => {
18550
+ if (!Number.isSafeInteger(pid) || pid <= 0)
18551
+ return false;
18552
+ try {
18553
+ process.kill(pid, 0);
18554
+ return true;
18555
+ } catch (error) {
18556
+ return error.code === "EPERM";
18557
+ }
18558
+ };
18559
+ var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
18560
+ const lock = metadataLockSchema.safeParse(payload);
18561
+ if (!lock.success)
18562
+ return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
18563
+ const { pid, processStart, host } = lock.data;
18564
+ const [lockName, lockSpace] = host.split("#");
18565
+ const [localName, localSpace] = localHost.split("#");
18566
+ if (lockSpace && localSpace && lockName === localName) {
18567
+ const [lockNs, lockBoot] = lockSpace.split(":");
18568
+ const [localNs, localBoot] = localSpace.split(":");
18569
+ if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
18570
+ return { state: "stale", reason: "lock was taken before this machine rebooted" };
18571
+ }
18572
+ if (host !== localHost)
18573
+ return ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS ? { state: "stale", reason: `lock from ${host} is older than its TTL` } : { state: "unknown", reason: `lock is held from ${host}; its pid cannot be checked here` };
18574
+ if (!pidAlive(pid))
18575
+ return { state: "stale", reason: `pid ${pid} is not running` };
18576
+ const currentStart = processStartOf(pid);
18577
+ if (processStart !== null && currentStart !== null && processStart !== currentStart)
18578
+ return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
18579
+ return { state: "live", reason: `held by running pid ${pid}` };
18580
+ };
18581
+ var ageOf = (file, nowMs) => {
18582
+ try {
18583
+ return nowMs - fs4.lstatSync(file).mtimeMs;
18584
+ } catch {
18585
+ return null;
18586
+ }
18587
+ };
18588
+ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
18589
+ const guard = `${lockPath}.reclaim`;
18590
+ const age = ageOf(guard, nowMs);
18591
+ if (age === null || age <= RECLAIM_GUARD_TTL_MS)
18592
+ return false;
18593
+ try {
18594
+ fs4.rmdirSync(guard);
18595
+ return true;
18596
+ } catch {
18597
+ return false;
18598
+ }
18599
+ };
18600
+
18601
+ // packages/workit-core/src/core/task-store.ts
18602
+ var RECOVERY_COPIES_PER_RECORD = 3;
18603
+ var STALE_TEMPORARY_MS = 60 * 60000;
18604
+ var RECOVERY_NAME = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/;
18605
+ var processEvidenceSchema = object2({
18606
+ state: _enum(["stopped", "accounted_for"]),
18607
+ pid: number2().int().nonnegative(),
18608
+ processStart: string2().nullable(),
18609
+ ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
18610
+ }).strict();
18417
18611
  var INDEX_VERSION = 1;
18418
18612
  var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
18419
18613
  var indexEntry = (task, file) => {
@@ -18441,7 +18635,7 @@ var indexEntry = (task, file) => {
18441
18635
  };
18442
18636
  var fileSignature = (file) => {
18443
18637
  try {
18444
- const stat = fs4.statSync(file, { bigint: true });
18638
+ const stat = fs5.statSync(file, { bigint: true });
18445
18639
  return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
18446
18640
  } catch {
18447
18641
  return null;
@@ -18458,7 +18652,7 @@ var cachedRuntimeVersion = null;
18458
18652
  var runtimeVersion = () => {
18459
18653
  if (cachedRuntimeVersion === null) {
18460
18654
  try {
18461
- const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
18655
+ const pkg = JSON.parse(fs5.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
18462
18656
  cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
18463
18657
  } catch {
18464
18658
  cachedRuntimeVersion = "0.0.0";
@@ -18487,36 +18681,20 @@ var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
18487
18681
  var isObject2 = (value) => typeof value === "object" && value !== null;
18488
18682
  var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
18489
18683
  var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
18490
- var parseMetadataLock = (raw) => {
18491
- let value;
18492
- try {
18493
- value = JSON.parse(raw);
18494
- } catch {
18495
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18496
- }
18497
- const parsed = metadataLockSchema.safeParse(value);
18498
- if (!parsed.success)
18499
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
18500
- return parsed.data;
18501
- };
18502
- var sameMetadataLock = (left, right) => {
18503
- const parsed = metadataLockSchema.safeParse(left);
18504
- return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
18505
- };
18506
18684
  var sameDirectoryIdentity = (left, right) => {
18507
18685
  if (!path4.isAbsolute(left) || !path4.isAbsolute(right))
18508
18686
  return false;
18509
18687
  const normalizedLeft = path4.resolve(left);
18510
18688
  const normalizedRight = path4.resolve(right);
18511
18689
  try {
18512
- const a = fs4.statSync(normalizedLeft, { bigint: true });
18513
- const b = fs4.statSync(normalizedRight, { bigint: true });
18690
+ const a = fs5.statSync(normalizedLeft, { bigint: true });
18691
+ const b = fs5.statSync(normalizedRight, { bigint: true });
18514
18692
  if (!a.isDirectory() || !b.isDirectory())
18515
18693
  return false;
18516
18694
  if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
18517
18695
  return true;
18518
- const canonicalLeft = fs4.realpathSync(normalizedLeft);
18519
- const canonicalRight = fs4.realpathSync(normalizedRight);
18696
+ const canonicalLeft = fs5.realpathSync(normalizedLeft);
18697
+ const canonicalRight = fs5.realpathSync(normalizedRight);
18520
18698
  if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
18521
18699
  return true;
18522
18700
  return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
@@ -18524,12 +18702,47 @@ var sameDirectoryIdentity = (left, right) => {
18524
18702
  return false;
18525
18703
  }
18526
18704
  };
18705
+ var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
18706
+ var isTransientWindowsError = (error) => {
18707
+ if (process.platform !== "win32")
18708
+ return false;
18709
+ const code = error?.code;
18710
+ return typeof code === "string" && TRANSIENT_WINDOWS_CODES.has(code);
18711
+ };
18712
+ var retryTransient = (run) => {
18713
+ if (process.platform !== "win32")
18714
+ return run();
18715
+ for (let attempt = 0;; attempt += 1) {
18716
+ try {
18717
+ return run();
18718
+ } catch (error) {
18719
+ if (attempt >= 20 || !isTransientWindowsError(error))
18720
+ throw error;
18721
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 * (attempt + 1));
18722
+ }
18723
+ }
18724
+ };
18527
18725
  var externalActionLockRoots = new AsyncLocalStorage;
18726
+ var heldInProcess = new Map;
18727
+ var holdRoot = (root) => heldInProcess.set(root, (heldInProcess.get(root) ?? 0) + 1);
18728
+ var dropRoot = (root) => {
18729
+ const count = (heldInProcess.get(root) ?? 1) - 1;
18730
+ if (count > 0)
18731
+ heldInProcess.set(root, count);
18732
+ else
18733
+ heldInProcess.delete(root);
18734
+ };
18735
+ var dedupeCandidates = (candidates) => {
18736
+ const last = new Map(candidates.map((candidate, index) => [candidate.id, index]));
18737
+ return candidates.filter((candidate, index) => last.get(candidate.id) === index);
18738
+ };
18528
18739
 
18529
18740
  class TaskStore {
18530
18741
  root;
18531
- constructor(root) {
18532
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
18742
+ lockTimeoutMs;
18743
+ constructor(root, options = {}) {
18744
+ this.root = fs5.existsSync(root) ? fs5.realpathSync(root) : path4.resolve(root);
18745
+ this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
18533
18746
  }
18534
18747
  readTask(taskId) {
18535
18748
  if (!validId(taskId))
@@ -18549,11 +18762,11 @@ class TaskStore {
18549
18762
  return result.result;
18550
18763
  }
18551
18764
  listTasks() {
18552
- if (!fs4.existsSync(this.tasksDir))
18765
+ if (!fs5.existsSync(this.tasksDir))
18553
18766
  return success(null, null, []);
18554
18767
  let names;
18555
18768
  try {
18556
- names = fs4.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18769
+ names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18557
18770
  } catch (error) {
18558
18771
  return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
18559
18772
  path: this.tasksDir
@@ -18582,11 +18795,11 @@ class TaskStore {
18582
18795
  return success(null, null, tasks);
18583
18796
  }
18584
18797
  listTaskIndex() {
18585
- if (!fs4.existsSync(this.tasksDir))
18798
+ if (!fs5.existsSync(this.tasksDir))
18586
18799
  return success(null, null, []);
18587
18800
  let names;
18588
18801
  try {
18589
- names = fs4.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18802
+ names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
18590
18803
  } catch (error) {
18591
18804
  return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
18592
18805
  path: this.tasksDir
@@ -18954,12 +19167,12 @@ class TaskStore {
18954
19167
  });
18955
19168
  }
18956
19169
  recoveryCandidates() {
18957
- if (!fs4.existsSync(this.recoveryDir))
19170
+ if (!fs5.existsSync(this.recoveryDir))
18958
19171
  return success(null, null, []);
18959
19172
  try {
18960
19173
  const candidates = [];
18961
- for (const name of fs4.readdirSync(this.recoveryDir)) {
18962
- const match = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/.exec(name);
19174
+ for (const name of fs5.readdirSync(this.recoveryDir)) {
19175
+ const match = RECOVERY_NAME.exec(name);
18963
19176
  if (match)
18964
19177
  candidates.push({
18965
19178
  target: match[1],
@@ -19007,7 +19220,7 @@ class TaskStore {
19007
19220
  return failure2("recovery_required", "validated recovery snapshot not found");
19008
19221
  selectedBytes = candidate;
19009
19222
  }
19010
- const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema) : this.parseBytes(selectedBytes, taskRecordSchema);
19223
+ const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
19011
19224
  if (!selectedRecord.ok)
19012
19225
  return selectedRecord;
19013
19226
  if (target === "workspace") {
@@ -19053,14 +19266,14 @@ class TaskStore {
19053
19266
  return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
19054
19267
  }
19055
19268
  if (target === "workspace") {
19056
- const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema);
19269
+ const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
19057
19270
  if (!parsed.ok)
19058
19271
  return parsed;
19059
19272
  const value = { ...parsed.data, revision: newRevision(), writer: null };
19060
19273
  const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
19061
19274
  return replaced.ok ? success(value.revision, value.revision, value) : replaced;
19062
19275
  }
19063
- const parsed = this.parseBytes(selectedBytes, taskRecordSchema);
19276
+ const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
19064
19277
  if (!parsed.ok)
19065
19278
  return parsed;
19066
19279
  const value = {
@@ -19071,7 +19284,7 @@ class TaskStore {
19071
19284
  };
19072
19285
  const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
19073
19286
  return replaced.ok ? success(value.revision, null, value) : replaced;
19074
- }, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate));
19287
+ }, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
19075
19288
  return result;
19076
19289
  } catch (error) {
19077
19290
  return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
@@ -19106,12 +19319,12 @@ class TaskStore {
19106
19319
  }
19107
19320
  readLockSnapshot() {
19108
19321
  try {
19109
- const stat = fs4.lstatSync(this.lockPath);
19322
+ const stat = fs5.lstatSync(this.lockPath);
19110
19323
  if (!stat.isFile() || stat.isSymbolicLink())
19111
19324
  return failure2("recovery_required", "metadata lock is not a regular file", {
19112
19325
  path: this.lockPath
19113
19326
  });
19114
- const raw = fs4.readFileSync(this.lockPath, "utf8");
19327
+ const raw = fs5.readFileSync(this.lockPath, "utf8");
19115
19328
  return success(null, null, { raw, data: parseMetadataLock(raw) });
19116
19329
  } catch (error) {
19117
19330
  if (error?.code === "ENOENT")
@@ -19122,22 +19335,50 @@ class TaskStore {
19122
19335
  }
19123
19336
  }
19124
19337
  metadataLockOptions() {
19338
+ const inProcess = heldInProcess.has(this.root);
19125
19339
  return {
19126
19340
  lockPath: this.lockPath,
19127
19341
  staleMs: Number.MAX_SAFE_INTEGER,
19128
- timeoutMs: 0,
19129
- retry: { retries: 0 },
19130
- staleRecovery: "fail-closed",
19131
- shouldReclaim: () => false,
19132
- parsePayload: parseMetadataLock,
19342
+ timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
19343
+ retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
19344
+ staleRecovery: "remove-if-unchanged",
19345
+ shouldReclaim: ({ payload, nowMs }) => {
19346
+ let ageMs = null;
19347
+ try {
19348
+ ageMs = nowMs - fs5.lstatSync(this.lockPath).mtimeMs;
19349
+ } catch {}
19350
+ return classifyLockOwner(payload, ageMs).state === "stale";
19351
+ },
19352
+ shouldRemoveStaleLock: () => true,
19353
+ parsePayload: parseMetadataLockOrNull,
19133
19354
  payload: () => ({
19134
19355
  pid: process.pid,
19135
- processStart: this.processStart(process.pid),
19136
- host: hostname(),
19356
+ processStart: processStartOf(process.pid),
19357
+ host: localLockHost(),
19137
19358
  nonce: randomUUID2()
19138
19359
  })
19139
19360
  };
19140
19361
  }
19362
+ acquireMetadataLock(options) {
19363
+ clearAbandonedReclaimGuard(this.lockPath);
19364
+ const deadline = Date.now() + (options.timeoutMs ?? 0);
19365
+ while (true) {
19366
+ try {
19367
+ return acquireFileLockSync(this.workspacePath, {
19368
+ ...options,
19369
+ timeoutMs: Math.max(0, deadline - Date.now())
19370
+ });
19371
+ } catch (error) {
19372
+ const code = error?.code;
19373
+ if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
19374
+ throw error;
19375
+ if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
19376
+ throw error;
19377
+ if (code !== "file_lock_stale")
19378
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
19379
+ }
19380
+ }
19381
+ }
19141
19382
  externalActionLockOptions() {
19142
19383
  const options = this.metadataLockOptions();
19143
19384
  return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
@@ -19155,10 +19396,11 @@ class TaskStore {
19155
19396
  }
19156
19397
  let handle;
19157
19398
  try {
19158
- handle = acquireFileLockSync(this.workspacePath, this.externalActionLockOptions());
19399
+ handle = this.acquireMetadataLock(this.externalActionLockOptions());
19159
19400
  } catch (error) {
19160
19401
  return this.lockFailure(error);
19161
19402
  }
19403
+ holdRoot(this.root);
19162
19404
  let result;
19163
19405
  try {
19164
19406
  if (!handle.verifyStillHeld())
@@ -19193,8 +19435,9 @@ class TaskStore {
19193
19435
  path: this.lockPath
19194
19436
  });
19195
19437
  }
19438
+ dropRoot(this.root);
19196
19439
  try {
19197
- handle.release();
19440
+ retryTransient(() => handle.release());
19198
19441
  } catch (error) {
19199
19442
  const released = this.lockFailure(error);
19200
19443
  const releaseError = released.ok ? "metadata lock release failed" : released.error;
@@ -19207,6 +19450,9 @@ class TaskStore {
19207
19450
  }
19208
19451
  recoveryLockOptions(observed, evidence, gate) {
19209
19452
  const options = this.metadataLockOptions();
19453
+ options.timeoutMs = 0;
19454
+ options.retry = { retries: 0 };
19455
+ options.parsePayload = parseMetadataLock;
19210
19456
  options.staleRecovery = "remove-if-unchanged";
19211
19457
  options.shouldReclaim = ({ payload }) => Boolean(observed && sameMetadataLock(payload, observed.data) && (evidence.state === "stopped" || evidence.state === "accounted_for"));
19212
19458
  options.shouldRemoveStaleLock = ({ raw, payload }) => {
@@ -19228,7 +19474,7 @@ class TaskStore {
19228
19474
  };
19229
19475
  this.replaceSnapshot(this.workspacePath, value, workspace);
19230
19476
  }
19231
- withLock(operation, options = this.metadataLockOptions()) {
19477
+ withLock(operation, options = this.metadataLockOptions(), contention = "busy") {
19232
19478
  try {
19233
19479
  this.initializeMutationStorage();
19234
19480
  } catch (error) {
@@ -19239,10 +19485,12 @@ class TaskStore {
19239
19485
  let handle;
19240
19486
  let result = failure2("storage_error", "metadata lock operation did not produce a result");
19241
19487
  try {
19242
- handle = acquireFileLockSync(this.workspacePath, options);
19488
+ handle = this.acquireMetadataLock(options);
19243
19489
  } catch (error) {
19244
- result = this.lockFailure(error);
19490
+ result = this.lockFailure(error, contention);
19245
19491
  }
19492
+ if (handle)
19493
+ holdRoot(this.root);
19246
19494
  if (handle) {
19247
19495
  try {
19248
19496
  if (!handle.verifyStillHeld())
@@ -19261,8 +19509,9 @@ class TaskStore {
19261
19509
  }
19262
19510
  }
19263
19511
  if (handle) {
19512
+ dropRoot(this.root);
19264
19513
  try {
19265
- handle.release();
19514
+ retryTransient(() => handle.release());
19266
19515
  } catch (error) {
19267
19516
  const releaseFailure = this.lockFailure(error);
19268
19517
  const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
@@ -19271,25 +19520,65 @@ class TaskStore {
19271
19520
  }
19272
19521
  return result;
19273
19522
  }
19274
- lockFailure(error) {
19523
+ lockDirWritable() {
19524
+ const probe = `${this.lockPath}.${process.pid}.${randomUUID2()}.probe`;
19525
+ try {
19526
+ fs5.closeSync(fs5.openSync(probe, "wx", 384));
19527
+ } catch {
19528
+ return false;
19529
+ }
19530
+ try {
19531
+ fs5.rmSync(probe, { force: true });
19532
+ } catch {}
19533
+ return true;
19534
+ }
19535
+ lockHolderPresent() {
19536
+ try {
19537
+ fs5.lstatSync(this.lockPath);
19538
+ return true;
19539
+ } catch (error) {
19540
+ return error?.code !== "ENOENT";
19541
+ }
19542
+ }
19543
+ lockFailure(error, contention = "busy") {
19275
19544
  const value = error;
19276
19545
  const code = typeof value?.code === "string" ? value.code : "";
19277
- if (code === "EEXIST" || code === "file_lock_timeout") {
19278
- const lock = this.readLockSnapshot();
19279
- if (lock.ok && lock.data?.data.externalAction)
19546
+ if (isTransientWindowsError(error) && !this.lockHolderPresent() && !this.lockDirWritable())
19547
+ return failure2("storage_error", `cannot create the workspace metadata lock (${code}); no other Workit call holds it`, {
19548
+ path: this.lockPath,
19549
+ guidance: `Check permissions and the read-only attribute on ${path4.dirname(this.lockPath)}.`
19550
+ });
19551
+ if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
19552
+ let lock = null;
19553
+ try {
19554
+ lock = parseMetadataLockOrNull(fs5.readFileSync(this.lockPath, "utf8"));
19555
+ } catch {}
19556
+ if (lock?.externalAction)
19280
19557
  return failure2("writer_conflict", "workspace is reserved by a managed external action", {
19281
19558
  outcome: "not_started",
19282
19559
  path: this.lockPath
19283
19560
  });
19561
+ if (contention === "busy")
19562
+ return failure2("busy", `workspace metadata lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
19563
+ outcome: "not_started",
19564
+ path: this.lockPath,
19565
+ guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
19566
+ });
19284
19567
  }
19285
19568
  const recovery = code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
19286
19569
  return failure2(recovery ? "recovery_required" : "storage_error", `metadata lock operation failed: ${String(error)}`, { path: this.lockPath });
19287
19570
  }
19288
19571
  initializeMutationStorage() {
19289
- fs4.mkdirSync(this.tasksDir, { recursive: true });
19290
- fs4.mkdirSync(this.recoveryDir, { recursive: true });
19291
- fs4.writeFileSync(this.gitignorePath, `*
19292
- `);
19572
+ retryTransient(() => fs5.mkdirSync(this.tasksDir, { recursive: true }));
19573
+ retryTransient(() => fs5.mkdirSync(this.recoveryDir, { recursive: true }));
19574
+ let current = null;
19575
+ try {
19576
+ current = retryTransient(() => fs5.readFileSync(this.gitignorePath, "utf8"));
19577
+ } catch {}
19578
+ if (current !== `*
19579
+ `)
19580
+ retryTransient(() => fs5.writeFileSync(this.gitignorePath, `*
19581
+ `));
19293
19582
  }
19294
19583
  replaceSnapshot(file, value, previous) {
19295
19584
  let temporary;
@@ -19301,14 +19590,14 @@ class TaskStore {
19301
19590
  else if (typeof previous === "string")
19302
19591
  this.saveRecovery(file, previous);
19303
19592
  temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
19304
- const fd = fs4.openSync(temporary, "wx", 384);
19593
+ const fd = fs5.openSync(temporary, "wx", 384);
19305
19594
  try {
19306
- fs4.writeSync(fd, jsonBytes(value));
19307
- fs4.fsyncSync(fd);
19595
+ fs5.writeSync(fd, jsonBytes(value));
19596
+ fs5.fsyncSync(fd);
19308
19597
  } finally {
19309
- fs4.closeSync(fd);
19598
+ fs5.closeSync(fd);
19310
19599
  }
19311
- fs4.renameSync(temporary, file);
19600
+ retryTransient(() => fs5.renameSync(temporary, file));
19312
19601
  temporary = undefined;
19313
19602
  this.fsyncDirectory(path4.dirname(file));
19314
19603
  if (path4.dirname(file) === this.tasksDir)
@@ -19321,13 +19610,13 @@ class TaskStore {
19321
19610
  } finally {
19322
19611
  if (temporary)
19323
19612
  try {
19324
- fs4.unlinkSync(temporary);
19613
+ fs5.unlinkSync(temporary);
19325
19614
  } catch {}
19326
19615
  }
19327
19616
  }
19328
19617
  readIndex(workspaceId) {
19329
19618
  try {
19330
- const value = JSON.parse(fs4.readFileSync(this.indexPath, "utf8"));
19619
+ const value = JSON.parse(fs5.readFileSync(this.indexPath, "utf8"));
19331
19620
  if (!isObject2(value) || value.version !== INDEX_VERSION || value.workspaceId !== workspaceId || !isObject2(value.tasks))
19332
19621
  return {};
19333
19622
  const tasks = {};
@@ -19342,15 +19631,15 @@ class TaskStore {
19342
19631
  writeIndex(workspaceId, entries) {
19343
19632
  const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
19344
19633
  try {
19345
- fs4.writeFileSync(temporary, JSON.stringify({
19634
+ fs5.writeFileSync(temporary, JSON.stringify({
19346
19635
  version: INDEX_VERSION,
19347
19636
  workspaceId,
19348
19637
  tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
19349
19638
  }), { mode: 384, flag: "wx" });
19350
- fs4.renameSync(temporary, this.indexPath);
19639
+ retryTransient(() => fs5.renameSync(temporary, this.indexPath));
19351
19640
  } catch {
19352
19641
  try {
19353
- fs4.unlinkSync(temporary);
19642
+ fs5.unlinkSync(temporary);
19354
19643
  } catch {}
19355
19644
  }
19356
19645
  }
@@ -19368,33 +19657,153 @@ class TaskStore {
19368
19657
  const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
19369
19658
  const id = target === "task" ? path4.basename(file, ".json") : "workspace";
19370
19659
  const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
19371
- if (fs4.existsSync(destination)) {
19372
- if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
19373
- return;
19374
- throw new Error("recovery copy already exists with different bytes");
19660
+ if (fs5.existsSync(destination)) {
19661
+ if (digestBytes(retryTransient(() => fs5.readFileSync(destination))) !== digestBytes(bytes))
19662
+ throw new Error("recovery copy already exists with different bytes");
19663
+ const stamp = new Date;
19664
+ retryTransient(() => fs5.utimesSync(destination, stamp, stamp));
19665
+ this.pruneRecovery(`${target}.${id}.`, destination);
19666
+ return;
19375
19667
  }
19376
19668
  let temporary;
19377
19669
  try {
19378
19670
  temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
19379
- const fd = fs4.openSync(temporary, "wx", 384);
19671
+ const fd = fs5.openSync(temporary, "wx", 384);
19380
19672
  try {
19381
- fs4.writeSync(fd, bytes);
19382
- fs4.fsyncSync(fd);
19673
+ fs5.writeSync(fd, bytes);
19674
+ fs5.fsyncSync(fd);
19383
19675
  } finally {
19384
- fs4.closeSync(fd);
19676
+ fs5.closeSync(fd);
19385
19677
  }
19386
- fs4.renameSync(temporary, destination);
19678
+ retryTransient(() => fs5.renameSync(temporary, destination));
19387
19679
  temporary = undefined;
19388
19680
  this.fsyncDirectory(this.recoveryDir);
19681
+ this.pruneRecovery(`${target}.${id}.`, destination);
19389
19682
  } finally {
19390
19683
  if (temporary)
19391
19684
  try {
19392
- fs4.unlinkSync(temporary);
19685
+ fs5.unlinkSync(temporary);
19393
19686
  } catch {}
19394
19687
  }
19395
19688
  }
19689
+ pruneRecovery(prefix, keep) {
19690
+ try {
19691
+ const copies = this.recoveryCopies(prefix);
19692
+ const surplus = this.surplusCopies(copies, path4.basename(keep));
19693
+ for (const copy of surplus)
19694
+ fs5.rmSync(copy.path, { force: true });
19695
+ } catch {}
19696
+ }
19697
+ recoveryCopies(prefix = "") {
19698
+ if (!fs5.existsSync(this.recoveryDir))
19699
+ return [];
19700
+ const copies = [];
19701
+ for (const name of fs5.readdirSync(this.recoveryDir)) {
19702
+ if (!name.startsWith(prefix))
19703
+ continue;
19704
+ const match = RECOVERY_NAME.exec(name);
19705
+ if (!match)
19706
+ continue;
19707
+ const file = path4.join(this.recoveryDir, name);
19708
+ try {
19709
+ const stat = fs5.lstatSync(file, { bigint: true });
19710
+ if (!stat.isFile())
19711
+ continue;
19712
+ copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
19713
+ } catch {}
19714
+ }
19715
+ return copies;
19716
+ }
19717
+ surplusCopies(copies, keep) {
19718
+ const ordered = copies.toSorted((left, right) => left.name === keep ? -1 : right.name === keep ? 1 : left.mtimeNs === right.mtimeNs ? left.name.localeCompare(right.name) : left.mtimeNs > right.mtimeNs ? -1 : 1);
19719
+ return ordered.slice(RECOVERY_COPIES_PER_RECORD);
19720
+ }
19721
+ collectGarbage(options = {}) {
19722
+ const dryRun = options.dryRun === true;
19723
+ const report = {
19724
+ dryRun,
19725
+ recovery: { removed: 0, removedBytes: 0, kept: 0 },
19726
+ temporary: { removed: 0 },
19727
+ candidates: { removed: 0, tasks: [], skippedActive: [], skippedClosed: [], failed: [] }
19728
+ };
19729
+ if (!fs5.existsSync(this.workitDir))
19730
+ return success(null, null, report);
19731
+ const sweep = () => {
19732
+ try {
19733
+ const groups = new Map;
19734
+ for (const copy of this.recoveryCopies())
19735
+ groups.set(copy.group, [...groups.get(copy.group) ?? [], copy]);
19736
+ for (const copies of groups.values()) {
19737
+ const surplus = this.surplusCopies(copies);
19738
+ report.recovery.kept += copies.length - surplus.length;
19739
+ for (const copy of surplus) {
19740
+ report.recovery.removed += 1;
19741
+ try {
19742
+ report.recovery.removedBytes += fs5.statSync(copy.path).size;
19743
+ } catch {}
19744
+ if (!dryRun)
19745
+ fs5.rmSync(copy.path, { force: true });
19746
+ }
19747
+ }
19748
+ const nowMs = Date.now();
19749
+ for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
19750
+ if (!fs5.existsSync(directory))
19751
+ continue;
19752
+ for (const name of fs5.readdirSync(directory)) {
19753
+ if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
19754
+ continue;
19755
+ const file = path4.join(directory, name);
19756
+ const stat = fs5.lstatSync(file);
19757
+ if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
19758
+ continue;
19759
+ report.temporary.removed += 1;
19760
+ if (!dryRun)
19761
+ fs5.rmSync(file, { force: true });
19762
+ }
19763
+ }
19764
+ return success(null, null, null);
19765
+ } catch (error) {
19766
+ return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
19767
+ path: this.recoveryDir
19768
+ });
19769
+ }
19770
+ };
19771
+ const pruned = dryRun ? sweep() : this.withLock(sweep);
19772
+ if (!pruned.ok)
19773
+ return pruned;
19774
+ const tasks = this.listTasks();
19775
+ if (!tasks.ok)
19776
+ return tasks;
19777
+ for (const task of tasks.data) {
19778
+ const deduped = dedupeCandidates(task.candidates);
19779
+ const removed = task.candidates.length - deduped.length;
19780
+ if (removed === 0)
19781
+ continue;
19782
+ if (task.status === "active") {
19783
+ report.candidates.skippedActive.push(task.id);
19784
+ continue;
19785
+ }
19786
+ if (task.status === "closed") {
19787
+ report.candidates.skippedClosed.push(task.id);
19788
+ continue;
19789
+ }
19790
+ if (!dryRun) {
19791
+ const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
19792
+ ...current,
19793
+ candidates: dedupeCandidates(current.candidates)
19794
+ }));
19795
+ if (!written.ok) {
19796
+ report.candidates.failed.push(task.id);
19797
+ continue;
19798
+ }
19799
+ }
19800
+ report.candidates.removed += removed;
19801
+ report.candidates.tasks.push(task.id);
19802
+ }
19803
+ return success(null, null, report);
19804
+ }
19396
19805
  findRecovery(target, taskId, digest, workspace) {
19397
- if (!fs4.existsSync(this.recoveryDir))
19806
+ if (!fs5.existsSync(this.recoveryDir))
19398
19807
  return null;
19399
19808
  const workspaceId = workspace?.id ?? this.trustedWorkspaceId();
19400
19809
  if (target === "workspace" && !workspaceId)
@@ -19406,9 +19815,9 @@ class TaskStore {
19406
19815
  if (candidate.target !== target || candidate.digest !== digest)
19407
19816
  continue;
19408
19817
  try {
19409
- if (!fs4.lstatSync(candidate.path).isFile())
19818
+ if (!fs5.lstatSync(candidate.path).isFile())
19410
19819
  continue;
19411
- const bytes = fs4.readFileSync(candidate.path);
19820
+ const bytes = fs5.readFileSync(candidate.path);
19412
19821
  if (digestBytes(bytes) !== digest)
19413
19822
  continue;
19414
19823
  if (target === "workspace") {
@@ -19426,11 +19835,11 @@ class TaskStore {
19426
19835
  }
19427
19836
  fsyncDirectory(directory) {
19428
19837
  try {
19429
- const fd = fs4.openSync(directory, "r");
19838
+ const fd = fs5.openSync(directory, "r");
19430
19839
  try {
19431
- fs4.fsyncSync(fd);
19840
+ fs5.fsyncSync(fd);
19432
19841
  } finally {
19433
- fs4.closeSync(fd);
19842
+ fs5.closeSync(fd);
19434
19843
  }
19435
19844
  } catch (error) {
19436
19845
  if (process.platform !== "win32")
@@ -19438,14 +19847,14 @@ class TaskStore {
19438
19847
  }
19439
19848
  }
19440
19849
  trustedWorkspaceId() {
19441
- if (!fs4.existsSync(this.tasksDir))
19850
+ if (!fs5.existsSync(this.tasksDir))
19442
19851
  return null;
19443
19852
  const ids = new Set;
19444
19853
  try {
19445
- for (const name of fs4.readdirSync(this.tasksDir)) {
19854
+ for (const name of fs5.readdirSync(this.tasksDir)) {
19446
19855
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
19447
19856
  return null;
19448
- const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
19857
+ const parsed = this.parseBytes(fs5.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
19449
19858
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
19450
19859
  return null;
19451
19860
  ids.add(parsed.data.workspaceId);
@@ -19457,14 +19866,14 @@ class TaskStore {
19457
19866
  }
19458
19867
  snapshotBytes(file) {
19459
19868
  try {
19460
- return fs4.readFileSync(file);
19869
+ return retryTransient(() => fs5.readFileSync(file));
19461
19870
  } catch {
19462
19871
  return null;
19463
19872
  }
19464
19873
  }
19465
19874
  readRecord(file, schema) {
19466
19875
  try {
19467
- const bytes = fs4.readFileSync(file, "utf8");
19876
+ const bytes = retryTransient(() => fs5.readFileSync(file, "utf8"));
19468
19877
  return { exists: true, result: this.parseBytes(bytes, schema) };
19469
19878
  } catch (error) {
19470
19879
  if (error?.code === "ENOENT")
@@ -19477,7 +19886,7 @@ class TaskStore {
19477
19886
  };
19478
19887
  }
19479
19888
  }
19480
- parseBytes(bytes, schema) {
19889
+ parseBytes(bytes, schema, mode = "read") {
19481
19890
  let value;
19482
19891
  try {
19483
19892
  value = JSON.parse(typeof bytes === "string" ? bytes : bytes.toString("utf8"));
@@ -19486,21 +19895,19 @@ class TaskStore {
19486
19895
  }
19487
19896
  if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
19488
19897
  return failure2("unsupported_version", "unsupported snapshot schema version");
19489
- const parsed = schema.safeParse(value);
19490
- if (parsed.success)
19898
+ const parsed = parseStoredRecord(schema, value);
19899
+ if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
19491
19900
  return success(null, null, parsed.data);
19492
19901
  const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
19902
+ const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
19903
+ if (parsed.success)
19904
+ return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
19905
+ if (parsed.critical.length > 0)
19906
+ return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
19493
19907
  if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
19494
19908
  return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
19495
19909
  return failure2("recovery_required", "snapshot does not satisfy its schema");
19496
19910
  }
19497
- processStart(pid) {
19498
- try {
19499
- return fs4.readFileSync(`/proc/${pid}/stat`, "utf8").split(" ")[21] ?? null;
19500
- } catch {
19501
- return null;
19502
- }
19503
- }
19504
19911
  conflict(expected, actual) {
19505
19912
  return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
19506
19913
  expectedRevision: expected,
@@ -19532,55 +19939,118 @@ class TaskStore {
19532
19939
  return path4.join(this.workitDir, ".gitignore");
19533
19940
  }
19534
19941
  }
19535
- // packages/workit-core/src/core/task-engine.ts
19536
- import { realpathSync as realpathSync6 } from "node:fs";
19537
-
19538
- // packages/workit-core/src/core/authority.ts
19539
- import { createHash as createHash5 } from "node:crypto";
19540
- import * as fs6 from "node:fs";
19541
- import path9 from "node:path";
19542
-
19543
- // packages/workit-core/src/core/auto-approval.ts
19544
- import { createHash as createHash4 } from "node:crypto";
19545
- import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
19546
- import path8 from "node:path";
19547
-
19548
- // packages/workit-core/src/core/workspaces.ts
19549
- import { createHash as createHash3 } from "node:crypto";
19550
- import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
19551
- import path6 from "node:path";
19552
-
19553
- // packages/workit-core/src/core/config.ts
19554
- import {
19555
- copyFileSync,
19556
- cpSync,
19557
- existsSync as existsSync3,
19558
- mkdirSync as mkdirSync2,
19559
- readFileSync as readFileSync2,
19560
- readdirSync as readdirSync2,
19561
- writeFileSync as writeFileSync2
19562
- } from "node:fs";
19563
- import os from "node:os";
19564
- import path5 from "node:path";
19565
-
19566
- // packages/workit-core/src/core/boundary.ts
19567
- import { statSync as statSync2 } from "node:fs";
19568
- var EVENT = {
19569
- initialization: "initialization",
19570
- provenance: "provenance",
19571
- assets: "assets",
19572
- configurationSource: "configuration_source",
19573
- hooks: "hooks",
19574
- mcpConnection: "mcp_connection",
19575
- migration: "migration",
19576
- installSteps: "install_steps",
19577
- uncaughtFailure: "uncaught_failure",
19578
- toolsFailed: "tools_failed",
19579
- doctor: "doctor"
19580
- };
19581
- var errorDetail = (err) => {
19582
- if (err instanceof Error) {
19583
- return { error_name: err.name, error: err.message };
19942
+ // packages/workit-core/src/core/workers.ts
19943
+ var same = (left, right) => {
19944
+ try {
19945
+ return canonicalJson(left) === canonicalJson(right);
19946
+ } catch {
19947
+ return false;
19948
+ }
19949
+ };
19950
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
19951
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
19952
+ var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
19953
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
19954
+ var validPath = (value) => typeof value === "string" && value.length > 0 && !Array.from(value).some((char) => {
19955
+ const code = char.charCodeAt(0);
19956
+ return code < 32 || code === 127;
19957
+ }) && !value.split(/[\\/]/).includes("..");
19958
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
19959
+ var workerIdOf = (value) => value.workerId ?? null;
19960
+ var currentWriterOwnsTask = (task, workspace, caller) => {
19961
+ const owner = workspace.writer?.owner;
19962
+ return task.status === "active" && task.workspaceId === workspace.id && owner?.taskId === task.id && owner.session.host === caller.host && owner.session.handle === caller.actor;
19963
+ };
19964
+ function assertProductWriteAllowed(input) {
19965
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
19966
+ return failure2("invalid_input", "invalid product write path");
19967
+ if (!input.store)
19968
+ return failure2("permission_denied", "write authorization requires core state");
19969
+ const task = input.store.readTask(input.task.id);
19970
+ if (!task.ok)
19971
+ return task;
19972
+ const workspace = input.store.readWorkspace();
19973
+ if (!workspace.ok)
19974
+ return workspace;
19975
+ if (!workspace.data)
19976
+ return failure2("not_found", "workspace not found");
19977
+ const workspaceRecord = workspace.data;
19978
+ if (task.data.status !== "active")
19979
+ return failure2("invalid_transition", "paused or closed tasks cannot own product writes");
19980
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
19981
+ return failure2("recovery_required", "task and workspace bindings are invalid");
19982
+ const writer = workspaceRecord.writer;
19983
+ if (!writer)
19984
+ return failure2("permission_denied", "checkout has no writer owner");
19985
+ if (writer.state === "uncertain")
19986
+ return failure2("recovery_required", "checkout writer requires recovery", {
19987
+ owner: writer.owner
19988
+ });
19989
+ const caller = callerValue(input.caller);
19990
+ const workerId = workerIdOf(input.caller);
19991
+ const owner = writer.owner;
19992
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
19993
+ return failure2("writer_conflict", "checkout is owned by another validated actor", { owner });
19994
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
19995
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
19996
+ return failure2("permission_denied", "only implementers can own product writes");
19997
+ 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))))
19998
+ return failure2("recovery_required", "worker session is not an active writer");
19999
+ return success(null, workspaceRecord.revision, owner);
20000
+ }
20001
+
20002
+ // packages/workit-core/src/core/task-evaluation.ts
20003
+ import { createHash as createHash6 } from "node:crypto";
20004
+ import * as fs8 from "node:fs";
20005
+ import { spawnSync as spawnSync3 } from "node:child_process";
20006
+ import path10 from "node:path";
20007
+
20008
+ // packages/workit-core/src/core/authority.ts
20009
+ import { createHash as createHash5 } from "node:crypto";
20010
+ import * as fs7 from "node:fs";
20011
+ import path9 from "node:path";
20012
+
20013
+ // packages/workit-core/src/core/auto-approval.ts
20014
+ import { createHash as createHash4 } from "node:crypto";
20015
+ import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
20016
+ import path8 from "node:path";
20017
+
20018
+ // packages/workit-core/src/core/workspaces.ts
20019
+ import { createHash as createHash3 } from "node:crypto";
20020
+ import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
20021
+ import path6 from "node:path";
20022
+
20023
+ // packages/workit-core/src/core/config.ts
20024
+ import {
20025
+ copyFileSync,
20026
+ cpSync,
20027
+ existsSync as existsSync3,
20028
+ mkdirSync as mkdirSync2,
20029
+ readFileSync as readFileSync2,
20030
+ readdirSync as readdirSync2,
20031
+ writeFileSync as writeFileSync2
20032
+ } from "node:fs";
20033
+ import os from "node:os";
20034
+ import path5 from "node:path";
20035
+
20036
+ // packages/workit-core/src/core/boundary.ts
20037
+ import { statSync as statSync2 } from "node:fs";
20038
+ var EVENT = {
20039
+ initialization: "initialization",
20040
+ provenance: "provenance",
20041
+ assets: "assets",
20042
+ configurationSource: "configuration_source",
20043
+ hooks: "hooks",
20044
+ mcpConnection: "mcp_connection",
20045
+ migration: "migration",
20046
+ installSteps: "install_steps",
20047
+ uncaughtFailure: "uncaught_failure",
20048
+ toolsFailed: "tools_failed",
20049
+ doctor: "doctor"
20050
+ };
20051
+ var errorDetail = (err) => {
20052
+ if (err instanceof Error) {
20053
+ return { error_name: err.name, error: err.message };
19584
20054
  }
19585
20055
  return { error_name: "unknown", error: String(err) };
19586
20056
  };
@@ -20207,13 +20677,13 @@ var gitContext = (workspaceRoot, paths = []) => {
20207
20677
  };
20208
20678
 
20209
20679
  // packages/workit-core/src/core/vcs-config.ts
20210
- import fs5 from "node:fs";
20680
+ import fs6 from "node:fs";
20211
20681
  import path7 from "node:path";
20212
- import { spawnSync } from "node:child_process";
20682
+ import { spawnSync as spawnSync2 } from "node:child_process";
20213
20683
  var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
20214
20684
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
20215
20685
  var remoteProvider = (cwd) => {
20216
- const r = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
20686
+ const r = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
20217
20687
  if (r.status !== 0)
20218
20688
  return null;
20219
20689
  const url = (r.stdout ?? "").trim();
@@ -20250,7 +20720,7 @@ var remoteEndpoint = (remote) => {
20250
20720
  var configuredSshEndpoint = (host) => {
20251
20721
  if (host.startsWith("-"))
20252
20722
  return null;
20253
- const resolved = spawnSync("ssh", ["-G", host], {
20723
+ const resolved = spawnSync2("ssh", ["-G", host], {
20254
20724
  encoding: "utf8",
20255
20725
  timeout: 5000,
20256
20726
  maxBuffer: 1024 * 1024,
@@ -20300,7 +20770,7 @@ var readVcsConfig = () => {
20300
20770
  const file = vcsConfigPath();
20301
20771
  let raw;
20302
20772
  try {
20303
- raw = fs5.readFileSync(file, "utf8");
20773
+ raw = fs6.readFileSync(file, "utf8");
20304
20774
  } catch {
20305
20775
  return { status: "missing", path: file, config: {} };
20306
20776
  }
@@ -20401,7 +20871,7 @@ function vcsCliIdentity(cwd) {
20401
20871
  PATH: process.env.PATH ?? "",
20402
20872
  ...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
20403
20873
  };
20404
- const result = spawnSync(bin, ["api", "user"], {
20874
+ const result = spawnSync2(bin, ["api", "user"], {
20405
20875
  cwd: root,
20406
20876
  encoding: "utf8",
20407
20877
  env
@@ -20443,7 +20913,7 @@ function mergedPrStyle(limit = 6, cwd) {
20443
20913
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
20444
20914
  descriptionPreview: desc.slice(0, 600)
20445
20915
  });
20446
- const remoteResult = spawnSync("git", ["remote", "get-url", "--push", "--all", "origin"], {
20916
+ const remoteResult = spawnSync2("git", ["remote", "get-url", "--push", "--all", "origin"], {
20447
20917
  cwd,
20448
20918
  encoding: "utf8"
20449
20919
  });
@@ -20459,7 +20929,7 @@ function mergedPrStyle(limit = 6, cwd) {
20459
20929
  if (!hostingApiHostMatches(remote, host))
20460
20930
  return { ok: false, error: "PR style target does not match the provider host" };
20461
20931
  if (provider === "gitlab") {
20462
- const run = (args) => spawnSync("glab", ["api", ...args], {
20932
+ const run = (args) => spawnSync2("glab", ["api", ...args], {
20463
20933
  cwd,
20464
20934
  encoding: "utf8",
20465
20935
  env
@@ -20479,7 +20949,7 @@ function mergedPrStyle(limit = 6, cwd) {
20479
20949
  });
20480
20950
  }
20481
20951
  } else if (provider === "github") {
20482
- const r = spawnSync("gh", [
20952
+ const r = spawnSync2("gh", [
20483
20953
  "pr",
20484
20954
  "list",
20485
20955
  "--repo",
@@ -20538,66 +21008,6 @@ var verifyPushIdentity = (root, provider, account) => {
20538
21008
  return { ok: true };
20539
21009
  };
20540
21010
 
20541
- // packages/workit-core/src/core/workers.ts
20542
- var same = (left, right) => {
20543
- try {
20544
- return canonicalJson(left) === canonicalJson(right);
20545
- } catch {
20546
- return false;
20547
- }
20548
- };
20549
- var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
20550
- var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
20551
- var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
20552
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
20553
- var validPath = (value) => typeof value === "string" && value.length > 0 && !Array.from(value).some((char) => {
20554
- const code = char.charCodeAt(0);
20555
- return code < 32 || code === 127;
20556
- }) && !value.split(/[\\/]/).includes("..");
20557
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
20558
- var workerIdOf = (value) => value.workerId ?? null;
20559
- var currentWriterOwnsTask = (task, workspace, caller) => {
20560
- const owner = workspace.writer?.owner;
20561
- return task.status === "active" && task.workspaceId === workspace.id && owner?.taskId === task.id && owner.session.host === caller.host && owner.session.handle === caller.actor;
20562
- };
20563
- function assertProductWriteAllowed(input) {
20564
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
20565
- return failure2("invalid_input", "invalid product write path");
20566
- if (!input.store)
20567
- return failure2("permission_denied", "write authorization requires core state");
20568
- const task = input.store.readTask(input.task.id);
20569
- if (!task.ok)
20570
- return task;
20571
- const workspace = input.store.readWorkspace();
20572
- if (!workspace.ok)
20573
- return workspace;
20574
- if (!workspace.data)
20575
- return failure2("not_found", "workspace not found");
20576
- const workspaceRecord = workspace.data;
20577
- if (task.data.status !== "active")
20578
- return failure2("invalid_transition", "paused or closed tasks cannot own product writes");
20579
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
20580
- return failure2("recovery_required", "task and workspace bindings are invalid");
20581
- const writer = workspaceRecord.writer;
20582
- if (!writer)
20583
- return failure2("permission_denied", "checkout has no writer owner");
20584
- if (writer.state === "uncertain")
20585
- return failure2("recovery_required", "checkout writer requires recovery", {
20586
- owner: writer.owner
20587
- });
20588
- const caller = callerValue(input.caller);
20589
- const workerId = workerIdOf(input.caller);
20590
- const owner = writer.owner;
20591
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
20592
- return failure2("writer_conflict", "checkout is owned by another validated actor", { owner });
20593
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
20594
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
20595
- return failure2("permission_denied", "only implementers can own product writes");
20596
- if (workerId !== null && (!worker?.data.session || worker.data.state === "cancelling" || worker.data.state === "unknown" || worker.data.state === "stopped" || !same(worker.data.session, callerSession(input.caller))))
20597
- return failure2("recovery_required", "worker session is not an active writer");
20598
- return success(null, workspaceRecord.revision, owner);
20599
- }
20600
-
20601
21011
  // packages/workit-core/src/core/auto-approval.ts
20602
21012
  var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
20603
21013
  var OPERATION_CLASS = {
@@ -20884,7 +21294,7 @@ var digestBytes2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
20884
21294
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
20885
21295
  let root;
20886
21296
  try {
20887
- root = fs6.realpathSync(checkoutRoot);
21297
+ root = fs7.realpathSync(checkoutRoot);
20888
21298
  } catch {
20889
21299
  return failure2("permission_denied", "checkout root is unavailable");
20890
21300
  }
@@ -20907,8 +21317,8 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
20907
21317
  if (!segment)
20908
21318
  continue;
20909
21319
  current = path9.join(current, segment);
20910
- const stat = fs6.lstatSync(current);
20911
- const real = fs6.realpathSync(current);
21320
+ const stat = fs7.lstatSync(current);
21321
+ const real = fs7.realpathSync(current);
20912
21322
  if (real !== root && !real.startsWith(`${root}${path9.sep}`))
20913
21323
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
20914
21324
  fields: [
@@ -20925,7 +21335,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
20925
21335
  if (current !== target && !stat.isDirectory())
20926
21336
  return failure2("invalid_input", "document reference ancestor is not a directory");
20927
21337
  }
20928
- if (digestBytes2(fs6.readFileSync(target)) !== reference.digest)
21338
+ if (digestBytes2(fs7.readFileSync(target)) !== reference.digest)
20929
21339
  return failure2("permission_denied", "approved document bytes have changed");
20930
21340
  } catch {
20931
21341
  return failure2("invalid_input", "approved document is unavailable");
@@ -21288,18 +21698,41 @@ function reconcileAction(input) {
21288
21698
  var bindingCovers = scopeCovers;
21289
21699
  var verifyDecisionContent = verifyContentRefs;
21290
21700
 
21291
- // packages/workit-core/src/core/policy-resolver.ts
21292
- var resolverInputSchema = object2({
21293
- intent: intentSchema,
21294
- assessment: assessmentSchema,
21295
- constraints: array(constraintSchema),
21296
- prior: object2({
21297
- decisions: array(decisionSchema),
21298
- findings: array(findingSchema),
21299
- requirements: array(requirementSchema)
21300
- }).strict(),
21301
- preferences: object2({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
21302
- }).strict();
21701
+ // packages/workit-core/src/core/task-evaluation.ts
21702
+ var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
21703
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
21704
+ var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
21705
+ var canonicalPath = (value) => {
21706
+ if (typeof value !== "string" || !value)
21707
+ return null;
21708
+ if (value.includes("\\") || value.split("/").includes(".."))
21709
+ return null;
21710
+ const normalized = path10.posix.normalize(value);
21711
+ if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
21712
+ return null;
21713
+ return normalized === "." ? "." : normalized.replace(/\/$/, "");
21714
+ };
21715
+ var canonicalScope = (scope) => {
21716
+ const paths = scope.paths.length ? scope.paths : ["."];
21717
+ const normalizedPaths = paths.map(canonicalPath);
21718
+ const normalizedExclusions = scope.exclusions.map(canonicalPath);
21719
+ if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
21720
+ return null;
21721
+ return {
21722
+ description: scope.description,
21723
+ paths: [...new Set(normalizedPaths)],
21724
+ exclusions: [...new Set(normalizedExclusions)]
21725
+ };
21726
+ };
21727
+ var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
21728
+ var scopeMatches = (value, scope) => {
21729
+ const normalized = canonicalScope(scope);
21730
+ if (!normalized)
21731
+ return false;
21732
+ const paths = normalized.paths;
21733
+ return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
21734
+ };
21735
+ var pathRelevant = (value, scope) => scopeMatches(value, scope);
21303
21736
  var compareCodeUnits2 = (left, right) => {
21304
21737
  const length = Math.min(left.length, right.length);
21305
21738
  for (let index = 0;index < length; index += 1) {
@@ -21309,362 +21742,8 @@ var compareCodeUnits2 = (left, right) => {
21309
21742
  }
21310
21743
  return left.length - right.length;
21311
21744
  };
21312
- var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits2);
21313
- var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits2(canonicalJson(left), canonicalJson(right)));
21314
- var normalizedScope = (scope) => ({
21315
- description: scope.description,
21316
- paths: stableUnique(scope.paths),
21317
- exclusions: stableUnique(scope.exclusions)
21318
- });
21319
- var normalizedRefList = (values) => stableList(values);
21320
- var normalizeAssessment = (assessment) => ({
21321
- facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
21322
- signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
21323
- name,
21324
- { ...signal, refs: normalizedRefList(signal.refs) }
21325
- ])),
21326
- consequences: stableList(assessment.consequences.map((consequence) => ({
21327
- ...consequence,
21328
- fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
21329
- }))),
21330
- verification: stableList(assessment.verification.map((verification) => ({
21331
- ...verification,
21332
- scope: normalizedScope(verification.scope),
21333
- availableChecks: normalizedRefList(verification.availableChecks),
21334
- gaps: stableUnique(verification.gaps)
21335
- })))
21336
- });
21337
- var normalizeConstraint = (constraint) => ({
21338
- ...constraint,
21339
- source: constraint.source,
21340
- requires: stableList(constraint.requires.map((requirement) => ({
21341
- ...requirement,
21342
- scope: normalizedScope(requirement.scope),
21343
- refs: normalizedRefList(requirement.refs)
21344
- })))
21345
- });
21346
- var normalizeDecision = (decision) => ({
21347
- ...decision,
21348
- binding: {
21349
- ...decision.binding,
21350
- scope: normalizedScope(decision.binding.scope),
21351
- contentRefs: normalizedRefList(decision.binding.contentRefs)
21352
- },
21353
- requirementIds: stableUnique(decision.requirementIds)
21354
- });
21355
- var normalizeIntent = (intent) => ({
21356
- ...intent,
21357
- scope: normalizedScope(intent.scope),
21358
- authorityRefs: normalizedRefList(intent.authorityRefs)
21359
- });
21360
- var normalizePrior = (prior) => ({
21361
- decisions: stableList(prior.decisions.map(normalizeDecision)),
21362
- findings: stableList(prior.findings.map((finding) => ({
21363
- ...finding,
21364
- scope: normalizedScope(finding.scope),
21365
- refs: normalizedRefList(finding.refs),
21366
- resolution: finding.resolution ? {
21367
- ...finding.resolution,
21368
- evidenceIds: stableUnique(finding.resolution.evidenceIds),
21369
- decisionIds: stableUnique(finding.resolution.decisionIds)
21370
- } : null
21371
- }))),
21372
- requirements: stableList(prior.requirements.map((requirement) => ({
21373
- ...requirement,
21374
- scope: normalizedScope(requirement.scope)
21375
- })))
21376
- });
21377
- var invalidFields = (error) => ({
21378
- fields: error.issues.map((issue) => ({
21379
- path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
21380
- reason: issue.message
21381
- }))
21382
- });
21383
- function normalizeResolverInput(input) {
21384
- const parsed = resolverInputSchema.safeParse(input);
21385
- if (!parsed.success)
21386
- return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
21387
- const value = parsed.data;
21388
- const signals = value.assessment.signals;
21389
- if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
21390
- return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
21391
- fields: [
21392
- {
21393
- path: "assessment.signals",
21394
- reason: "contradictory behaviorChange and mechanicalLowRisk"
21395
- }
21396
- ]
21397
- });
21398
- const ids = value.constraints.map((constraint) => constraint.id);
21399
- if (new Set(ids).size !== ids.length)
21400
- return failure2("invalid_input", "constraint IDs must be unique", {
21401
- fields: [{ path: "constraints", reason: "duplicate constraint id" }]
21402
- });
21403
- if (value.preferences?.fast && value.preferences.thorough)
21404
- return failure2("invalid_input", "fast and thorough preferences contradict", {
21405
- fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
21406
- });
21407
- const normalized = {
21408
- intent: normalizeIntent(value.intent),
21409
- assessment: normalizeAssessment(value.assessment),
21410
- constraints: stableList(value.constraints.map(normalizeConstraint)),
21411
- prior: normalizePrior(value.prior),
21412
- ...value.preferences ? { preferences: { ...value.preferences } } : {},
21413
- policyVersion: POLICY_VERSION
21414
- };
21415
- return success(null, null, normalized);
21416
- }
21417
- var requirement = (spec) => ({
21418
- ...spec,
21419
- id: requirementId({
21420
- ruleId: spec.ruleId,
21421
- scope: spec.scope,
21422
- satisfaction: spec.satisfaction,
21423
- before: spec.before,
21424
- dependentAction: spec.dependentAction
21425
- })
21426
- });
21427
- var signalReason = (input, name) => input.assessment.signals[name].reason;
21428
- function resolveRequirements(input) {
21429
- const { signals } = input.assessment;
21430
- const scope = input.intent.scope;
21431
- const requirements = [];
21432
- const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
21433
- const hasUnknownFact = [
21434
- ...input.assessment.facts,
21435
- ...input.assessment.consequences.map(({ fact }) => fact)
21436
- ].some((fact) => fact.basis === "unknown");
21437
- if (hasUnknownSignal || hasUnknownFact)
21438
- requirements.push(requirement({
21439
- ruleId: "consequential-unknown",
21440
- dimension: "investigation",
21441
- scope,
21442
- reason: `Resolve consequential uncertainty before the dependent action: ${[
21443
- ...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
21444
- ...hasUnknownFact ? ["unknown fact"] : []
21445
- ].join(", ")}`,
21446
- satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
21447
- before: "dependent_action",
21448
- dependentAction: null,
21449
- acceptanceAllowed: false
21450
- }));
21451
- if (signals.productChoiceOpen.value === true)
21452
- requirements.push(requirement({
21453
- ruleId: "product-decision",
21454
- dimension: "decisions",
21455
- scope,
21456
- reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
21457
- satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
21458
- before: "dependent_action",
21459
- dependentAction: null,
21460
- acceptanceAllowed: false
21461
- }));
21462
- const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
21463
- if (signals.behaviorChange.value === true)
21464
- requirements.push(requirement({
21465
- ruleId: "behavioral-verification",
21466
- dimension: "testing",
21467
- scope,
21468
- reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
21469
- satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
21470
- before: "close",
21471
- dependentAction: null,
21472
- acceptanceAllowed: false
21473
- }), requirement({
21474
- ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
21475
- dimension: "review",
21476
- scope,
21477
- reason: independentBehaviorReview ? "Consequential behavior or the thorough preference requires fresh-context review." : "Bounded behavior changes use self-review unless consequential risks or trusted rules require more.",
21478
- satisfaction: independentBehaviorReview ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
21479
- before: "close",
21480
- dependentAction: null,
21481
- acceptanceAllowed: !independentBehaviorReview
21482
- }));
21483
- else if (signals.mechanicalLowRisk.value === true)
21484
- requirements.push(requirement({
21485
- ruleId: "mechanical-existing-checks",
21486
- dimension: "verification",
21487
- scope,
21488
- reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
21489
- satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
21490
- before: "close",
21491
- dependentAction: null,
21492
- acceptanceAllowed: true
21493
- }), requirement({
21494
- ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
21495
- dimension: "review",
21496
- scope,
21497
- reason: input.preferences?.thorough ? "The thorough preference upgrades mechanical self-review to fresh-context review." : "Mechanical low-risk work uses self-review rather than mandatory fresh-context review.",
21498
- satisfaction: input.preferences?.thorough ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
21499
- before: "close",
21500
- dependentAction: null,
21501
- acceptanceAllowed: !input.preferences?.thorough
21502
- }));
21503
- if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
21504
- requirements.push(requirement({
21505
- ruleId: "pre-pr-cleanup",
21506
- dimension: "verification",
21507
- scope,
21508
- reason: "Implementation output must be deslopped before it is delivered.",
21509
- satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
21510
- before: "dependent_action",
21511
- dependentAction: "hosting.pull_request",
21512
- acceptanceAllowed: true
21513
- }));
21514
- if (signals.durableAgreementNeeded.value === true)
21515
- requirements.push(requirement({
21516
- ruleId: "durable-spec",
21517
- dimension: "artifacts",
21518
- scope,
21519
- reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
21520
- satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
21521
- before: "write",
21522
- dependentAction: null,
21523
- acceptanceAllowed: true
21524
- }));
21525
- if (signals.coordinationPlanNeeded.value === true)
21526
- requirements.push(requirement({
21527
- ruleId: "coordination-plan",
21528
- dimension: "continuity",
21529
- scope,
21530
- reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
21531
- satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
21532
- before: "dependent_action",
21533
- dependentAction: null,
21534
- acceptanceAllowed: true
21535
- }));
21536
- if (signals.helperUseful.value === true && !input.preferences?.fast)
21537
- requirements.push(requirement({
21538
- ruleId: "helper-usefulness",
21539
- dimension: "delegation",
21540
- scope,
21541
- reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
21542
- satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
21543
- before: "dependent_action",
21544
- dependentAction: null,
21545
- acceptanceAllowed: true
21546
- }));
21547
- for (const constraint of input.constraints)
21548
- for (const obligation of constraint.requires) {
21549
- const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
21550
- requirements.push(requirement({
21551
- ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
21552
- kind: obligation.kind,
21553
- scope: normalizedScope(obligation.scope),
21554
- refs: normalizedRefList(obligation.refs),
21555
- method: obligation.method,
21556
- before: obligation.before,
21557
- dependentAction: obligation.dependentAction
21558
- })}`,
21559
- dimension,
21560
- scope: obligation.scope,
21561
- reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
21562
- satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
21563
- before: obligation.before,
21564
- dependentAction: obligation.dependentAction,
21565
- acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
21566
- }));
21567
- }
21568
- const seen = new Set;
21569
- return requirements.filter((item) => {
21570
- if (seen.has(item.id))
21571
- return false;
21572
- seen.add(item.id);
21573
- return true;
21574
- });
21575
- }
21576
- function resolvePolicy(input) {
21577
- const normalized = normalizeResolverInput(input);
21578
- if (!normalized.ok)
21579
- return normalized;
21580
- const requirements = resolveRequirements(normalized.data);
21581
- const policy = {
21582
- policyVersion: POLICY_VERSION,
21583
- inputDigest: sha256(canonicalJson(normalized.data)),
21584
- requirements
21585
- };
21586
- const parsed = policySchema.safeParse(policy);
21587
- if (!parsed.success)
21588
- return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
21589
- return success(null, null, parsed.data);
21590
- }
21591
- function diffPolicy(previous, next, reason, now) {
21592
- if (!policySchema.safeParse(previous).success && previous !== null)
21593
- throw new TypeError("previous policy is invalid");
21594
- if (!policySchema.safeParse(next).success)
21595
- throw new TypeError("next policy is invalid");
21596
- const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
21597
- const newIds = new Set(next.requirements.map((item) => item.id));
21598
- const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits2);
21599
- const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits2);
21600
- if (added.length === 0 && retired.length === 0)
21601
- return null;
21602
- const normalizedReason = reason.trim();
21603
- const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
21604
- const change = {
21605
- recordedAt: now,
21606
- fromInputDigest: previous?.inputDigest ?? null,
21607
- toInputDigest: next.inputDigest,
21608
- added,
21609
- retired,
21610
- reason: changeReason
21611
- };
21612
- const parsed = policyChangeSchema.safeParse(change);
21613
- if (!parsed.success)
21614
- throw new TypeError("policy change is invalid");
21615
- return parsed.data;
21616
- }
21617
-
21618
- // packages/workit-core/src/core/task-evaluation.ts
21619
- import { createHash as createHash6 } from "node:crypto";
21620
- import * as fs7 from "node:fs";
21621
- import { spawnSync as spawnSync2 } from "node:child_process";
21622
- import path10 from "node:path";
21623
- var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
21624
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
21625
- var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
21626
- var canonicalPath = (value) => {
21627
- if (typeof value !== "string" || !value)
21628
- return null;
21629
- if (value.includes("\\") || value.split("/").includes(".."))
21630
- return null;
21631
- const normalized = path10.posix.normalize(value);
21632
- if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
21633
- return null;
21634
- return normalized === "." ? "." : normalized.replace(/\/$/, "");
21635
- };
21636
- var canonicalScope = (scope) => {
21637
- const paths = scope.paths.length ? scope.paths : ["."];
21638
- const normalizedPaths = paths.map(canonicalPath);
21639
- const normalizedExclusions = scope.exclusions.map(canonicalPath);
21640
- if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
21641
- return null;
21642
- return {
21643
- description: scope.description,
21644
- paths: [...new Set(normalizedPaths)],
21645
- exclusions: [...new Set(normalizedExclusions)]
21646
- };
21647
- };
21648
- var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
21649
- var scopeMatches = (value, scope) => {
21650
- const normalized = canonicalScope(scope);
21651
- if (!normalized)
21652
- return false;
21653
- const paths = normalized.paths;
21654
- return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
21655
- };
21656
- var pathRelevant = (value, scope) => scopeMatches(value, scope);
21657
- var compareCodeUnits3 = (left, right) => {
21658
- const length = Math.min(left.length, right.length);
21659
- for (let index = 0;index < length; index += 1) {
21660
- const difference = left.charCodeAt(index) - right.charCodeAt(index);
21661
- if (difference !== 0)
21662
- return difference;
21663
- }
21664
- return left.length - right.length;
21665
- };
21666
21745
  var gitPaths = (root) => {
21667
- const result = spawnSync2("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
21746
+ const result = spawnSync3("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
21668
21747
  cwd: root,
21669
21748
  encoding: "buffer"
21670
21749
  });
@@ -21677,7 +21756,7 @@ var gitPaths = (root) => {
21677
21756
  };
21678
21757
  }
21679
21758
  const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")) : [];
21680
- const stagedDeleted = spawnSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
21759
+ const stagedDeleted = spawnSync3("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
21681
21760
  if (stagedDeleted.status !== 0) {
21682
21761
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
21683
21762
  if (!/not a git repository/i.test(stderr))
@@ -21690,7 +21769,7 @@ var gitPaths = (root) => {
21690
21769
  var walk = (root, directory, output) => {
21691
21770
  let names;
21692
21771
  try {
21693
- names = fs7.readdirSync(directory);
21772
+ names = fs8.readdirSync(directory);
21694
21773
  } catch {
21695
21774
  return true;
21696
21775
  }
@@ -21701,7 +21780,7 @@ var walk = (root, directory, output) => {
21701
21780
  const target = path10.join(directory, name);
21702
21781
  let stat;
21703
21782
  try {
21704
- stat = fs7.lstatSync(target);
21783
+ stat = fs8.lstatSync(target);
21705
21784
  } catch {
21706
21785
  uncertain = true;
21707
21786
  continue;
@@ -21720,10 +21799,10 @@ var scopeRoots = (root, scope) => {
21720
21799
  if (!inside(root, target))
21721
21800
  return failure2("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
21722
21801
  let ancestor = target;
21723
- while (!fs7.existsSync(ancestor) && ancestor !== root)
21802
+ while (!fs8.existsSync(ancestor) && ancestor !== root)
21724
21803
  ancestor = path10.dirname(ancestor);
21725
21804
  try {
21726
- if (!inside(root, fs7.realpathSync(ancestor)))
21805
+ if (!inside(root, fs8.realpathSync(ancestor)))
21727
21806
  return failure2("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
21728
21807
  } catch {
21729
21808
  return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
@@ -21735,7 +21814,7 @@ var scopeRoots = (root, scope) => {
21735
21814
  function captureCandidate(root, scope, environment = []) {
21736
21815
  let checkout;
21737
21816
  try {
21738
- checkout = fs7.realpathSync(root);
21817
+ checkout = fs8.realpathSync(root);
21739
21818
  } catch {
21740
21819
  return failure2("invalid_input", "candidate root does not exist");
21741
21820
  }
@@ -21753,11 +21832,11 @@ function captureCandidate(root, scope, environment = []) {
21753
21832
  if (item === ".git" || item === ".workit")
21754
21833
  continue;
21755
21834
  names.add(item);
21756
- if (!fs7.existsSync(target))
21835
+ if (!fs8.existsSync(target))
21757
21836
  continue;
21758
21837
  let stat;
21759
21838
  try {
21760
- stat = fs7.lstatSync(target);
21839
+ stat = fs8.lstatSync(target);
21761
21840
  } catch {
21762
21841
  uncertain = true;
21763
21842
  continue;
@@ -21775,15 +21854,15 @@ function captureCandidate(root, scope, environment = []) {
21775
21854
  continue;
21776
21855
  const target = path10.join(checkout, item);
21777
21856
  try {
21778
- const stat = fs7.lstatSync(target);
21857
+ const stat = fs8.lstatSync(target);
21779
21858
  if (stat.isSymbolicLink()) {
21780
- const link = fs7.readlinkSync(target);
21859
+ const link = fs8.readlinkSync(target);
21781
21860
  files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
21782
21861
  } else if (stat.isFile()) {
21783
21862
  files.push({
21784
21863
  path: item,
21785
21864
  kind: "file",
21786
- digest: digestBytes3(fs7.readFileSync(target)),
21865
+ digest: digestBytes3(fs8.readFileSync(target)),
21787
21866
  executable: (stat.mode & 73) !== 0
21788
21867
  });
21789
21868
  } else if (!stat.isDirectory()) {
@@ -21805,10 +21884,10 @@ function captureCandidate(root, scope, environment = []) {
21805
21884
  const namesByEnvironment = supplied.map(({ name }) => name);
21806
21885
  if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
21807
21886
  return failure2("invalid_input", "candidate environment names must be unique and non-empty");
21808
- const values = supplied.toSorted((left, right) => compareCodeUnits3(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
21887
+ const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
21809
21888
  if (values.some(({ value }) => value === null))
21810
21889
  completeness = "uncertain";
21811
- const headResult = spawnSync2("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
21890
+ const headResult = spawnSync3("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
21812
21891
  if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
21813
21892
  completeness = "uncertain";
21814
21893
  const head = headResult.status === 0 ? headResult.stdout.trim() : null;
@@ -21859,420 +21938,862 @@ function evaluateEvidence(task, candidate) {
21859
21938
  if (evidence.kind === "check" || evidence.kind === "review") {
21860
21939
  if (!evidence.beforeCandidateId || !evidence.candidateId)
21861
21940
  return {
21862
- evidenceId: entry.id,
21863
- status: "stale",
21864
- reason: "check or review evidence lacks start and end candidates"
21941
+ evidenceId: entry.id,
21942
+ status: "stale",
21943
+ reason: "check or review evidence lacks start and end candidates"
21944
+ };
21945
+ if (evidence.beforeCandidateId !== evidence.candidateId)
21946
+ return {
21947
+ evidenceId: entry.id,
21948
+ status: "stale",
21949
+ reason: "candidate changed during the check or review"
21950
+ };
21951
+ }
21952
+ if (!evidence.candidateId || !current)
21953
+ return {
21954
+ evidenceId: entry.id,
21955
+ status: evidence.result,
21956
+ reason: "evidence has no candidate binding"
21957
+ };
21958
+ const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
21959
+ if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
21960
+ return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
21961
+ if (observed.id === current.id)
21962
+ return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
21963
+ const scopes = evidenceScopes(task, evidence.requirementIds);
21964
+ const changed = changedPaths(observed, current);
21965
+ const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
21966
+ if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
21967
+ return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
21968
+ return {
21969
+ evidenceId: entry.id,
21970
+ status: evidence.result,
21971
+ reason: "candidate changed outside evidence scope"
21972
+ };
21973
+ });
21974
+ }
21975
+ var scopeCovers2 = scopeCovers;
21976
+ var checkPin = (assignment, beforeCandidateId, candidateId) => {
21977
+ const pin = assignment.candidateId ?? null;
21978
+ if (pin === null)
21979
+ return true;
21980
+ return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
21981
+ };
21982
+ var resolveBinding = (evidence, pin, currentCandidateId) => {
21983
+ const bound = evidence.kind === "check" || evidence.kind === "review";
21984
+ const fallback = bound ? pin ?? currentCandidateId : null;
21985
+ return {
21986
+ beforeCandidateId: evidence.beforeCandidateId ?? fallback,
21987
+ candidateId: evidence.candidateId ?? fallback
21988
+ };
21989
+ };
21990
+ var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId != null);
21991
+ 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")));
21992
+ 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 }));
21993
+ var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
21994
+ if (kind !== "passed")
21995
+ return false;
21996
+ if (dimension === "testing" || dimension === "verification")
21997
+ return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
21998
+ if (dimension === "review")
21999
+ return evidenceKind === "review";
22000
+ if (dimension === "investigation" || dimension === "challenge")
22001
+ return evidenceKind === "investigation";
22002
+ if (dimension === "artifacts" || dimension === "continuity")
22003
+ return evidenceKind === "artifact";
22004
+ return false;
22005
+ };
22006
+ var expectedKinds = (dimension, ruleId) => {
22007
+ if (dimension === "testing")
22008
+ return "check";
22009
+ if (dimension === "verification")
22010
+ return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
22011
+ if (dimension === "review")
22012
+ return "review";
22013
+ if (dimension === "investigation" || dimension === "challenge")
22014
+ return "investigation";
22015
+ if (dimension === "artifacts" || dimension === "continuity")
22016
+ return "artifact";
22017
+ if (dimension === "decisions")
22018
+ return "decision";
22019
+ return "worker report";
22020
+ };
22021
+ function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
22022
+ const evidence = evaluateEvidence(task, candidate);
22023
+ if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
22024
+ return task.policy.requirements.map((requirement) => ({
22025
+ requirementId: requirement.id,
22026
+ status: "unsatisfied",
22027
+ evidenceIds: [],
22028
+ decisionIds: [],
22029
+ reason: "stored policy version is unsupported; reassessment is required"
22030
+ }));
22031
+ return (task.policy?.requirements ?? []).map((requirement) => {
22032
+ const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
22033
+ const passed = related.filter(({ entry, evaluation }) => {
22034
+ if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
22035
+ return false;
22036
+ if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
22037
+ return false;
22038
+ if (requirement.dimension !== "review")
22039
+ return true;
22040
+ const reviewSession = sessionFromRef(entry.data.reviewContext);
22041
+ const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
22042
+ 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));
22043
+ return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
22044
+ });
22045
+ if (passed.length) {
22046
+ const currentId = candidate?.id ?? null;
22047
+ 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;
22048
+ if (withRed.length)
22049
+ return {
22050
+ requirementId: requirement.id,
22051
+ status: "satisfied",
22052
+ evidenceIds: withRed.map(({ entry }) => entry.id),
22053
+ decisionIds: [],
22054
+ reason: "fresh applicable evidence passed"
21865
22055
  };
21866
- if (evidence.beforeCandidateId !== evidence.candidateId)
22056
+ if (requirement.dimension === "testing")
21867
22057
  return {
21868
- evidenceId: entry.id,
21869
- status: "stale",
21870
- reason: "candidate changed during the check or review"
22058
+ requirementId: requirement.id,
22059
+ status: "unsatisfied",
22060
+ evidenceIds: [],
22061
+ decisionIds: [],
22062
+ reason: "testing requirement has GREEN evidence but no preceding RED"
21871
22063
  };
21872
22064
  }
21873
- if (!evidence.candidateId || !current)
22065
+ if (requirement.dimension === "delegation") {
22066
+ const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
22067
+ 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))));
22068
+ if (reconciled)
22069
+ return {
22070
+ requirementId: requirement.id,
22071
+ status: "satisfied",
22072
+ evidenceIds: [],
22073
+ decisionIds: [],
22074
+ reason: "bounded helper completed and its result is recorded"
22075
+ };
22076
+ }
22077
+ const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
22078
+ if (decisions.length)
21874
22079
  return {
21875
- evidenceId: entry.id,
21876
- status: evidence.result,
21877
- reason: "evidence has no candidate binding"
22080
+ requirementId: requirement.id,
22081
+ status: "satisfied",
22082
+ evidenceIds: [],
22083
+ decisionIds: decisions.map((decision) => decision.id),
22084
+ reason: "an applicable approved or stated decision satisfies the requirement"
21878
22085
  };
21879
- const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
21880
- if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
21881
- return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
21882
- if (observed.id === current.id)
21883
- return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
21884
- const scopes = evidenceScopes(task, evidence.requirementIds);
21885
- const changed = changedPaths(observed, current);
21886
- const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
21887
- if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
21888
- return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
22086
+ const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
22087
+ if (limitations.length)
22088
+ return {
22089
+ requirementId: requirement.id,
22090
+ status: "accepted_limitation",
22091
+ evidenceIds: [],
22092
+ decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
22093
+ reason: "an applicable approved limitation permits the missing evidence"
22094
+ };
22095
+ const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
22096
+ if (!unavailable && related.length > 0 && !passed.length)
22097
+ return {
22098
+ requirementId: requirement.id,
22099
+ status: "unsatisfied",
22100
+ evidenceIds: related.map(({ entry }) => entry.id),
22101
+ decisionIds: [],
22102
+ reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
22103
+ };
22104
+ return {
22105
+ requirementId: requirement.id,
22106
+ status: unavailable ? "unavailable" : "unsatisfied",
22107
+ evidenceIds: related.map(({ entry }) => entry.id),
22108
+ decisionIds: [],
22109
+ reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
22110
+ };
22111
+ });
22112
+ }
22113
+ function evaluateClosure(requestedOutcome, view) {
22114
+ if (requestedOutcome !== "stopped" && !view.task.policy)
22115
+ return failure2("requirements_unsatisfied", "task has not been assessed");
22116
+ if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
22117
+ return failure2("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
22118
+ const evaluations = view.requirements;
22119
+ const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
22120
+ if (requestedOutcome !== "stopped" && openFindings.length)
22121
+ return failure2("requirements_unsatisfied", "open findings must be resolved before closure");
22122
+ if (requestedOutcome !== "stopped") {
22123
+ const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
22124
+ for (const entry of view.task.findings) {
22125
+ if (entry.data.disposition === "fixed") {
22126
+ const verified = entry.data.resolution?.evidenceIds.some((id) => {
22127
+ const evidence = view.task.evidence.find((item) => item.id === id);
22128
+ if (!evidence)
22129
+ return false;
22130
+ return findingVerificationPasses(entry.data.candidateId, {
22131
+ kind: evidence.data.kind,
22132
+ candidateId: evidence.data.candidateId,
22133
+ status: evidenceById.get(id) ?? "missing"
22134
+ });
22135
+ });
22136
+ if (!verified)
22137
+ return failure2("requirements_unsatisfied", "fixed findings require current verification");
22138
+ }
22139
+ if (entry.data.disposition === "deferred") {
22140
+ const valid = entry.data.resolution?.decisionIds.some((id) => {
22141
+ const decisionEntry = view.task.decisions.find((item) => item.id === id);
22142
+ const decision = decisionEntry?.data;
22143
+ 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)));
22144
+ });
22145
+ if (!valid)
22146
+ return failure2("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
22147
+ }
22148
+ }
22149
+ }
22150
+ const blocking = evaluations.filter((item) => {
22151
+ if (item.status !== "unsatisfied" && item.status !== "unavailable")
22152
+ return false;
22153
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
22154
+ return !requirement || requirement.before === "close";
22155
+ });
22156
+ if (requestedOutcome !== "stopped" && blocking.length)
22157
+ return failure2("requirements_unsatisfied", "applicable requirements are unsatisfied", {
22158
+ requirementIds: blocking.map((item) => item.requirementId),
22159
+ requirements: blocking.map((item) => {
22160
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
22161
+ return {
22162
+ requirementId: item.requirementId,
22163
+ ruleId: requirement?.ruleId ?? "unknown",
22164
+ reason: item.reason,
22165
+ satisfaction: requirement?.satisfaction ?? "",
22166
+ dependentAction: requirement?.dependentAction ?? null
22167
+ };
22168
+ })
22169
+ });
22170
+ const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
22171
+ if (requestedOutcome === "verified" && accepted.length)
22172
+ return failure2("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
22173
+ requirementIds: accepted.map((item) => item.requirementId)
22174
+ });
22175
+ const evidenceIds = [
22176
+ ...new Set([
22177
+ ...evaluations.flatMap((item) => item.evidenceIds),
22178
+ ...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
22179
+ ])
22180
+ ];
22181
+ const decisionIds = [
22182
+ ...new Set([
22183
+ ...evaluations.flatMap((item) => item.decisionIds),
22184
+ ...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
22185
+ ])
22186
+ ];
22187
+ return success(null, null, {
22188
+ outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
22189
+ evidenceIds,
22190
+ decisionIds,
22191
+ requirementIds: evaluations.map((item) => item.requirementId)
22192
+ });
22193
+ }
22194
+
22195
+ // packages/workit-core/src/core/methods.ts
22196
+ var METHODS = {
22197
+ "workit-challenge": { dimensions: ["challenge", "decisions"] },
22198
+ "workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
22199
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
22200
+ "workit-plan": { dimensions: ["artifacts", "continuity"] },
22201
+ "workit-implement": { dimensions: ["delegation"] },
22202
+ "workit-debug": {},
22203
+ "workit-handoff": {},
22204
+ "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
22205
+ };
22206
+ var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
22207
+ var assuranceFor = (id, definition, capabilities) => {
22208
+ const keys = new Set([
22209
+ id,
22210
+ ...definition.dimensions ?? [],
22211
+ ...definition.ruleIds ?? []
22212
+ ]);
22213
+ const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
22214
+ if (id === "workit-review" && matches.length === 0)
22215
+ return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
22216
+ if (matches.some((capability) => capability.assurance === "unavailable"))
22217
+ return {
22218
+ assurance: "unavailable",
22219
+ reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
22220
+ };
22221
+ if (matches.some((capability) => capability.assurance === "enforced"))
22222
+ return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
22223
+ return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
22224
+ };
22225
+ function selectMethods(policy, capabilities) {
22226
+ return Object.entries(METHODS).flatMap(([id, definition]) => {
22227
+ const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
22228
+ if (requirements.length === 0)
22229
+ return [];
22230
+ const capability = assuranceFor(id, definition, capabilities);
22231
+ const reasons = [
22232
+ ...new Set(requirements.map((requirement) => requirement.reason)),
22233
+ ...capability.reasons
22234
+ ];
22235
+ if (capability.assurance === "unavailable")
22236
+ reasons.unshift("required capability is unavailable");
22237
+ return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
22238
+ });
22239
+ }
22240
+
22241
+ // packages/workit-core/src/core/task-context.ts
22242
+ function reconcileResume(view, observations = []) {
22243
+ if (observations.length > 0)
22244
+ return failure2("permission_denied", "worker observations require native host verification");
22245
+ const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
22246
+ if (!candidate.ok)
22247
+ return candidate;
22248
+ const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
22249
+ const blockers = [...view.task.progress.blockers];
22250
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
22251
+ blockers.push({
22252
+ reason: "worker state requires reconciliation",
22253
+ dependentAction: "resume",
22254
+ refs: []
22255
+ });
22256
+ return success(null, null, {
22257
+ candidate: candidate.data,
22258
+ staleEvidenceIds,
22259
+ workerUpdates: [],
22260
+ blockers,
22261
+ reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
22262
+ });
22263
+ }
22264
+ var COMPACT_MAX_BYTES = 4096;
22265
+ var COMPACT_MAX_ITEMS = 8;
22266
+ var COMPACT_TEXT_BYTES = 128;
22267
+ var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
22268
+ var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
22269
+ var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
22270
+ var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
22271
+ const separator = match.search(/[:=]/u);
22272
+ if (separator >= 0)
22273
+ return `${match.slice(0, separator)}=<redacted>`;
22274
+ const space = match.search(/\s/u);
22275
+ return `${match.slice(0, space)} <redacted>`;
22276
+ }).slice(0, limit);
22277
+ var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
22278
+ var compactTimestamp = (value) => `${value.slice(0, 19)}.${value.slice(20, -1).padEnd(9, "0")}`;
22279
+ var compactDecisionChoice = (entry) => {
22280
+ const stated = entry.data.binding.statedChoice?.text;
22281
+ const approved = entry.data.response === "approved" ? entry.data.binding.approvedContent : "";
22282
+ const source = stated?.trim() || approved.trim() || null;
22283
+ return compactText(source, COMPACT_TEXT_BYTES);
22284
+ };
22285
+ var compactReference = (ref) => {
22286
+ if (ref.kind === "file")
21889
22287
  return {
21890
- evidenceId: entry.id,
21891
- status: evidence.result,
21892
- reason: "candidate changed outside evidence scope"
22288
+ kind: "file",
22289
+ path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
21893
22290
  };
21894
- });
21895
- }
21896
- var scopeCovers2 = scopeCovers;
21897
- var checkPin = (assignment, beforeCandidateId, candidateId) => {
21898
- const pin = assignment.candidateId ?? null;
21899
- if (pin === null)
21900
- return true;
21901
- return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
22291
+ if (ref.kind === "record")
22292
+ return {
22293
+ kind: "record",
22294
+ collection: ref.collection,
22295
+ id: ref.id
22296
+ };
22297
+ return { kind: ref.kind };
21902
22298
  };
21903
- var resolveBinding = (evidence, pin, currentCandidateId) => {
21904
- const bound = evidence.kind === "check" || evidence.kind === "review";
21905
- const fallback = bound ? pin ?? currentCandidateId : null;
21906
- return {
21907
- beforeCandidateId: evidence.beforeCandidateId ?? fallback,
21908
- candidateId: evidence.candidateId ?? fallback
22299
+ function compactTaskContext(view) {
22300
+ const decisions = view.task.decisions.slice().toSorted((left, right) => {
22301
+ const leftAt = compactTimestamp(left.recordedAt);
22302
+ const rightAt = compactTimestamp(right.recordedAt);
22303
+ if (leftAt !== rightAt)
22304
+ return leftAt > rightAt ? -1 : 1;
22305
+ return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
22306
+ }).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
22307
+ id: entry.id,
22308
+ purpose: entry.data.purpose,
22309
+ status: entry.data.response,
22310
+ digest: entry.data.digest,
22311
+ choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
22312
+ references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
22313
+ }));
22314
+ const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
22315
+ id: method.id,
22316
+ assurance: method.assurance,
22317
+ reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
22318
+ })) : [];
22319
+ const gaps = Array.from(new Set([
22320
+ ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
22321
+ ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
22322
+ ...view.task.progress.blockers.map((entry) => entry.reason)
22323
+ ])).toSorted().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
22324
+ const context = {
22325
+ objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
22326
+ status: view.task.status,
22327
+ decisions,
22328
+ methods,
22329
+ gaps,
22330
+ nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
21909
22331
  };
22332
+ let encoded = JSON.stringify(context);
22333
+ while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
22334
+ if (context.gaps.length)
22335
+ context.gaps.pop();
22336
+ else if (context.decisions.length)
22337
+ context.decisions.pop();
22338
+ else
22339
+ context.methods.pop();
22340
+ encoded = JSON.stringify(context);
22341
+ }
22342
+ if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
22343
+ context.objective = compactText(context.objective, 64) ?? "";
22344
+ context.nextAction = compactText(context.nextAction, 64);
22345
+ encoded = JSON.stringify(context);
22346
+ }
22347
+ return encoded;
22348
+ }
22349
+ var exportDigest = (bundle) => sha256({
22350
+ schemaVersion: bundle.schemaVersion,
22351
+ exportedAt: bundle.exportedAt,
22352
+ sourceWorkspaceId: bundle.sourceWorkspaceId,
22353
+ task: bundle.task
22354
+ });
22355
+ // packages/workit-core/src/core/external-action.ts
22356
+ var chainStepSchema = union([
22357
+ string2().min(1),
22358
+ object2({ branch: string2().min(1) }).strict(),
22359
+ object2({ pr: literal(true) }).strict()
22360
+ ]);
22361
+ var externalActionSchema = discriminatedUnion("operation", [
22362
+ object2({
22363
+ operation: literal("git.branch_setup"),
22364
+ payload: object2({
22365
+ action: _enum(["setup", "reapply_stash"]).optional(),
22366
+ sdd_dir: string2().optional(),
22367
+ target_branch: string2().optional(),
22368
+ stash: _enum(["yes", "no"]).optional(),
22369
+ cwd: string2().min(1).optional()
22370
+ }).strict()
22371
+ }).strict(),
22372
+ object2({
22373
+ operation: literal("git.commit"),
22374
+ payload: object2({
22375
+ message: string2().min(1).optional(),
22376
+ plan_steps: array(chainStepSchema).min(1).max(32).optional(),
22377
+ plan_branch: string2().min(1).optional(),
22378
+ cwd: string2().min(1).optional()
22379
+ }).strict()
22380
+ }).strict(),
22381
+ object2({
22382
+ operation: literal("git.push"),
22383
+ payload: object2({ branch: string2().min(1).optional(), cwd: string2().min(1).optional() }).strict()
22384
+ }).strict(),
22385
+ object2({
22386
+ operation: literal("hosting.pull_request"),
22387
+ payload: object2({
22388
+ title: string2().min(1),
22389
+ body: string2().optional(),
22390
+ draft: boolean2().optional(),
22391
+ target_branch: string2().optional(),
22392
+ babysit: boolean2().optional(),
22393
+ cwd: string2().min(1).optional()
22394
+ }).strict()
22395
+ }).strict(),
22396
+ object2({
22397
+ operation: literal("hosting.merge"),
22398
+ payload: object2({
22399
+ target_branch: string2().min(1).optional(),
22400
+ source_branch: string2().min(1).optional(),
22401
+ cwd: string2().min(1).optional()
22402
+ }).strict()
22403
+ }).strict(),
22404
+ object2({
22405
+ operation: literal("hosting.delete_branch"),
22406
+ payload: object2({ branch: string2().min(1), cwd: string2().min(1).optional() }).strict()
22407
+ }).strict(),
22408
+ object2({
22409
+ operation: literal("youtrack.update"),
22410
+ payload: object2({
22411
+ issueId: string2().min(1),
22412
+ markdown: string2().min(1),
22413
+ minutes: number2().positive().optional()
22414
+ }).strict()
22415
+ }).strict(),
22416
+ object2({
22417
+ operation: literal("youtrack.time"),
22418
+ payload: object2({
22419
+ issueId: string2().min(1),
22420
+ minutes: number2().positive(),
22421
+ text: string2().optional(),
22422
+ dateMs: number2().finite().optional()
22423
+ }).strict()
22424
+ }).strict(),
22425
+ object2({
22426
+ operation: literal("youtrack.meeting"),
22427
+ payload: object2({
22428
+ issueId: string2().min(1),
22429
+ minutes: number2().positive(),
22430
+ text: string2().min(1)
22431
+ }).strict()
22432
+ }).strict(),
22433
+ object2({
22434
+ operation: literal("changelog.apply"),
22435
+ payload: object2({
22436
+ entries: union([
22437
+ record(string2(), array(string2())),
22438
+ array(object2({ category: string2().min(1), text: string2().min(1) }).strict())
22439
+ ]).optional(),
22440
+ path: string2().optional(),
22441
+ normalize_only: boolean2().optional()
22442
+ }).strict()
22443
+ }).strict(),
22444
+ object2({
22445
+ operation: literal("context.read"),
22446
+ payload: object2({
22447
+ kind: _enum([
22448
+ "git",
22449
+ "pr",
22450
+ "youtrack",
22451
+ "github_issue",
22452
+ "gitlab_issue",
22453
+ "changelog",
22454
+ "release",
22455
+ "affected"
22456
+ ]),
22457
+ range: string2().optional(),
22458
+ issueId: string2().optional(),
22459
+ issueUrl: string2().optional(),
22460
+ issueRef: string2().optional(),
22461
+ mode: string2().optional(),
22462
+ specPath: string2().optional(),
22463
+ planPath: string2().optional(),
22464
+ cwd: string2().min(1).optional()
22465
+ }).strict()
22466
+ }).strict()
22467
+ ]);
22468
+ // packages/workit-core/src/core/task-engine.ts
22469
+ import { realpathSync as realpathSync6 } from "node:fs";
22470
+
22471
+ // packages/workit-core/src/core/policy-resolver.ts
22472
+ var resolverInputSchema = object2({
22473
+ intent: intentSchema,
22474
+ assessment: assessmentSchema,
22475
+ constraints: array(constraintSchema),
22476
+ prior: object2({
22477
+ decisions: array(decisionSchema),
22478
+ findings: array(findingSchema),
22479
+ requirements: array(requirementSchema)
22480
+ }).strict(),
22481
+ preferences: object2({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
22482
+ }).strict();
22483
+ var compareCodeUnits3 = (left, right) => {
22484
+ const length = Math.min(left.length, right.length);
22485
+ for (let index = 0;index < length; index += 1) {
22486
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
22487
+ if (difference !== 0)
22488
+ return difference;
22489
+ }
22490
+ return left.length - right.length;
21910
22491
  };
21911
- var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId != null);
21912
- 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")));
21913
- 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 }));
21914
- var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
21915
- if (kind !== "passed")
21916
- return false;
21917
- if (dimension === "testing" || dimension === "verification")
21918
- return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
21919
- if (dimension === "review")
21920
- return evidenceKind === "review";
21921
- if (dimension === "investigation" || dimension === "challenge")
21922
- return evidenceKind === "investigation";
21923
- if (dimension === "artifacts" || dimension === "continuity")
21924
- return evidenceKind === "artifact";
21925
- return false;
21926
- };
21927
- var expectedKinds = (dimension, ruleId) => {
21928
- if (dimension === "testing")
21929
- return "check";
21930
- if (dimension === "verification")
21931
- return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
21932
- if (dimension === "review")
21933
- return "review";
21934
- if (dimension === "investigation" || dimension === "challenge")
21935
- return "investigation";
21936
- if (dimension === "artifacts" || dimension === "continuity")
21937
- return "artifact";
21938
- if (dimension === "decisions")
21939
- return "decision";
21940
- return "worker report";
21941
- };
21942
- function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
21943
- const evidence = evaluateEvidence(task, candidate);
21944
- if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
21945
- return task.policy.requirements.map((requirement) => ({
21946
- requirementId: requirement.id,
21947
- status: "unsatisfied",
21948
- evidenceIds: [],
21949
- decisionIds: [],
21950
- reason: "stored policy version is unsupported; reassessment is required"
21951
- }));
21952
- return (task.policy?.requirements ?? []).map((requirement) => {
21953
- const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
21954
- const passed = related.filter(({ entry, evaluation }) => {
21955
- if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
21956
- return false;
21957
- if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
21958
- return false;
21959
- if (requirement.dimension !== "review")
21960
- return true;
21961
- const reviewSession = sessionFromRef(entry.data.reviewContext);
21962
- const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
21963
- 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));
21964
- return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
22492
+ var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits3);
22493
+ var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits3(canonicalJson(left), canonicalJson(right)));
22494
+ var normalizedScope = (scope) => ({
22495
+ description: scope.description,
22496
+ paths: stableUnique(scope.paths),
22497
+ exclusions: stableUnique(scope.exclusions)
22498
+ });
22499
+ var normalizedRefList = (values) => stableList(values);
22500
+ var normalizeAssessment = (assessment) => ({
22501
+ facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
22502
+ signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
22503
+ name,
22504
+ { ...signal, refs: normalizedRefList(signal.refs) }
22505
+ ])),
22506
+ consequences: stableList(assessment.consequences.map((consequence) => ({
22507
+ ...consequence,
22508
+ fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
22509
+ }))),
22510
+ verification: stableList(assessment.verification.map((verification) => ({
22511
+ ...verification,
22512
+ scope: normalizedScope(verification.scope),
22513
+ availableChecks: normalizedRefList(verification.availableChecks),
22514
+ gaps: stableUnique(verification.gaps)
22515
+ })))
22516
+ });
22517
+ var normalizeConstraint = (constraint) => ({
22518
+ ...constraint,
22519
+ source: constraint.source,
22520
+ requires: stableList(constraint.requires.map((requirement) => ({
22521
+ ...requirement,
22522
+ scope: normalizedScope(requirement.scope),
22523
+ refs: normalizedRefList(requirement.refs)
22524
+ })))
22525
+ });
22526
+ var normalizeDecision = (decision) => ({
22527
+ ...decision,
22528
+ binding: {
22529
+ ...decision.binding,
22530
+ scope: normalizedScope(decision.binding.scope),
22531
+ contentRefs: normalizedRefList(decision.binding.contentRefs)
22532
+ },
22533
+ requirementIds: stableUnique(decision.requirementIds)
22534
+ });
22535
+ var normalizeIntent = (intent) => ({
22536
+ ...intent,
22537
+ scope: normalizedScope(intent.scope),
22538
+ authorityRefs: normalizedRefList(intent.authorityRefs)
22539
+ });
22540
+ var normalizePrior = (prior) => ({
22541
+ decisions: stableList(prior.decisions.map(normalizeDecision)),
22542
+ findings: stableList(prior.findings.map((finding) => ({
22543
+ ...finding,
22544
+ scope: normalizedScope(finding.scope),
22545
+ refs: normalizedRefList(finding.refs),
22546
+ resolution: finding.resolution ? {
22547
+ ...finding.resolution,
22548
+ evidenceIds: stableUnique(finding.resolution.evidenceIds),
22549
+ decisionIds: stableUnique(finding.resolution.decisionIds)
22550
+ } : null
22551
+ }))),
22552
+ requirements: stableList(prior.requirements.map((requirement) => ({
22553
+ ...requirement,
22554
+ scope: normalizedScope(requirement.scope)
22555
+ })))
22556
+ });
22557
+ var invalidFields = (error) => ({
22558
+ fields: error.issues.map((issue) => ({
22559
+ path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
22560
+ reason: issue.message
22561
+ }))
22562
+ });
22563
+ function normalizeResolverInput(input) {
22564
+ const parsed = resolverInputSchema.safeParse(input);
22565
+ if (!parsed.success)
22566
+ return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
22567
+ const value = parsed.data;
22568
+ const signals = value.assessment.signals;
22569
+ if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
22570
+ return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
22571
+ fields: [
22572
+ {
22573
+ path: "assessment.signals",
22574
+ reason: "contradictory behaviorChange and mechanicalLowRisk"
22575
+ }
22576
+ ]
21965
22577
  });
21966
- if (passed.length) {
21967
- const currentId = candidate?.id ?? null;
21968
- 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;
21969
- if (withRed.length)
21970
- return {
21971
- requirementId: requirement.id,
21972
- status: "satisfied",
21973
- evidenceIds: withRed.map(({ entry }) => entry.id),
21974
- decisionIds: [],
21975
- reason: "fresh applicable evidence passed"
21976
- };
21977
- if (requirement.dimension === "testing")
21978
- return {
21979
- requirementId: requirement.id,
21980
- status: "unsatisfied",
21981
- evidenceIds: [],
21982
- decisionIds: [],
21983
- reason: "testing requirement has GREEN evidence but no preceding RED"
21984
- };
21985
- }
21986
- if (requirement.dimension === "delegation") {
21987
- const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
21988
- 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))));
21989
- if (reconciled)
21990
- return {
21991
- requirementId: requirement.id,
21992
- status: "satisfied",
21993
- evidenceIds: [],
21994
- decisionIds: [],
21995
- reason: "bounded helper completed and its result is recorded"
21996
- };
21997
- }
21998
- const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
21999
- if (decisions.length)
22000
- return {
22001
- requirementId: requirement.id,
22002
- status: "satisfied",
22003
- evidenceIds: [],
22004
- decisionIds: decisions.map((decision) => decision.id),
22005
- reason: "an applicable approved or stated decision satisfies the requirement"
22006
- };
22007
- const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
22008
- if (limitations.length)
22009
- return {
22010
- requirementId: requirement.id,
22011
- status: "accepted_limitation",
22012
- evidenceIds: [],
22013
- decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
22014
- reason: "an applicable approved limitation permits the missing evidence"
22015
- };
22016
- const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
22017
- if (!unavailable && related.length > 0 && !passed.length)
22018
- return {
22019
- requirementId: requirement.id,
22020
- status: "unsatisfied",
22021
- evidenceIds: related.map(({ entry }) => entry.id),
22022
- decisionIds: [],
22023
- reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
22024
- };
22025
- return {
22026
- requirementId: requirement.id,
22027
- status: unavailable ? "unavailable" : "unsatisfied",
22028
- evidenceIds: related.map(({ entry }) => entry.id),
22029
- decisionIds: [],
22030
- reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
22031
- };
22032
- });
22578
+ const ids = value.constraints.map((constraint) => constraint.id);
22579
+ if (new Set(ids).size !== ids.length)
22580
+ return failure2("invalid_input", "constraint IDs must be unique", {
22581
+ fields: [{ path: "constraints", reason: "duplicate constraint id" }]
22582
+ });
22583
+ if (value.preferences?.fast && value.preferences.thorough)
22584
+ return failure2("invalid_input", "fast and thorough preferences contradict", {
22585
+ fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
22586
+ });
22587
+ const normalized = {
22588
+ intent: normalizeIntent(value.intent),
22589
+ assessment: normalizeAssessment(value.assessment),
22590
+ constraints: stableList(value.constraints.map(normalizeConstraint)),
22591
+ prior: normalizePrior(value.prior),
22592
+ ...value.preferences ? { preferences: { ...value.preferences } } : {},
22593
+ policyVersion: POLICY_VERSION
22594
+ };
22595
+ return success(null, null, normalized);
22033
22596
  }
22034
- function evaluateClosure(requestedOutcome, view) {
22035
- if (requestedOutcome !== "stopped" && !view.task.policy)
22036
- return failure2("requirements_unsatisfied", "task has not been assessed");
22037
- if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
22038
- return failure2("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
22039
- const evaluations = view.requirements;
22040
- const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
22041
- if (requestedOutcome !== "stopped" && openFindings.length)
22042
- return failure2("requirements_unsatisfied", "open findings must be resolved before closure");
22043
- if (requestedOutcome !== "stopped") {
22044
- const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
22045
- for (const entry of view.task.findings) {
22046
- if (entry.data.disposition === "fixed") {
22047
- const verified = entry.data.resolution?.evidenceIds.some((id) => {
22048
- const evidence = view.task.evidence.find((item) => item.id === id);
22049
- if (!evidence)
22050
- return false;
22051
- return findingVerificationPasses(entry.data.candidateId, {
22052
- kind: evidence.data.kind,
22053
- candidateId: evidence.data.candidateId,
22054
- status: evidenceById.get(id) ?? "missing"
22055
- });
22056
- });
22057
- if (!verified)
22058
- return failure2("requirements_unsatisfied", "fixed findings require current verification");
22059
- }
22060
- if (entry.data.disposition === "deferred") {
22061
- const valid = entry.data.resolution?.decisionIds.some((id) => {
22062
- const decisionEntry = view.task.decisions.find((item) => item.id === id);
22063
- const decision = decisionEntry?.data;
22064
- 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)));
22065
- });
22066
- if (!valid)
22067
- return failure2("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
22068
- }
22597
+ var requirement = (spec) => ({
22598
+ ...spec,
22599
+ id: requirementId({
22600
+ ruleId: spec.ruleId,
22601
+ scope: spec.scope,
22602
+ satisfaction: spec.satisfaction,
22603
+ before: spec.before,
22604
+ dependentAction: spec.dependentAction
22605
+ })
22606
+ });
22607
+ var signalReason = (input, name) => input.assessment.signals[name].reason;
22608
+ function resolveRequirements(input) {
22609
+ const { signals } = input.assessment;
22610
+ const scope = input.intent.scope;
22611
+ const requirements = [];
22612
+ const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
22613
+ const hasUnknownFact = [
22614
+ ...input.assessment.facts,
22615
+ ...input.assessment.consequences.map(({ fact }) => fact)
22616
+ ].some((fact) => fact.basis === "unknown");
22617
+ if (hasUnknownSignal || hasUnknownFact)
22618
+ requirements.push(requirement({
22619
+ ruleId: "consequential-unknown",
22620
+ dimension: "investigation",
22621
+ scope,
22622
+ reason: `Resolve consequential uncertainty before the dependent action: ${[
22623
+ ...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
22624
+ ...hasUnknownFact ? ["unknown fact"] : []
22625
+ ].join(", ")}`,
22626
+ satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
22627
+ before: "dependent_action",
22628
+ dependentAction: null,
22629
+ acceptanceAllowed: false
22630
+ }));
22631
+ if (signals.productChoiceOpen.value === true)
22632
+ requirements.push(requirement({
22633
+ ruleId: "product-decision",
22634
+ dimension: "decisions",
22635
+ scope,
22636
+ reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
22637
+ satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
22638
+ before: "dependent_action",
22639
+ dependentAction: null,
22640
+ acceptanceAllowed: false
22641
+ }));
22642
+ const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
22643
+ if (signals.behaviorChange.value === true)
22644
+ requirements.push(requirement({
22645
+ ruleId: "behavioral-verification",
22646
+ dimension: "testing",
22647
+ scope,
22648
+ reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
22649
+ satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
22650
+ before: "close",
22651
+ dependentAction: null,
22652
+ acceptanceAllowed: false
22653
+ }), requirement({
22654
+ ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
22655
+ dimension: "review",
22656
+ scope,
22657
+ 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.",
22658
+ 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.",
22659
+ before: "close",
22660
+ dependentAction: null,
22661
+ acceptanceAllowed: !independentBehaviorReview
22662
+ }));
22663
+ else if (signals.mechanicalLowRisk.value === true)
22664
+ requirements.push(requirement({
22665
+ ruleId: "mechanical-existing-checks",
22666
+ dimension: "verification",
22667
+ scope,
22668
+ reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
22669
+ satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
22670
+ before: "close",
22671
+ dependentAction: null,
22672
+ acceptanceAllowed: true
22673
+ }), requirement({
22674
+ ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
22675
+ dimension: "review",
22676
+ scope,
22677
+ 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.",
22678
+ 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.",
22679
+ before: "close",
22680
+ dependentAction: null,
22681
+ acceptanceAllowed: !input.preferences?.thorough
22682
+ }));
22683
+ if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
22684
+ requirements.push(requirement({
22685
+ ruleId: "pre-pr-cleanup",
22686
+ dimension: "verification",
22687
+ scope,
22688
+ reason: "Implementation output must be deslopped before it is delivered.",
22689
+ satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
22690
+ before: "dependent_action",
22691
+ dependentAction: "hosting.pull_request",
22692
+ acceptanceAllowed: true
22693
+ }));
22694
+ if (signals.durableAgreementNeeded.value === true)
22695
+ requirements.push(requirement({
22696
+ ruleId: "durable-spec",
22697
+ dimension: "artifacts",
22698
+ scope,
22699
+ reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
22700
+ satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
22701
+ before: "write",
22702
+ dependentAction: null,
22703
+ acceptanceAllowed: true
22704
+ }));
22705
+ if (signals.coordinationPlanNeeded.value === true)
22706
+ requirements.push(requirement({
22707
+ ruleId: "coordination-plan",
22708
+ dimension: "continuity",
22709
+ scope,
22710
+ reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
22711
+ satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
22712
+ before: "dependent_action",
22713
+ dependentAction: null,
22714
+ acceptanceAllowed: true
22715
+ }));
22716
+ if (signals.helperUseful.value === true && !input.preferences?.fast)
22717
+ requirements.push(requirement({
22718
+ ruleId: "helper-usefulness",
22719
+ dimension: "delegation",
22720
+ scope,
22721
+ reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
22722
+ satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
22723
+ before: "dependent_action",
22724
+ dependentAction: null,
22725
+ acceptanceAllowed: true
22726
+ }));
22727
+ for (const constraint of input.constraints)
22728
+ for (const obligation of constraint.requires) {
22729
+ const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
22730
+ requirements.push(requirement({
22731
+ ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
22732
+ kind: obligation.kind,
22733
+ scope: normalizedScope(obligation.scope),
22734
+ refs: normalizedRefList(obligation.refs),
22735
+ method: obligation.method,
22736
+ before: obligation.before,
22737
+ dependentAction: obligation.dependentAction
22738
+ })}`,
22739
+ dimension,
22740
+ scope: obligation.scope,
22741
+ reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
22742
+ satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
22743
+ before: obligation.before,
22744
+ dependentAction: obligation.dependentAction,
22745
+ acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
22746
+ }));
22069
22747
  }
22070
- }
22071
- const blocking = evaluations.filter((item) => {
22072
- if (item.status !== "unsatisfied" && item.status !== "unavailable")
22748
+ const seen = new Set;
22749
+ return requirements.filter((item) => {
22750
+ if (seen.has(item.id))
22073
22751
  return false;
22074
- const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
22075
- return !requirement || requirement.before === "close";
22076
- });
22077
- if (requestedOutcome !== "stopped" && blocking.length)
22078
- return failure2("requirements_unsatisfied", "applicable requirements are unsatisfied", {
22079
- requirementIds: blocking.map((item) => item.requirementId),
22080
- requirements: blocking.map((item) => {
22081
- const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
22082
- return {
22083
- requirementId: item.requirementId,
22084
- ruleId: requirement?.ruleId ?? "unknown",
22085
- reason: item.reason,
22086
- satisfaction: requirement?.satisfaction ?? "",
22087
- dependentAction: requirement?.dependentAction ?? null
22088
- };
22089
- })
22090
- });
22091
- const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
22092
- if (requestedOutcome === "verified" && accepted.length)
22093
- return failure2("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
22094
- requirementIds: accepted.map((item) => item.requirementId)
22095
- });
22096
- const evidenceIds = [
22097
- ...new Set([
22098
- ...evaluations.flatMap((item) => item.evidenceIds),
22099
- ...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
22100
- ])
22101
- ];
22102
- const decisionIds = [
22103
- ...new Set([
22104
- ...evaluations.flatMap((item) => item.decisionIds),
22105
- ...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
22106
- ])
22107
- ];
22108
- return success(null, null, {
22109
- outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
22110
- evidenceIds,
22111
- decisionIds,
22112
- requirementIds: evaluations.map((item) => item.requirementId)
22113
- });
22114
- }
22115
-
22116
- // packages/workit-core/src/core/methods.ts
22117
- var METHODS = {
22118
- "workit-challenge": { dimensions: ["challenge", "decisions"] },
22119
- "workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
22120
- "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
22121
- "workit-plan": { dimensions: ["artifacts", "continuity"] },
22122
- "workit-implement": { dimensions: ["delegation"] },
22123
- "workit-debug": {},
22124
- "workit-handoff": {},
22125
- "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
22126
- };
22127
- var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
22128
- var assuranceFor = (id, definition, capabilities) => {
22129
- const keys = new Set([
22130
- id,
22131
- ...definition.dimensions ?? [],
22132
- ...definition.ruleIds ?? []
22133
- ]);
22134
- const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
22135
- if (id === "workit-review" && matches.length === 0)
22136
- return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
22137
- if (matches.some((capability) => capability.assurance === "unavailable"))
22138
- return {
22139
- assurance: "unavailable",
22140
- reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
22141
- };
22142
- if (matches.some((capability) => capability.assurance === "enforced"))
22143
- return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
22144
- return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
22145
- };
22146
- function selectMethods(policy, capabilities) {
22147
- return Object.entries(METHODS).flatMap(([id, definition]) => {
22148
- const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
22149
- if (requirements.length === 0)
22150
- return [];
22151
- const capability = assuranceFor(id, definition, capabilities);
22152
- const reasons = [
22153
- ...new Set(requirements.map((requirement) => requirement.reason)),
22154
- ...capability.reasons
22155
- ];
22156
- if (capability.assurance === "unavailable")
22157
- reasons.unshift("required capability is unavailable");
22158
- return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
22752
+ seen.add(item.id);
22753
+ return true;
22159
22754
  });
22160
22755
  }
22161
-
22162
- // packages/workit-core/src/core/task-context.ts
22163
- function reconcileResume(view, observations = []) {
22164
- if (observations.length > 0)
22165
- return failure2("permission_denied", "worker observations require native host verification");
22166
- const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
22167
- if (!candidate.ok)
22168
- return candidate;
22169
- const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
22170
- const blockers = [...view.task.progress.blockers];
22171
- if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
22172
- blockers.push({
22173
- reason: "worker state requires reconciliation",
22174
- dependentAction: "resume",
22175
- refs: []
22176
- });
22177
- return success(null, null, {
22178
- candidate: candidate.data,
22179
- staleEvidenceIds,
22180
- workerUpdates: [],
22181
- blockers,
22182
- reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
22183
- });
22756
+ function resolvePolicy(input) {
22757
+ const normalized = normalizeResolverInput(input);
22758
+ if (!normalized.ok)
22759
+ return normalized;
22760
+ const requirements = resolveRequirements(normalized.data);
22761
+ const policy = {
22762
+ policyVersion: POLICY_VERSION,
22763
+ inputDigest: sha256(canonicalJson(normalized.data)),
22764
+ requirements
22765
+ };
22766
+ const parsed = policySchema.safeParse(policy);
22767
+ if (!parsed.success)
22768
+ return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
22769
+ return success(null, null, parsed.data);
22184
22770
  }
22185
- var COMPACT_MAX_BYTES = 4096;
22186
- var COMPACT_MAX_ITEMS = 8;
22187
- var COMPACT_TEXT_BYTES = 128;
22188
- var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
22189
- var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
22190
- var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
22191
- var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
22192
- const separator = match.search(/[:=]/u);
22193
- if (separator >= 0)
22194
- return `${match.slice(0, separator)}=<redacted>`;
22195
- const space = match.search(/\s/u);
22196
- return `${match.slice(0, space)} <redacted>`;
22197
- }).slice(0, limit);
22198
- var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
22199
- var compactTimestamp = (value) => `${value.slice(0, 19)}.${value.slice(20, -1).padEnd(9, "0")}`;
22200
- var compactDecisionChoice = (entry) => {
22201
- const stated = entry.data.binding.statedChoice?.text;
22202
- const approved = entry.data.response === "approved" ? entry.data.binding.approvedContent : "";
22203
- const source = stated?.trim() || approved.trim() || null;
22204
- return compactText(source, COMPACT_TEXT_BYTES);
22205
- };
22206
- var compactReference = (ref) => {
22207
- if (ref.kind === "file")
22208
- return {
22209
- kind: "file",
22210
- path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
22211
- };
22212
- if (ref.kind === "record")
22213
- return {
22214
- kind: "record",
22215
- collection: ref.collection,
22216
- id: ref.id
22217
- };
22218
- return { kind: ref.kind };
22219
- };
22220
- function compactTaskContext(view) {
22221
- const decisions = view.task.decisions.slice().toSorted((left, right) => {
22222
- const leftAt = compactTimestamp(left.recordedAt);
22223
- const rightAt = compactTimestamp(right.recordedAt);
22224
- if (leftAt !== rightAt)
22225
- return leftAt > rightAt ? -1 : 1;
22226
- return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
22227
- }).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
22228
- id: entry.id,
22229
- purpose: entry.data.purpose,
22230
- status: entry.data.response,
22231
- digest: entry.data.digest,
22232
- choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
22233
- references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
22234
- }));
22235
- const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
22236
- id: method.id,
22237
- assurance: method.assurance,
22238
- reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
22239
- })) : [];
22240
- const gaps = Array.from(new Set([
22241
- ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
22242
- ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
22243
- ...view.task.progress.blockers.map((entry) => entry.reason)
22244
- ])).toSorted().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
22245
- const context = {
22246
- objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
22247
- status: view.task.status,
22248
- decisions,
22249
- methods,
22250
- gaps,
22251
- nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
22771
+ function diffPolicy(previous, next, reason, now) {
22772
+ if (!policySchema.safeParse(previous).success && previous !== null)
22773
+ throw new TypeError("previous policy is invalid");
22774
+ if (!policySchema.safeParse(next).success)
22775
+ throw new TypeError("next policy is invalid");
22776
+ const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
22777
+ const newIds = new Set(next.requirements.map((item) => item.id));
22778
+ const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits3);
22779
+ const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits3);
22780
+ if (added.length === 0 && retired.length === 0)
22781
+ return null;
22782
+ const normalizedReason = reason.trim();
22783
+ const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
22784
+ const change = {
22785
+ recordedAt: now,
22786
+ fromInputDigest: previous?.inputDigest ?? null,
22787
+ toInputDigest: next.inputDigest,
22788
+ added,
22789
+ retired,
22790
+ reason: changeReason
22252
22791
  };
22253
- let encoded = JSON.stringify(context);
22254
- while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
22255
- if (context.gaps.length)
22256
- context.gaps.pop();
22257
- else if (context.decisions.length)
22258
- context.decisions.pop();
22259
- else
22260
- context.methods.pop();
22261
- encoded = JSON.stringify(context);
22262
- }
22263
- if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
22264
- context.objective = compactText(context.objective, 64) ?? "";
22265
- context.nextAction = compactText(context.nextAction, 64);
22266
- encoded = JSON.stringify(context);
22267
- }
22268
- return encoded;
22792
+ const parsed = policyChangeSchema.safeParse(change);
22793
+ if (!parsed.success)
22794
+ throw new TypeError("policy change is invalid");
22795
+ return parsed.data;
22269
22796
  }
22270
- var exportDigest = (bundle) => sha256({
22271
- schemaVersion: bundle.schemaVersion,
22272
- exportedAt: bundle.exportedAt,
22273
- sourceWorkspaceId: bundle.sourceWorkspaceId,
22274
- task: bundle.task
22275
- });
22276
22797
 
22277
22798
  // packages/workit-core/src/core/task-engine.ts
22278
22799
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
@@ -24254,122 +24775,6 @@ class WorkitCore {
24254
24775
  return this.summary(changed.data.task);
24255
24776
  }
24256
24777
  }
24257
-
24258
- // packages/workit-core/src/core/session-context.ts
24259
- var cache = new Map;
24260
- // packages/workit-core/src/core/external-action.ts
24261
- var chainStepSchema = union([
24262
- string2().min(1),
24263
- object2({ branch: string2().min(1) }).strict(),
24264
- object2({ pr: literal(true) }).strict()
24265
- ]);
24266
- var externalActionSchema = discriminatedUnion("operation", [
24267
- object2({
24268
- operation: literal("git.branch_setup"),
24269
- payload: object2({
24270
- action: _enum(["setup", "reapply_stash"]).optional(),
24271
- sdd_dir: string2().optional(),
24272
- target_branch: string2().optional(),
24273
- stash: _enum(["yes", "no"]).optional(),
24274
- cwd: string2().min(1).optional()
24275
- }).strict()
24276
- }).strict(),
24277
- object2({
24278
- operation: literal("git.commit"),
24279
- payload: object2({
24280
- message: string2().min(1).optional(),
24281
- plan_steps: array(chainStepSchema).min(1).max(32).optional(),
24282
- plan_branch: string2().min(1).optional(),
24283
- cwd: string2().min(1).optional()
24284
- }).strict()
24285
- }).strict(),
24286
- object2({
24287
- operation: literal("git.push"),
24288
- payload: object2({ branch: string2().min(1).optional(), cwd: string2().min(1).optional() }).strict()
24289
- }).strict(),
24290
- object2({
24291
- operation: literal("hosting.pull_request"),
24292
- payload: object2({
24293
- title: string2().min(1),
24294
- body: string2().optional(),
24295
- draft: boolean2().optional(),
24296
- target_branch: string2().optional(),
24297
- babysit: boolean2().optional(),
24298
- cwd: string2().min(1).optional()
24299
- }).strict()
24300
- }).strict(),
24301
- object2({
24302
- operation: literal("hosting.merge"),
24303
- payload: object2({
24304
- target_branch: string2().min(1).optional(),
24305
- source_branch: string2().min(1).optional(),
24306
- cwd: string2().min(1).optional()
24307
- }).strict()
24308
- }).strict(),
24309
- object2({
24310
- operation: literal("hosting.delete_branch"),
24311
- payload: object2({ branch: string2().min(1), cwd: string2().min(1).optional() }).strict()
24312
- }).strict(),
24313
- object2({
24314
- operation: literal("youtrack.update"),
24315
- payload: object2({
24316
- issueId: string2().min(1),
24317
- markdown: string2().min(1),
24318
- minutes: number2().positive().optional()
24319
- }).strict()
24320
- }).strict(),
24321
- object2({
24322
- operation: literal("youtrack.time"),
24323
- payload: object2({
24324
- issueId: string2().min(1),
24325
- minutes: number2().positive(),
24326
- text: string2().optional(),
24327
- dateMs: number2().finite().optional()
24328
- }).strict()
24329
- }).strict(),
24330
- object2({
24331
- operation: literal("youtrack.meeting"),
24332
- payload: object2({
24333
- issueId: string2().min(1),
24334
- minutes: number2().positive(),
24335
- text: string2().min(1)
24336
- }).strict()
24337
- }).strict(),
24338
- object2({
24339
- operation: literal("changelog.apply"),
24340
- payload: object2({
24341
- entries: union([
24342
- record(string2(), array(string2())),
24343
- array(object2({ category: string2().min(1), text: string2().min(1) }).strict())
24344
- ]).optional(),
24345
- path: string2().optional(),
24346
- normalize_only: boolean2().optional()
24347
- }).strict()
24348
- }).strict(),
24349
- object2({
24350
- operation: literal("context.read"),
24351
- payload: object2({
24352
- kind: _enum([
24353
- "git",
24354
- "pr",
24355
- "youtrack",
24356
- "github_issue",
24357
- "gitlab_issue",
24358
- "changelog",
24359
- "release",
24360
- "affected"
24361
- ]),
24362
- range: string2().optional(),
24363
- issueId: string2().optional(),
24364
- issueUrl: string2().optional(),
24365
- issueRef: string2().optional(),
24366
- mode: string2().optional(),
24367
- specPath: string2().optional(),
24368
- planPath: string2().optional(),
24369
- cwd: string2().min(1).optional()
24370
- }).strict()
24371
- }).strict()
24372
- ]);
24373
24778
  // packages/workit-core/src/core.ts
24374
24779
  var revision2 = /^[A-Za-z0-9@][A-Za-z0-9@._/~^{}-]*$/;
24375
24780
  function gitRevisionParts(value) {
@@ -24413,7 +24818,7 @@ function resolveInside(root, candidate) {
24413
24818
  }
24414
24819
  function run2(root, executable, args, env = {}) {
24415
24820
  const cwd = realpathSync7(root);
24416
- const result = spawnSync3(executable, args, {
24821
+ const result = spawnSync4(executable, args, {
24417
24822
  cwd,
24418
24823
  encoding: "utf8",
24419
24824
  env: { ...process.env, ...env }
@@ -24484,9 +24889,9 @@ var applyPatterns = (value) => {
24484
24889
  var redactSecrets = (value) => applyPatterns(value);
24485
24890
 
24486
24891
  // packages/workit-core/src/core/pr-create.ts
24487
- import fs8 from "node:fs";
24892
+ import fs9 from "node:fs";
24488
24893
  import path12 from "node:path";
24489
- import { spawnSync as spawnSync4 } from "node:child_process";
24894
+ import { spawnSync as spawnSync5 } from "node:child_process";
24490
24895
  function parseGhIssue(value) {
24491
24896
  const m = /issues\/(\d+)/.exec(value);
24492
24897
  return m ? m[1] : String(value).trim().replace(/^#/, "");
@@ -24499,7 +24904,7 @@ function whichOnPath(tool) {
24499
24904
  for (const name of names) {
24500
24905
  const candidate = path12.join(dir, name);
24501
24906
  try {
24502
- fs8.accessSync(candidate, fs8.constants.X_OK);
24907
+ fs9.accessSync(candidate, fs9.constants.X_OK);
24503
24908
  return candidate;
24504
24909
  } catch {}
24505
24910
  }
@@ -24508,7 +24913,7 @@ function whichOnPath(tool) {
24508
24913
  }
24509
24914
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
24510
24915
  var pushRemote = (cwd) => {
24511
- const result = spawnSync4("git", ["remote", "get-url", "--push", "--all", "origin"], {
24916
+ const result = spawnSync5("git", ["remote", "get-url", "--push", "--all", "origin"], {
24512
24917
  cwd,
24513
24918
  encoding: "utf8"
24514
24919
  });
@@ -24519,7 +24924,7 @@ var pushRemote = (cwd) => {
24519
24924
  };
24520
24925
 
24521
24926
  // packages/workit-core/src/core/repo-context.ts
24522
- import { spawnSync as spawnSync5 } from "node:child_process";
24927
+ import { spawnSync as spawnSync6 } from "node:child_process";
24523
24928
  import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
24524
24929
  import path13 from "node:path";
24525
24930
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
@@ -24548,7 +24953,7 @@ var invalidRange = (cwd) => ({
24548
24953
  cwd
24549
24954
  });
24550
24955
  var runGit = (cwd, args) => {
24551
- const result = spawnSync5("git", args, { cwd, encoding: "utf8" });
24956
+ const result = spawnSync6("git", args, { cwd, encoding: "utf8" });
24552
24957
  return {
24553
24958
  stdout: (result.stdout ?? "").trimEnd(),
24554
24959
  stderr: (result.stderr ?? "").trimEnd(),
@@ -24721,7 +25126,7 @@ var documentationFiles = (cwd) => {
24721
25126
  walk(cwd, 1);
24722
25127
  return matches.toSorted().slice(0, 200);
24723
25128
  };
24724
- var readTrimmed = (file, maxLines) => {
25129
+ var readTrimmed2 = (file, maxLines) => {
24725
25130
  if (!existsSync6(file))
24726
25131
  return "";
24727
25132
  const lines = readFileSync7(file, "utf8").split(`
@@ -24788,7 +25193,7 @@ ${status.stderr}` : "");
24788
25193
  stdout += `template_path: ${template}
24789
25194
 
24790
25195
  `;
24791
- stdout += readTrimmed(path13.join(cwd, template), 220);
25196
+ stdout += readTrimmed2(path13.join(cwd, template), 220);
24792
25197
  } else {
24793
25198
  stdout += `template_path: none
24794
25199
 
@@ -24860,7 +25265,7 @@ function changelogContext(root, range) {
24860
25265
  stdout += printSection("Existing CHANGELOG.md");
24861
25266
  const changelogPath = path13.join(cwd, "CHANGELOG.md");
24862
25267
  if (existsSync6(changelogPath)) {
24863
- stdout += readTrimmed(changelogPath, 260) + `
25268
+ stdout += readTrimmed2(changelogPath, 260) + `
24864
25269
  `;
24865
25270
  } else {
24866
25271
  stdout += `CHANGELOG.md not found.
@@ -24902,7 +25307,7 @@ function docsRefreshContext(root, range) {
24902
25307
  stdout += printSection("README Preview");
24903
25308
  const readme = path13.join(cwd, "README.md");
24904
25309
  if (existsSync6(readme)) {
24905
- stdout += readTrimmed(readme, 220) + `
25310
+ stdout += readTrimmed2(readme, 220) + `
24906
25311
  `;
24907
25312
  } else {
24908
25313
  stdout += `README.md not found.
@@ -24989,7 +25394,7 @@ function releaseNotesContext(root, rangeOrTag) {
24989
25394
  }
24990
25395
 
24991
25396
  // packages/workit-core/src/core/tracker-issues.ts
24992
- import { spawnSync as spawnSync6 } from "node:child_process";
25397
+ import { spawnSync as spawnSync7 } from "node:child_process";
24993
25398
  var repoPathFromRemote = (remote) => {
24994
25399
  let rest = (remote || "").trim().replace(/\/+$/, "");
24995
25400
  if (!rest)
@@ -25010,7 +25415,7 @@ var defaultCli = (bin, available) => {
25010
25415
  if (!available)
25011
25416
  return null;
25012
25417
  return (args, root) => {
25013
- const out = spawnSync6(bin, args, {
25418
+ const out = spawnSync7(bin, args, {
25014
25419
  cwd: root,
25015
25420
  encoding: "utf8",
25016
25421
  env: { ...process.env, PATH: process.env.PATH ?? "" }
@@ -25088,7 +25493,7 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
25088
25493
  };
25089
25494
 
25090
25495
  // packages/workit-core/src/core/youtrack.ts
25091
- import fs9 from "node:fs";
25496
+ import fs10 from "node:fs";
25092
25497
  import path14 from "node:path";
25093
25498
 
25094
25499
  // packages/workit-core/src/core/templates.ts
@@ -25107,17 +25512,17 @@ var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? p
25107
25512
  var youTrackTokenModeOk = (p) => {
25108
25513
  if (process.platform === "win32")
25109
25514
  return true;
25110
- const mode = fs9.statSync(p).mode & 511;
25515
+ const mode = fs10.statSync(p).mode & 511;
25111
25516
  return mode === 384;
25112
25517
  };
25113
25518
  function readYouTrackConfig(required) {
25114
25519
  const cfgPath = youTrackConfigPath();
25115
- if (!fs9.existsSync(cfgPath)) {
25520
+ if (!fs10.existsSync(cfgPath)) {
25116
25521
  return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
25117
25522
  }
25118
25523
  let parsed;
25119
25524
  try {
25120
- parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
25525
+ parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
25121
25526
  } catch {
25122
25527
  return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
25123
25528
  }
@@ -25128,11 +25533,11 @@ function readYouTrackConfig(required) {
25128
25533
  }
25129
25534
  var readYouTrackContextConfig = () => {
25130
25535
  const cfgPath = youTrackReadOnlyConfigPath();
25131
- if (!fs9.existsSync(cfgPath))
25536
+ if (!fs10.existsSync(cfgPath))
25132
25537
  return { data: {} };
25133
25538
  let parsed;
25134
25539
  try {
25135
- parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
25540
+ parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
25136
25541
  } catch {
25137
25542
  return { error: "YouTrack configuration is unavailable" };
25138
25543
  }
@@ -25172,11 +25577,11 @@ var youTrackToken = () => {
25172
25577
  return loaded;
25173
25578
  const tokenFile = String(loaded.config.tokenFile ?? "");
25174
25579
  const tokenPath = tokenFile ? path14.isAbsolute(tokenFile) ? path14.resolve(tokenFile) : path14.resolve(process.cwd(), tokenFile) : "";
25175
- if (!tokenPath || !fs9.existsSync(tokenPath))
25580
+ if (!tokenPath || !fs10.existsSync(tokenPath))
25176
25581
  return { error: "missing youtrack.token" };
25177
25582
  if (!youTrackTokenModeOk(tokenPath))
25178
25583
  return { error: "youtrack.token mode must be 0600" };
25179
- const token = fs9.readFileSync(tokenPath, "utf8").trim();
25584
+ const token = fs10.readFileSync(tokenPath, "utf8").trim();
25180
25585
  if (!token)
25181
25586
  return { error: "empty token file" };
25182
25587
  if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
@@ -25241,9 +25646,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
25241
25646
  } catch {
25242
25647
  continue;
25243
25648
  }
25244
- if (!fs9.existsSync(full))
25649
+ if (!fs10.existsSync(full))
25245
25650
  continue;
25246
- const text = fs9.readFileSync(full, "utf8");
25651
+ const text = fs10.readFileSync(full, "utf8");
25247
25652
  const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
25248
25653
  if (m)
25249
25654
  return m[1];
@@ -25804,72 +26209,168 @@ var main = async () => {
25804
26209
  var entryPath = process.argv[1] ? path16.resolve(process.argv[1]) : "";
25805
26210
  if (entryPath === path16.resolve(fileURLToPath3(import.meta.url)))
25806
26211
  await main();
26212
+ // packages/workit-core/src/hooks/descriptor.ts
26213
+ var support = (descriptor, axis) => {
26214
+ if (axis.startsWith("event:"))
26215
+ return descriptor.events[axis.slice("event:".length)].support;
26216
+ if (axis === "shellPolicy.deny")
26217
+ return descriptor.shellPolicy.deny;
26218
+ if (axis === "stopControl" || axis === "shellAvailable")
26219
+ return descriptor[axis];
26220
+ const [group, key] = axis.split(".");
26221
+ return descriptor[group][key];
26222
+ };
26223
+ var usable = (value) => value === "native" || value === "partial";
26224
+ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map((rule) => {
26225
+ const levels = rule.requires.map((axis) => support(descriptor, axis));
26226
+ const available = rule.assurance !== "unavailable" && levels.every(usable) && (rule.observed ?? []).every((flag) => observed[flag] === true);
26227
+ const assurance = !available ? "unavailable" : rule.assurance === "enforced" && (levels.length === 0 || levels.some((level) => level !== "native")) ? "agent_guided" : rule.assurance;
26228
+ return {
26229
+ name: rule.name,
26230
+ surface: rule.surface,
26231
+ assurance,
26232
+ reason: assurance === "unavailable" ? rule.unavailableReason ?? rule.reason : rule.reason,
26233
+ refs: rule.refs.map((handle) => ({ kind: "host", host: descriptor.host, handle }))
26234
+ };
26235
+ });
26236
+ // packages/workit-core/src/hooks/context.ts
26237
+ var cache = new Map;
26238
+ // packages/workit-core/src/hooks/handle.ts
26239
+ var offered = new Set;
26240
+ // packages/workit-core/src/hooks/hosts/fields.ts
26241
+ var SHELLS = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
25807
26242
 
25808
- // packages/workit-cursor/hooks/workit-hook.ts
25809
- var hostRef = (handle) => ({ kind: "host", host: "cursor", handle });
25810
- var cursorCapabilities = (availability = {}) => {
25811
- const has = (name) => availability[name] === true;
25812
- return [
26243
+ // packages/workit-core/src/hooks/hosts/claude-code.ts
26244
+ var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
26245
+ var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
26246
+ var CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit", "SubagentStart"]);
26247
+
26248
+ // packages/workit-core/src/hooks/hosts/codex.ts
26249
+ var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
26250
+ var SOURCES2 = new Set(["startup", "resume", "clear", "compact"]);
26251
+
26252
+ // packages/workit-core/src/hooks/hosts/cursor.ts
26253
+ var undocumented = { support: "undocumented", native: null };
26254
+ var none = { support: "none", native: null };
26255
+ var CURSOR_DESCRIPTOR = {
26256
+ host: "cursor",
26257
+ label: "Cursor",
26258
+ verifiedAgainst: "Cursor hooks docs; payloads in test/fixtures/hooks/cursor",
26259
+ docs: ["https://cursor.com/docs/agent/hooks"],
26260
+ transport: "hook-process",
26261
+ events: {
26262
+ "session.start": { support: "native", native: "sessionStart" },
26263
+ "context.turn": none,
26264
+ "shell.pre": { support: "native", native: "beforeShellExecution" },
26265
+ "tool.pre": { support: "native", native: "preToolUse" },
26266
+ "shell.post": undocumented,
26267
+ "subagent.start": { support: "native", native: "subagentStart" },
26268
+ "subagent.stop": { support: "partial", native: "subagentStop" },
26269
+ "prompt.submit": undocumented,
26270
+ "compact.pre": { support: "partial", native: "preCompact" },
26271
+ stop: undocumented
26272
+ },
26273
+ shellPolicy: { deny: "native", channel: "exit2+json", failClosed: true },
26274
+ context: {
26275
+ sessionStart: "native",
26276
+ perTurn: "none",
26277
+ afterCompact: "partial",
26278
+ task: "single-active"
26279
+ },
26280
+ subagents: {
26281
+ identity: "native",
26282
+ parentBinding: "native",
26283
+ blockStart: "native",
26284
+ worktreeIsolation: "undocumented",
26285
+ maxConcurrency: "undocumented"
26286
+ },
26287
+ provenance: {
26288
+ sessionId: "native",
26289
+ agentIdOnTool: "undocumented",
26290
+ postToolObserve: "undocumented"
26291
+ },
26292
+ interaction: { questions: "none", writeBoundary: "partial" },
26293
+ stopControl: "undocumented",
26294
+ shellAvailable: "native",
26295
+ perEventCost: "npx-network",
26296
+ capabilities: [
25813
26297
  {
25814
26298
  name: "interactive_decision",
25815
26299
  surface: "AskQuestion",
26300
+ refs: ["AskQuestion"],
26301
+ requires: [],
25816
26302
  assurance: "agent_guided",
25817
- reason: "Cursor does not expose AskQuestion answers to Workit hooks or MCP",
25818
- refs: [hostRef("AskQuestion")]
26303
+ reason: "Cursor does not expose AskQuestion answers to Workit hooks or MCP"
25819
26304
  },
25820
26305
  {
25821
26306
  name: "known_product_writes",
25822
26307
  surface: "preToolUse",
26308
+ refs: ["preToolUse"],
26309
+ requires: [],
25823
26310
  assurance: "unavailable",
25824
- reason: "file writes are host-policy; the Cursor hook no longer gates write tools or shell commands",
25825
- refs: [hostRef("preToolUse")]
26311
+ reason: "file writes are host-policy; the Cursor hook no longer gates write tools or shell commands"
25826
26312
  },
25827
26313
  {
25828
26314
  name: "native_subagents",
25829
26315
  surface: "subagentStart/subagentStop",
25830
- assurance: has("subagentStart") && has("subagentStop") ? "agent_guided" : "unavailable",
25831
- reason: has("subagentStart") && has("subagentStop") ? "reviewer/investigator starts are bounded; Cursor implementer delegation is unavailable and subagentStop lacks a stable child identity" : "Cursor native subagent lifecycle hooks are incomplete",
25832
- refs: [hostRef("subagentStart"), hostRef("subagentStop")]
26316
+ refs: ["subagentStart", "subagentStop"],
26317
+ requires: ["event:subagent.start", "event:subagent.stop"],
26318
+ observed: ["subagent.start", "subagent.stop"],
26319
+ assurance: "agent_guided",
26320
+ reason: "reviewer/investigator starts are bounded; Cursor implementer delegation is unavailable and subagentStop lacks a stable child identity",
26321
+ unavailableReason: "Cursor native subagent lifecycle hooks are incomplete"
25833
26322
  },
25834
26323
  {
25835
26324
  name: "native_subagent_start",
25836
26325
  surface: "subagentStart",
25837
- assurance: has("subagentStart") ? "enforced" : "unavailable",
25838
- reason: has("subagentStart") ? "Cursor subagentStart enforces explicit reviewer/investigator markers; implementer delegation is unavailable" : "Cursor subagentStart is absent",
25839
- refs: [hostRef("subagentStart")]
26326
+ refs: ["subagentStart"],
26327
+ requires: ["event:subagent.start", "subagents.blockStart"],
26328
+ observed: ["subagent.start"],
26329
+ assurance: "enforced",
26330
+ reason: "Cursor subagentStart enforces explicit reviewer/investigator markers; implementer delegation is unavailable",
26331
+ unavailableReason: "Cursor subagentStart is absent"
25840
26332
  },
25841
26333
  {
25842
26334
  name: "fresh-context-review",
25843
26335
  surface: "subagentStart",
25844
- assurance: has("subagentStart") ? "agent_guided" : "unavailable",
25845
- reason: has("subagentStart") ? "independent review runs as a bounded reviewer subagent; stops lack stable identity, so reviewer exclusivity is evaluated from recorded evidence" : "Cursor subagentStart is unavailable for independent review",
25846
- refs: [hostRef("subagentStart")]
26336
+ refs: ["subagentStart"],
26337
+ requires: ["event:subagent.start"],
26338
+ observed: ["subagent.start"],
26339
+ assurance: "agent_guided",
26340
+ reason: "independent review runs as a bounded reviewer subagent; stops lack stable identity, so reviewer exclusivity is evaluated from recorded evidence",
26341
+ unavailableReason: "Cursor subagentStart is unavailable for independent review"
25847
26342
  },
25848
26343
  {
25849
26344
  name: "arbitrary_shell_write",
25850
26345
  surface: "unobservable_shell",
26346
+ refs: ["beforeShellExecution"],
26347
+ requires: [],
25851
26348
  assurance: "unavailable",
25852
- reason: "Only explicitly parsed shell targets are interceptable; arbitrary shell writes are not provable",
25853
- refs: [hostRef("beforeShellExecution")]
26349
+ reason: "Only explicitly parsed shell targets are interceptable; arbitrary shell writes are not provable"
25854
26350
  },
25855
26351
  {
25856
26352
  name: "compact_context",
25857
26353
  surface: "sessionStart/preCompact",
25858
- assurance: has("sessionStart") ? "agent_guided" : "unavailable",
25859
- reason: "sessionStart injects context; preCompact can only show a bounded user reminder",
25860
- refs: [hostRef("sessionStart"), hostRef("preCompact")]
26354
+ refs: ["sessionStart", "preCompact"],
26355
+ requires: ["context.sessionStart"],
26356
+ observed: ["session.start"],
26357
+ assurance: "agent_guided",
26358
+ reason: "sessionStart injects context; preCompact can only show a bounded user reminder"
25861
26359
  }
25862
- ];
26360
+ ]
25863
26361
  };
25864
- var eventNames = new Set([
25865
- "sessionStart",
25866
- "preToolUse",
25867
- "beforeShellExecution",
25868
- "subagentStart",
25869
- "subagentStop",
25870
- "preCompact"
25871
- ]);
25872
- var historyOfferSessions = new Set;
26362
+ var BLOCKING = new Set(["preToolUse", "beforeShellExecution", "subagentStart"]);
26363
+
26364
+ // packages/workit-core/src/hooks/run.ts
26365
+ if (false) {}
26366
+ // packages/workit-cursor/hooks/workit-hook.ts
26367
+ var cursorCapabilities = (availability = {}) => capabilitiesFor(CURSOR_DESCRIPTOR, {
26368
+ "session.start": availability.sessionStart,
26369
+ "tool.pre": availability.preToolUse,
26370
+ "shell.pre": availability.beforeShellExecution,
26371
+ "subagent.start": availability.subagentStart,
26372
+ "subagent.stop": availability.subagentStop
26373
+ });
25873
26374
  if (false)
25874
26375
  ;
25875
26376
  // packages/workit-cursor/mcp/run-server.ts