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