@brainervirus/workit-codex 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.
@@ -6625,7 +6625,7 @@ import { existsSync as existsSync8, realpathSync as realpathSync9, statSync as s
6625
6625
  import path16 from "node:path";
6626
6626
 
6627
6627
  // packages/workit-core/src/core.ts
6628
- import { spawnSync as spawnSync3 } from "node:child_process";
6628
+ import { spawnSync as spawnSync4 } from "node:child_process";
6629
6629
  import { existsSync as existsSync5, realpathSync as realpathSync7 } from "node:fs";
6630
6630
  import path11 from "node:path";
6631
6631
 
@@ -14487,7 +14487,8 @@ var hostSchema = _enum([
14487
14487
  "codex_cli",
14488
14488
  "codex_desktop",
14489
14489
  "pi",
14490
- "workit_cli"
14490
+ "workit_cli",
14491
+ "claude_code"
14491
14492
  ]);
14492
14493
  var assuranceSchema = _enum(["enforced", "agent_guided", "unavailable"]);
14493
14494
  var outcomeSchema = _enum(["verified", "accepted_limitations", "stopped"]);
@@ -14937,15 +14938,70 @@ var taskRecordSchema = object({
14937
14938
  decisions: array(entrySchema(decisionSchema)),
14938
14939
  actionProgress: actionProgressListSchema.optional(),
14939
14940
  findings: array(entrySchema(findingSchema)),
14940
- workers: array(entrySchema(workerSchema))
14941
+ workers: array(entrySchema(workerSchema)),
14942
+ critical: array(nonEmpty).optional()
14941
14943
  }).strict();
14944
+ var stripCount = (strips) => strips.reduce((sum, strip) => sum + strip.keys.length, 0);
14945
+ var strippable = (issues, prefix = []) => {
14946
+ const strips = [];
14947
+ for (const issue of issues) {
14948
+ if (issue.code === "unrecognized_keys") {
14949
+ strips.push({ path: [...prefix, ...issue.path], keys: issue.keys });
14950
+ continue;
14951
+ }
14952
+ if (issue.code !== "invalid_union")
14953
+ return null;
14954
+ 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);
14955
+ if (!branch)
14956
+ return null;
14957
+ strips.push(...branch);
14958
+ }
14959
+ return strips;
14960
+ };
14961
+ var recordPath = (path) => path.map((key) => typeof key === "number" ? "*" : String(key)).join(".");
14962
+ var overlaps = (left, right) => left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
14963
+ var parseStoredRecord = (schema, value) => {
14964
+ let parsed = schema.safeParse(value);
14965
+ if (parsed.success)
14966
+ return { success: true, data: parsed.data, stripped: [] };
14967
+ const first = { success: false, error: parsed.error, critical: [] };
14968
+ 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(".")) : [];
14969
+ const stripped = [];
14970
+ let current = structuredClone(value);
14971
+ for (let round = 0;round < 8 && !parsed.success; round++) {
14972
+ const strips = strippable(parsed.error.issues);
14973
+ if (strips === null || strips.length === 0)
14974
+ return first;
14975
+ for (const strip of strips) {
14976
+ let target = current;
14977
+ for (const key of strip.path)
14978
+ target = typeof target === "object" && target !== null ? target[key] : undefined;
14979
+ if (typeof target !== "object" || target === null)
14980
+ return first;
14981
+ for (const key of strip.keys) {
14982
+ stripped.push(recordPath([...strip.path, key]));
14983
+ delete target[key];
14984
+ }
14985
+ }
14986
+ parsed = schema.safeParse(current);
14987
+ }
14988
+ if (!parsed.success)
14989
+ return first;
14990
+ const critical = [
14991
+ ...new Set(stripped.filter((item) => declared.some((path) => overlaps(item, path))))
14992
+ ];
14993
+ if (critical.length > 0)
14994
+ return { ...first, critical };
14995
+ return { success: true, data: parsed.data, stripped: [...new Set(stripped)] };
14996
+ };
14942
14997
  var workspaceRecordSchema = object({
14943
14998
  schemaVersion: literal(1),
14944
14999
  id,
14945
15000
  revision,
14946
15001
  root: nonEmpty,
14947
15002
  runtime: runtimeSchema.optional(),
14948
- writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
15003
+ writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable(),
15004
+ critical: array(nonEmpty).optional()
14949
15005
  }).strict();
14950
15006
  var capabilitySchema = object({
14951
15007
  name: nonEmpty,
@@ -15183,6 +15239,11 @@ var operationSchemas = {
15183
15239
  writer: discriminatedUnion("action", Object.values(writerOperations)),
15184
15240
  state: discriminatedUnion("action", Object.values(stateOperations))
15185
15241
  };
15242
+ var { recover: _unadvertisedRecover, ...advertisedStateOperations } = stateOperations;
15243
+ var advertisedOperationSchemas = {
15244
+ ...operationSchemas,
15245
+ state: discriminatedUnion("action", Object.values(advertisedStateOperations))
15246
+ };
15186
15247
  var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
15187
15248
  var success = (revisionValue, workspaceRevisionValue, data) => ({
15188
15249
  ok: true,
@@ -15206,7 +15267,7 @@ function parseOperation(family, input) {
15206
15267
  return failure2("invalid_input", "operation input is invalid", { fields });
15207
15268
  }
15208
15269
  function operationJsonSchema(family) {
15209
- return toJSONSchema(operationSchemas[family], { target: "draft-2020-12" });
15270
+ return toJSONSchema(advertisedOperationSchemas[family], { target: "draft-2020-12" });
15210
15271
  }
15211
15272
  var OPERATION_SCHEMA_DEPTH = 1;
15212
15273
  var schemaShapeDescription = (value, depth = 0) => {
@@ -15398,8 +15459,7 @@ function requirementId(input) {
15398
15459
  // packages/workit-core/src/core/task-store.ts
15399
15460
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
15400
15461
  import { AsyncLocalStorage } from "node:async_hooks";
15401
- import * as fs4 from "node:fs";
15402
- import { hostname } from "node:os";
15462
+ import * as fs5 from "node:fs";
15403
15463
  import path4 from "node:path";
15404
15464
 
15405
15465
  // packages/workit-core/src/core/package-root.ts
@@ -16022,13 +16082,10 @@ function acquireFileLockSync(targetPath, options) {
16022
16082
  }
16023
16083
  }
16024
16084
  }
16025
- // packages/workit-core/src/core/task-store.ts
16026
- var processEvidenceSchema = object({
16027
- state: _enum(["stopped", "accounted_for"]),
16028
- pid: number2().int().nonnegative(),
16029
- processStart: string2().nullable(),
16030
- ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
16031
- }).strict();
16085
+ // packages/workit-core/src/core/store-lock.ts
16086
+ import { spawnSync } from "node:child_process";
16087
+ import fs4 from "node:fs";
16088
+ import { hostname } from "node:os";
16032
16089
  var metadataLockSchema = object({
16033
16090
  pid: number2().int().nonnegative().safe(),
16034
16091
  processStart: string2().nullable(),
@@ -16036,6 +16093,143 @@ var metadataLockSchema = object({
16036
16093
  nonce: string2().min(1),
16037
16094
  externalAction: literal(true).optional()
16038
16095
  }).strict();
16096
+ var FOREIGN_LOCK_TTL_MS = 10 * 60000;
16097
+ var UNREADABLE_LOCK_TTL_MS = 30000;
16098
+ var RECLAIM_GUARD_TTL_MS = 30000;
16099
+ var defaultLockTimeoutMs = 250;
16100
+ var defaultLockTimeout = () => defaultLockTimeoutMs;
16101
+ var parseMetadataLock = (raw) => {
16102
+ let value;
16103
+ try {
16104
+ value = JSON.parse(raw);
16105
+ } catch {
16106
+ throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
16107
+ }
16108
+ const parsed = metadataLockSchema.safeParse(value);
16109
+ if (!parsed.success)
16110
+ throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
16111
+ return parsed.data;
16112
+ };
16113
+ var parseMetadataLockOrNull = (raw) => {
16114
+ try {
16115
+ return parseMetadataLock(raw);
16116
+ } catch {
16117
+ return null;
16118
+ }
16119
+ };
16120
+ var sameMetadataLock = (left, right) => {
16121
+ const parsed = metadataLockSchema.safeParse(left);
16122
+ return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
16123
+ };
16124
+ var parseProcStatStart = (stat) => {
16125
+ const close = stat.lastIndexOf(")");
16126
+ if (close < 0)
16127
+ return null;
16128
+ return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
16129
+ };
16130
+ var processStartOf = (pid) => {
16131
+ if (process.platform === "linux") {
16132
+ try {
16133
+ return parseProcStatStart(fs4.readFileSync(`/proc/${pid}/stat`, "utf8"));
16134
+ } catch {
16135
+ return null;
16136
+ }
16137
+ }
16138
+ if (process.platform === "darwin" || process.platform === "freebsd") {
16139
+ try {
16140
+ const run = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
16141
+ encoding: "utf8",
16142
+ timeout: 1000
16143
+ });
16144
+ const value = run.status === 0 ? run.stdout.trim() : "";
16145
+ return value || null;
16146
+ } catch {
16147
+ return null;
16148
+ }
16149
+ }
16150
+ return null;
16151
+ };
16152
+ var readTrimmed = (file) => {
16153
+ try {
16154
+ return fs4.readFileSync(file, "utf8").trim() || null;
16155
+ } catch {
16156
+ return null;
16157
+ }
16158
+ };
16159
+ var cachedLockHost = null;
16160
+ var localLockHost = () => {
16161
+ if (cachedLockHost !== null)
16162
+ return cachedLockHost;
16163
+ let pidns = null;
16164
+ try {
16165
+ pidns = /\[(\d+)\]/.exec(fs4.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
16166
+ } catch {}
16167
+ const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
16168
+ cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
16169
+ return cachedLockHost;
16170
+ };
16171
+ var pidAlive = (pid) => {
16172
+ if (!Number.isSafeInteger(pid) || pid <= 0)
16173
+ return false;
16174
+ try {
16175
+ process.kill(pid, 0);
16176
+ return true;
16177
+ } catch (error) {
16178
+ return error.code === "EPERM";
16179
+ }
16180
+ };
16181
+ var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
16182
+ const lock = metadataLockSchema.safeParse(payload);
16183
+ if (!lock.success)
16184
+ return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
16185
+ const { pid, processStart, host } = lock.data;
16186
+ const [lockName, lockSpace] = host.split("#");
16187
+ const [localName, localSpace] = localHost.split("#");
16188
+ if (lockSpace && localSpace && lockName === localName) {
16189
+ const [lockNs, lockBoot] = lockSpace.split(":");
16190
+ const [localNs, localBoot] = localSpace.split(":");
16191
+ if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
16192
+ return { state: "stale", reason: "lock was taken before this machine rebooted" };
16193
+ }
16194
+ if (host !== localHost)
16195
+ 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` };
16196
+ if (!pidAlive(pid))
16197
+ return { state: "stale", reason: `pid ${pid} is not running` };
16198
+ const currentStart = processStartOf(pid);
16199
+ if (processStart !== null && currentStart !== null && processStart !== currentStart)
16200
+ return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
16201
+ return { state: "live", reason: `held by running pid ${pid}` };
16202
+ };
16203
+ var ageOf = (file, nowMs) => {
16204
+ try {
16205
+ return nowMs - fs4.lstatSync(file).mtimeMs;
16206
+ } catch {
16207
+ return null;
16208
+ }
16209
+ };
16210
+ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
16211
+ const guard = `${lockPath}.reclaim`;
16212
+ const age = ageOf(guard, nowMs);
16213
+ if (age === null || age <= RECLAIM_GUARD_TTL_MS)
16214
+ return false;
16215
+ try {
16216
+ fs4.rmdirSync(guard);
16217
+ return true;
16218
+ } catch {
16219
+ return false;
16220
+ }
16221
+ };
16222
+
16223
+ // packages/workit-core/src/core/task-store.ts
16224
+ var RECOVERY_COPIES_PER_RECORD = 3;
16225
+ var STALE_TEMPORARY_MS = 60 * 60000;
16226
+ var RECOVERY_NAME = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/;
16227
+ var processEvidenceSchema = object({
16228
+ state: _enum(["stopped", "accounted_for"]),
16229
+ pid: number2().int().nonnegative(),
16230
+ processStart: string2().nullable(),
16231
+ ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
16232
+ }).strict();
16039
16233
  var INDEX_VERSION = 1;
16040
16234
  var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
16041
16235
  var indexEntry = (task, file) => {
@@ -16063,7 +16257,7 @@ var indexEntry = (task, file) => {
16063
16257
  };
16064
16258
  var fileSignature = (file) => {
16065
16259
  try {
16066
- const stat = fs4.statSync(file, { bigint: true });
16260
+ const stat = fs5.statSync(file, { bigint: true });
16067
16261
  return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
16068
16262
  } catch {
16069
16263
  return null;
@@ -16080,7 +16274,7 @@ var cachedRuntimeVersion = null;
16080
16274
  var runtimeVersion = () => {
16081
16275
  if (cachedRuntimeVersion === null) {
16082
16276
  try {
16083
- const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
16277
+ const pkg = JSON.parse(fs5.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
16084
16278
  cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
16085
16279
  } catch {
16086
16280
  cachedRuntimeVersion = "0.0.0";
@@ -16109,36 +16303,20 @@ var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
16109
16303
  var isObject2 = (value) => typeof value === "object" && value !== null;
16110
16304
  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);
16111
16305
  var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
16112
- var parseMetadataLock = (raw) => {
16113
- let value;
16114
- try {
16115
- value = JSON.parse(raw);
16116
- } catch {
16117
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
16118
- }
16119
- const parsed = metadataLockSchema.safeParse(value);
16120
- if (!parsed.success)
16121
- throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
16122
- return parsed.data;
16123
- };
16124
- var sameMetadataLock = (left, right) => {
16125
- const parsed = metadataLockSchema.safeParse(left);
16126
- return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
16127
- };
16128
16306
  var sameDirectoryIdentity = (left, right) => {
16129
16307
  if (!path4.isAbsolute(left) || !path4.isAbsolute(right))
16130
16308
  return false;
16131
16309
  const normalizedLeft = path4.resolve(left);
16132
16310
  const normalizedRight = path4.resolve(right);
16133
16311
  try {
16134
- const a = fs4.statSync(normalizedLeft, { bigint: true });
16135
- const b = fs4.statSync(normalizedRight, { bigint: true });
16312
+ const a = fs5.statSync(normalizedLeft, { bigint: true });
16313
+ const b = fs5.statSync(normalizedRight, { bigint: true });
16136
16314
  if (!a.isDirectory() || !b.isDirectory())
16137
16315
  return false;
16138
16316
  if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
16139
16317
  return true;
16140
- const canonicalLeft = fs4.realpathSync(normalizedLeft);
16141
- const canonicalRight = fs4.realpathSync(normalizedRight);
16318
+ const canonicalLeft = fs5.realpathSync(normalizedLeft);
16319
+ const canonicalRight = fs5.realpathSync(normalizedRight);
16142
16320
  if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
16143
16321
  return true;
16144
16322
  return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
@@ -16146,12 +16324,47 @@ var sameDirectoryIdentity = (left, right) => {
16146
16324
  return false;
16147
16325
  }
16148
16326
  };
16327
+ var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
16328
+ var isTransientWindowsError = (error) => {
16329
+ if (process.platform !== "win32")
16330
+ return false;
16331
+ const code = error?.code;
16332
+ return typeof code === "string" && TRANSIENT_WINDOWS_CODES.has(code);
16333
+ };
16334
+ var retryTransient = (run) => {
16335
+ if (process.platform !== "win32")
16336
+ return run();
16337
+ for (let attempt = 0;; attempt += 1) {
16338
+ try {
16339
+ return run();
16340
+ } catch (error) {
16341
+ if (attempt >= 20 || !isTransientWindowsError(error))
16342
+ throw error;
16343
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 * (attempt + 1));
16344
+ }
16345
+ }
16346
+ };
16149
16347
  var externalActionLockRoots = new AsyncLocalStorage;
16348
+ var heldInProcess = new Map;
16349
+ var holdRoot = (root) => heldInProcess.set(root, (heldInProcess.get(root) ?? 0) + 1);
16350
+ var dropRoot = (root) => {
16351
+ const count = (heldInProcess.get(root) ?? 1) - 1;
16352
+ if (count > 0)
16353
+ heldInProcess.set(root, count);
16354
+ else
16355
+ heldInProcess.delete(root);
16356
+ };
16357
+ var dedupeCandidates = (candidates) => {
16358
+ const last = new Map(candidates.map((candidate, index) => [candidate.id, index]));
16359
+ return candidates.filter((candidate, index) => last.get(candidate.id) === index);
16360
+ };
16150
16361
 
16151
16362
  class TaskStore {
16152
16363
  root;
16153
- constructor(root) {
16154
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
16364
+ lockTimeoutMs;
16365
+ constructor(root, options = {}) {
16366
+ this.root = fs5.existsSync(root) ? fs5.realpathSync(root) : path4.resolve(root);
16367
+ this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
16155
16368
  }
16156
16369
  readTask(taskId) {
16157
16370
  if (!validId(taskId))
@@ -16171,11 +16384,11 @@ class TaskStore {
16171
16384
  return result.result;
16172
16385
  }
16173
16386
  listTasks() {
16174
- if (!fs4.existsSync(this.tasksDir))
16387
+ if (!fs5.existsSync(this.tasksDir))
16175
16388
  return success(null, null, []);
16176
16389
  let names;
16177
16390
  try {
16178
- names = fs4.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
16391
+ names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
16179
16392
  } catch (error) {
16180
16393
  return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
16181
16394
  path: this.tasksDir
@@ -16204,11 +16417,11 @@ class TaskStore {
16204
16417
  return success(null, null, tasks);
16205
16418
  }
16206
16419
  listTaskIndex() {
16207
- if (!fs4.existsSync(this.tasksDir))
16420
+ if (!fs5.existsSync(this.tasksDir))
16208
16421
  return success(null, null, []);
16209
16422
  let names;
16210
16423
  try {
16211
- names = fs4.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
16424
+ names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
16212
16425
  } catch (error) {
16213
16426
  return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
16214
16427
  path: this.tasksDir
@@ -16576,12 +16789,12 @@ class TaskStore {
16576
16789
  });
16577
16790
  }
16578
16791
  recoveryCandidates() {
16579
- if (!fs4.existsSync(this.recoveryDir))
16792
+ if (!fs5.existsSync(this.recoveryDir))
16580
16793
  return success(null, null, []);
16581
16794
  try {
16582
16795
  const candidates = [];
16583
- for (const name of fs4.readdirSync(this.recoveryDir)) {
16584
- const match = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/.exec(name);
16796
+ for (const name of fs5.readdirSync(this.recoveryDir)) {
16797
+ const match = RECOVERY_NAME.exec(name);
16585
16798
  if (match)
16586
16799
  candidates.push({
16587
16800
  target: match[1],
@@ -16629,7 +16842,7 @@ class TaskStore {
16629
16842
  return failure2("recovery_required", "validated recovery snapshot not found");
16630
16843
  selectedBytes = candidate;
16631
16844
  }
16632
- const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema) : this.parseBytes(selectedBytes, taskRecordSchema);
16845
+ const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
16633
16846
  if (!selectedRecord.ok)
16634
16847
  return selectedRecord;
16635
16848
  if (target === "workspace") {
@@ -16675,14 +16888,14 @@ class TaskStore {
16675
16888
  return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
16676
16889
  }
16677
16890
  if (target === "workspace") {
16678
- const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema);
16891
+ const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
16679
16892
  if (!parsed.ok)
16680
16893
  return parsed;
16681
16894
  const value = { ...parsed.data, revision: newRevision(), writer: null };
16682
16895
  const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
16683
16896
  return replaced.ok ? success(value.revision, value.revision, value) : replaced;
16684
16897
  }
16685
- const parsed = this.parseBytes(selectedBytes, taskRecordSchema);
16898
+ const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
16686
16899
  if (!parsed.ok)
16687
16900
  return parsed;
16688
16901
  const value = {
@@ -16693,7 +16906,7 @@ class TaskStore {
16693
16906
  };
16694
16907
  const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
16695
16908
  return replaced.ok ? success(value.revision, null, value) : replaced;
16696
- }, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate));
16909
+ }, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
16697
16910
  return result;
16698
16911
  } catch (error) {
16699
16912
  return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
@@ -16728,12 +16941,12 @@ class TaskStore {
16728
16941
  }
16729
16942
  readLockSnapshot() {
16730
16943
  try {
16731
- const stat = fs4.lstatSync(this.lockPath);
16944
+ const stat = fs5.lstatSync(this.lockPath);
16732
16945
  if (!stat.isFile() || stat.isSymbolicLink())
16733
16946
  return failure2("recovery_required", "metadata lock is not a regular file", {
16734
16947
  path: this.lockPath
16735
16948
  });
16736
- const raw = fs4.readFileSync(this.lockPath, "utf8");
16949
+ const raw = fs5.readFileSync(this.lockPath, "utf8");
16737
16950
  return success(null, null, { raw, data: parseMetadataLock(raw) });
16738
16951
  } catch (error) {
16739
16952
  if (error?.code === "ENOENT")
@@ -16744,22 +16957,50 @@ class TaskStore {
16744
16957
  }
16745
16958
  }
16746
16959
  metadataLockOptions() {
16960
+ const inProcess = heldInProcess.has(this.root);
16747
16961
  return {
16748
16962
  lockPath: this.lockPath,
16749
16963
  staleMs: Number.MAX_SAFE_INTEGER,
16750
- timeoutMs: 0,
16751
- retry: { retries: 0 },
16752
- staleRecovery: "fail-closed",
16753
- shouldReclaim: () => false,
16754
- parsePayload: parseMetadataLock,
16964
+ timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
16965
+ retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
16966
+ staleRecovery: "remove-if-unchanged",
16967
+ shouldReclaim: ({ payload, nowMs }) => {
16968
+ let ageMs = null;
16969
+ try {
16970
+ ageMs = nowMs - fs5.lstatSync(this.lockPath).mtimeMs;
16971
+ } catch {}
16972
+ return classifyLockOwner(payload, ageMs).state === "stale";
16973
+ },
16974
+ shouldRemoveStaleLock: () => true,
16975
+ parsePayload: parseMetadataLockOrNull,
16755
16976
  payload: () => ({
16756
16977
  pid: process.pid,
16757
- processStart: this.processStart(process.pid),
16758
- host: hostname(),
16978
+ processStart: processStartOf(process.pid),
16979
+ host: localLockHost(),
16759
16980
  nonce: randomUUID2()
16760
16981
  })
16761
16982
  };
16762
16983
  }
16984
+ acquireMetadataLock(options) {
16985
+ clearAbandonedReclaimGuard(this.lockPath);
16986
+ const deadline = Date.now() + (options.timeoutMs ?? 0);
16987
+ while (true) {
16988
+ try {
16989
+ return acquireFileLockSync(this.workspacePath, {
16990
+ ...options,
16991
+ timeoutMs: Math.max(0, deadline - Date.now())
16992
+ });
16993
+ } catch (error) {
16994
+ const code = error?.code;
16995
+ if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
16996
+ throw error;
16997
+ if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
16998
+ throw error;
16999
+ if (code !== "file_lock_stale")
17000
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
17001
+ }
17002
+ }
17003
+ }
16763
17004
  externalActionLockOptions() {
16764
17005
  const options = this.metadataLockOptions();
16765
17006
  return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
@@ -16777,10 +17018,11 @@ class TaskStore {
16777
17018
  }
16778
17019
  let handle;
16779
17020
  try {
16780
- handle = acquireFileLockSync(this.workspacePath, this.externalActionLockOptions());
17021
+ handle = this.acquireMetadataLock(this.externalActionLockOptions());
16781
17022
  } catch (error) {
16782
17023
  return this.lockFailure(error);
16783
17024
  }
17025
+ holdRoot(this.root);
16784
17026
  let result;
16785
17027
  try {
16786
17028
  if (!handle.verifyStillHeld())
@@ -16815,8 +17057,9 @@ class TaskStore {
16815
17057
  path: this.lockPath
16816
17058
  });
16817
17059
  }
17060
+ dropRoot(this.root);
16818
17061
  try {
16819
- handle.release();
17062
+ retryTransient(() => handle.release());
16820
17063
  } catch (error) {
16821
17064
  const released = this.lockFailure(error);
16822
17065
  const releaseError = released.ok ? "metadata lock release failed" : released.error;
@@ -16829,6 +17072,9 @@ class TaskStore {
16829
17072
  }
16830
17073
  recoveryLockOptions(observed, evidence, gate) {
16831
17074
  const options = this.metadataLockOptions();
17075
+ options.timeoutMs = 0;
17076
+ options.retry = { retries: 0 };
17077
+ options.parsePayload = parseMetadataLock;
16832
17078
  options.staleRecovery = "remove-if-unchanged";
16833
17079
  options.shouldReclaim = ({ payload }) => Boolean(observed && sameMetadataLock(payload, observed.data) && (evidence.state === "stopped" || evidence.state === "accounted_for"));
16834
17080
  options.shouldRemoveStaleLock = ({ raw, payload }) => {
@@ -16850,7 +17096,7 @@ class TaskStore {
16850
17096
  };
16851
17097
  this.replaceSnapshot(this.workspacePath, value, workspace);
16852
17098
  }
16853
- withLock(operation, options = this.metadataLockOptions()) {
17099
+ withLock(operation, options = this.metadataLockOptions(), contention = "busy") {
16854
17100
  try {
16855
17101
  this.initializeMutationStorage();
16856
17102
  } catch (error) {
@@ -16861,10 +17107,12 @@ class TaskStore {
16861
17107
  let handle;
16862
17108
  let result = failure2("storage_error", "metadata lock operation did not produce a result");
16863
17109
  try {
16864
- handle = acquireFileLockSync(this.workspacePath, options);
17110
+ handle = this.acquireMetadataLock(options);
16865
17111
  } catch (error) {
16866
- result = this.lockFailure(error);
17112
+ result = this.lockFailure(error, contention);
16867
17113
  }
17114
+ if (handle)
17115
+ holdRoot(this.root);
16868
17116
  if (handle) {
16869
17117
  try {
16870
17118
  if (!handle.verifyStillHeld())
@@ -16883,8 +17131,9 @@ class TaskStore {
16883
17131
  }
16884
17132
  }
16885
17133
  if (handle) {
17134
+ dropRoot(this.root);
16886
17135
  try {
16887
- handle.release();
17136
+ retryTransient(() => handle.release());
16888
17137
  } catch (error) {
16889
17138
  const releaseFailure = this.lockFailure(error);
16890
17139
  const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
@@ -16893,25 +17142,65 @@ class TaskStore {
16893
17142
  }
16894
17143
  return result;
16895
17144
  }
16896
- lockFailure(error) {
17145
+ lockDirWritable() {
17146
+ const probe = `${this.lockPath}.${process.pid}.${randomUUID2()}.probe`;
17147
+ try {
17148
+ fs5.closeSync(fs5.openSync(probe, "wx", 384));
17149
+ } catch {
17150
+ return false;
17151
+ }
17152
+ try {
17153
+ fs5.rmSync(probe, { force: true });
17154
+ } catch {}
17155
+ return true;
17156
+ }
17157
+ lockHolderPresent() {
17158
+ try {
17159
+ fs5.lstatSync(this.lockPath);
17160
+ return true;
17161
+ } catch (error) {
17162
+ return error?.code !== "ENOENT";
17163
+ }
17164
+ }
17165
+ lockFailure(error, contention = "busy") {
16897
17166
  const value = error;
16898
17167
  const code = typeof value?.code === "string" ? value.code : "";
16899
- if (code === "EEXIST" || code === "file_lock_timeout") {
16900
- const lock = this.readLockSnapshot();
16901
- if (lock.ok && lock.data?.data.externalAction)
17168
+ if (isTransientWindowsError(error) && !this.lockHolderPresent() && !this.lockDirWritable())
17169
+ return failure2("storage_error", `cannot create the workspace metadata lock (${code}); no other Workit call holds it`, {
17170
+ path: this.lockPath,
17171
+ guidance: `Check permissions and the read-only attribute on ${path4.dirname(this.lockPath)}.`
17172
+ });
17173
+ if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
17174
+ let lock = null;
17175
+ try {
17176
+ lock = parseMetadataLockOrNull(fs5.readFileSync(this.lockPath, "utf8"));
17177
+ } catch {}
17178
+ if (lock?.externalAction)
16902
17179
  return failure2("writer_conflict", "workspace is reserved by a managed external action", {
16903
17180
  outcome: "not_started",
16904
17181
  path: this.lockPath
16905
17182
  });
17183
+ if (contention === "busy")
17184
+ return failure2("busy", `workspace metadata lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
17185
+ outcome: "not_started",
17186
+ path: this.lockPath,
17187
+ guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
17188
+ });
16906
17189
  }
16907
17190
  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));
16908
17191
  return failure2(recovery ? "recovery_required" : "storage_error", `metadata lock operation failed: ${String(error)}`, { path: this.lockPath });
16909
17192
  }
16910
17193
  initializeMutationStorage() {
16911
- fs4.mkdirSync(this.tasksDir, { recursive: true });
16912
- fs4.mkdirSync(this.recoveryDir, { recursive: true });
16913
- fs4.writeFileSync(this.gitignorePath, `*
16914
- `);
17194
+ retryTransient(() => fs5.mkdirSync(this.tasksDir, { recursive: true }));
17195
+ retryTransient(() => fs5.mkdirSync(this.recoveryDir, { recursive: true }));
17196
+ let current = null;
17197
+ try {
17198
+ current = retryTransient(() => fs5.readFileSync(this.gitignorePath, "utf8"));
17199
+ } catch {}
17200
+ if (current !== `*
17201
+ `)
17202
+ retryTransient(() => fs5.writeFileSync(this.gitignorePath, `*
17203
+ `));
16915
17204
  }
16916
17205
  replaceSnapshot(file, value, previous) {
16917
17206
  let temporary;
@@ -16923,14 +17212,14 @@ class TaskStore {
16923
17212
  else if (typeof previous === "string")
16924
17213
  this.saveRecovery(file, previous);
16925
17214
  temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
16926
- const fd = fs4.openSync(temporary, "wx", 384);
17215
+ const fd = fs5.openSync(temporary, "wx", 384);
16927
17216
  try {
16928
- fs4.writeSync(fd, jsonBytes(value));
16929
- fs4.fsyncSync(fd);
17217
+ fs5.writeSync(fd, jsonBytes(value));
17218
+ fs5.fsyncSync(fd);
16930
17219
  } finally {
16931
- fs4.closeSync(fd);
17220
+ fs5.closeSync(fd);
16932
17221
  }
16933
- fs4.renameSync(temporary, file);
17222
+ retryTransient(() => fs5.renameSync(temporary, file));
16934
17223
  temporary = undefined;
16935
17224
  this.fsyncDirectory(path4.dirname(file));
16936
17225
  if (path4.dirname(file) === this.tasksDir)
@@ -16943,13 +17232,13 @@ class TaskStore {
16943
17232
  } finally {
16944
17233
  if (temporary)
16945
17234
  try {
16946
- fs4.unlinkSync(temporary);
17235
+ fs5.unlinkSync(temporary);
16947
17236
  } catch {}
16948
17237
  }
16949
17238
  }
16950
17239
  readIndex(workspaceId) {
16951
17240
  try {
16952
- const value = JSON.parse(fs4.readFileSync(this.indexPath, "utf8"));
17241
+ const value = JSON.parse(fs5.readFileSync(this.indexPath, "utf8"));
16953
17242
  if (!isObject2(value) || value.version !== INDEX_VERSION || value.workspaceId !== workspaceId || !isObject2(value.tasks))
16954
17243
  return {};
16955
17244
  const tasks = {};
@@ -16964,15 +17253,15 @@ class TaskStore {
16964
17253
  writeIndex(workspaceId, entries) {
16965
17254
  const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
16966
17255
  try {
16967
- fs4.writeFileSync(temporary, JSON.stringify({
17256
+ fs5.writeFileSync(temporary, JSON.stringify({
16968
17257
  version: INDEX_VERSION,
16969
17258
  workspaceId,
16970
17259
  tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
16971
17260
  }), { mode: 384, flag: "wx" });
16972
- fs4.renameSync(temporary, this.indexPath);
17261
+ retryTransient(() => fs5.renameSync(temporary, this.indexPath));
16973
17262
  } catch {
16974
17263
  try {
16975
- fs4.unlinkSync(temporary);
17264
+ fs5.unlinkSync(temporary);
16976
17265
  } catch {}
16977
17266
  }
16978
17267
  }
@@ -16990,33 +17279,153 @@ class TaskStore {
16990
17279
  const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
16991
17280
  const id = target === "task" ? path4.basename(file, ".json") : "workspace";
16992
17281
  const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
16993
- if (fs4.existsSync(destination)) {
16994
- if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
16995
- return;
16996
- throw new Error("recovery copy already exists with different bytes");
17282
+ if (fs5.existsSync(destination)) {
17283
+ if (digestBytes(retryTransient(() => fs5.readFileSync(destination))) !== digestBytes(bytes))
17284
+ throw new Error("recovery copy already exists with different bytes");
17285
+ const stamp = new Date;
17286
+ retryTransient(() => fs5.utimesSync(destination, stamp, stamp));
17287
+ this.pruneRecovery(`${target}.${id}.`, destination);
17288
+ return;
16997
17289
  }
16998
17290
  let temporary;
16999
17291
  try {
17000
17292
  temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
17001
- const fd = fs4.openSync(temporary, "wx", 384);
17293
+ const fd = fs5.openSync(temporary, "wx", 384);
17002
17294
  try {
17003
- fs4.writeSync(fd, bytes);
17004
- fs4.fsyncSync(fd);
17295
+ fs5.writeSync(fd, bytes);
17296
+ fs5.fsyncSync(fd);
17005
17297
  } finally {
17006
- fs4.closeSync(fd);
17298
+ fs5.closeSync(fd);
17007
17299
  }
17008
- fs4.renameSync(temporary, destination);
17300
+ retryTransient(() => fs5.renameSync(temporary, destination));
17009
17301
  temporary = undefined;
17010
17302
  this.fsyncDirectory(this.recoveryDir);
17303
+ this.pruneRecovery(`${target}.${id}.`, destination);
17011
17304
  } finally {
17012
17305
  if (temporary)
17013
17306
  try {
17014
- fs4.unlinkSync(temporary);
17307
+ fs5.unlinkSync(temporary);
17015
17308
  } catch {}
17016
17309
  }
17017
17310
  }
17311
+ pruneRecovery(prefix, keep) {
17312
+ try {
17313
+ const copies = this.recoveryCopies(prefix);
17314
+ const surplus = this.surplusCopies(copies, path4.basename(keep));
17315
+ for (const copy of surplus)
17316
+ fs5.rmSync(copy.path, { force: true });
17317
+ } catch {}
17318
+ }
17319
+ recoveryCopies(prefix = "") {
17320
+ if (!fs5.existsSync(this.recoveryDir))
17321
+ return [];
17322
+ const copies = [];
17323
+ for (const name of fs5.readdirSync(this.recoveryDir)) {
17324
+ if (!name.startsWith(prefix))
17325
+ continue;
17326
+ const match = RECOVERY_NAME.exec(name);
17327
+ if (!match)
17328
+ continue;
17329
+ const file = path4.join(this.recoveryDir, name);
17330
+ try {
17331
+ const stat = fs5.lstatSync(file, { bigint: true });
17332
+ if (!stat.isFile())
17333
+ continue;
17334
+ copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
17335
+ } catch {}
17336
+ }
17337
+ return copies;
17338
+ }
17339
+ surplusCopies(copies, keep) {
17340
+ 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);
17341
+ return ordered.slice(RECOVERY_COPIES_PER_RECORD);
17342
+ }
17343
+ collectGarbage(options = {}) {
17344
+ const dryRun = options.dryRun === true;
17345
+ const report = {
17346
+ dryRun,
17347
+ recovery: { removed: 0, removedBytes: 0, kept: 0 },
17348
+ temporary: { removed: 0 },
17349
+ candidates: { removed: 0, tasks: [], skippedActive: [], skippedClosed: [], failed: [] }
17350
+ };
17351
+ if (!fs5.existsSync(this.workitDir))
17352
+ return success(null, null, report);
17353
+ const sweep = () => {
17354
+ try {
17355
+ const groups = new Map;
17356
+ for (const copy of this.recoveryCopies())
17357
+ groups.set(copy.group, [...groups.get(copy.group) ?? [], copy]);
17358
+ for (const copies of groups.values()) {
17359
+ const surplus = this.surplusCopies(copies);
17360
+ report.recovery.kept += copies.length - surplus.length;
17361
+ for (const copy of surplus) {
17362
+ report.recovery.removed += 1;
17363
+ try {
17364
+ report.recovery.removedBytes += fs5.statSync(copy.path).size;
17365
+ } catch {}
17366
+ if (!dryRun)
17367
+ fs5.rmSync(copy.path, { force: true });
17368
+ }
17369
+ }
17370
+ const nowMs = Date.now();
17371
+ for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
17372
+ if (!fs5.existsSync(directory))
17373
+ continue;
17374
+ for (const name of fs5.readdirSync(directory)) {
17375
+ if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
17376
+ continue;
17377
+ const file = path4.join(directory, name);
17378
+ const stat = fs5.lstatSync(file);
17379
+ if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
17380
+ continue;
17381
+ report.temporary.removed += 1;
17382
+ if (!dryRun)
17383
+ fs5.rmSync(file, { force: true });
17384
+ }
17385
+ }
17386
+ return success(null, null, null);
17387
+ } catch (error) {
17388
+ return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
17389
+ path: this.recoveryDir
17390
+ });
17391
+ }
17392
+ };
17393
+ const pruned = dryRun ? sweep() : this.withLock(sweep);
17394
+ if (!pruned.ok)
17395
+ return pruned;
17396
+ const tasks = this.listTasks();
17397
+ if (!tasks.ok)
17398
+ return tasks;
17399
+ for (const task of tasks.data) {
17400
+ const deduped = dedupeCandidates(task.candidates);
17401
+ const removed = task.candidates.length - deduped.length;
17402
+ if (removed === 0)
17403
+ continue;
17404
+ if (task.status === "active") {
17405
+ report.candidates.skippedActive.push(task.id);
17406
+ continue;
17407
+ }
17408
+ if (task.status === "closed") {
17409
+ report.candidates.skippedClosed.push(task.id);
17410
+ continue;
17411
+ }
17412
+ if (!dryRun) {
17413
+ const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
17414
+ ...current,
17415
+ candidates: dedupeCandidates(current.candidates)
17416
+ }));
17417
+ if (!written.ok) {
17418
+ report.candidates.failed.push(task.id);
17419
+ continue;
17420
+ }
17421
+ }
17422
+ report.candidates.removed += removed;
17423
+ report.candidates.tasks.push(task.id);
17424
+ }
17425
+ return success(null, null, report);
17426
+ }
17018
17427
  findRecovery(target, taskId, digest, workspace) {
17019
- if (!fs4.existsSync(this.recoveryDir))
17428
+ if (!fs5.existsSync(this.recoveryDir))
17020
17429
  return null;
17021
17430
  const workspaceId = workspace?.id ?? this.trustedWorkspaceId();
17022
17431
  if (target === "workspace" && !workspaceId)
@@ -17028,9 +17437,9 @@ class TaskStore {
17028
17437
  if (candidate.target !== target || candidate.digest !== digest)
17029
17438
  continue;
17030
17439
  try {
17031
- if (!fs4.lstatSync(candidate.path).isFile())
17440
+ if (!fs5.lstatSync(candidate.path).isFile())
17032
17441
  continue;
17033
- const bytes = fs4.readFileSync(candidate.path);
17442
+ const bytes = fs5.readFileSync(candidate.path);
17034
17443
  if (digestBytes(bytes) !== digest)
17035
17444
  continue;
17036
17445
  if (target === "workspace") {
@@ -17048,11 +17457,11 @@ class TaskStore {
17048
17457
  }
17049
17458
  fsyncDirectory(directory) {
17050
17459
  try {
17051
- const fd = fs4.openSync(directory, "r");
17460
+ const fd = fs5.openSync(directory, "r");
17052
17461
  try {
17053
- fs4.fsyncSync(fd);
17462
+ fs5.fsyncSync(fd);
17054
17463
  } finally {
17055
- fs4.closeSync(fd);
17464
+ fs5.closeSync(fd);
17056
17465
  }
17057
17466
  } catch (error) {
17058
17467
  if (process.platform !== "win32")
@@ -17060,14 +17469,14 @@ class TaskStore {
17060
17469
  }
17061
17470
  }
17062
17471
  trustedWorkspaceId() {
17063
- if (!fs4.existsSync(this.tasksDir))
17472
+ if (!fs5.existsSync(this.tasksDir))
17064
17473
  return null;
17065
17474
  const ids = new Set;
17066
17475
  try {
17067
- for (const name of fs4.readdirSync(this.tasksDir)) {
17476
+ for (const name of fs5.readdirSync(this.tasksDir)) {
17068
17477
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
17069
17478
  return null;
17070
- const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
17479
+ const parsed = this.parseBytes(fs5.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
17071
17480
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
17072
17481
  return null;
17073
17482
  ids.add(parsed.data.workspaceId);
@@ -17079,14 +17488,14 @@ class TaskStore {
17079
17488
  }
17080
17489
  snapshotBytes(file) {
17081
17490
  try {
17082
- return fs4.readFileSync(file);
17491
+ return retryTransient(() => fs5.readFileSync(file));
17083
17492
  } catch {
17084
17493
  return null;
17085
17494
  }
17086
17495
  }
17087
17496
  readRecord(file, schema) {
17088
17497
  try {
17089
- const bytes = fs4.readFileSync(file, "utf8");
17498
+ const bytes = retryTransient(() => fs5.readFileSync(file, "utf8"));
17090
17499
  return { exists: true, result: this.parseBytes(bytes, schema) };
17091
17500
  } catch (error) {
17092
17501
  if (error?.code === "ENOENT")
@@ -17099,7 +17508,7 @@ class TaskStore {
17099
17508
  };
17100
17509
  }
17101
17510
  }
17102
- parseBytes(bytes, schema) {
17511
+ parseBytes(bytes, schema, mode = "read") {
17103
17512
  let value;
17104
17513
  try {
17105
17514
  value = JSON.parse(typeof bytes === "string" ? bytes : bytes.toString("utf8"));
@@ -17108,21 +17517,19 @@ class TaskStore {
17108
17517
  }
17109
17518
  if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
17110
17519
  return failure2("unsupported_version", "unsupported snapshot schema version");
17111
- const parsed = schema.safeParse(value);
17112
- if (parsed.success)
17520
+ const parsed = parseStoredRecord(schema, value);
17521
+ if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
17113
17522
  return success(null, null, parsed.data);
17114
17523
  const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
17524
+ const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
17525
+ if (parsed.success)
17526
+ return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
17527
+ if (parsed.critical.length > 0)
17528
+ return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
17115
17529
  if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
17116
17530
  return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
17117
17531
  return failure2("recovery_required", "snapshot does not satisfy its schema");
17118
17532
  }
17119
- processStart(pid) {
17120
- try {
17121
- return fs4.readFileSync(`/proc/${pid}/stat`, "utf8").split(" ")[21] ?? null;
17122
- } catch {
17123
- return null;
17124
- }
17125
- }
17126
17533
  conflict(expected, actual) {
17127
17534
  return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
17128
17535
  expectedRevision: expected,
@@ -17154,55 +17561,118 @@ class TaskStore {
17154
17561
  return path4.join(this.workitDir, ".gitignore");
17155
17562
  }
17156
17563
  }
17157
- // packages/workit-core/src/core/task-engine.ts
17158
- import { realpathSync as realpathSync6 } from "node:fs";
17159
-
17160
- // packages/workit-core/src/core/authority.ts
17161
- import { createHash as createHash5 } from "node:crypto";
17162
- import * as fs6 from "node:fs";
17163
- import path9 from "node:path";
17164
-
17165
- // packages/workit-core/src/core/auto-approval.ts
17166
- import { createHash as createHash4 } from "node:crypto";
17167
- import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
17168
- import path8 from "node:path";
17169
-
17170
- // packages/workit-core/src/core/workspaces.ts
17171
- import { createHash as createHash3 } from "node:crypto";
17172
- import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
17173
- import path6 from "node:path";
17174
-
17175
- // packages/workit-core/src/core/config.ts
17176
- import {
17177
- copyFileSync,
17178
- cpSync,
17179
- existsSync as existsSync3,
17180
- mkdirSync as mkdirSync2,
17181
- readFileSync as readFileSync2,
17182
- readdirSync as readdirSync2,
17183
- writeFileSync as writeFileSync2
17184
- } from "node:fs";
17185
- import os from "node:os";
17186
- import path5 from "node:path";
17187
-
17188
- // packages/workit-core/src/core/boundary.ts
17189
- import { statSync as statSync2 } from "node:fs";
17190
- var EVENT = {
17191
- initialization: "initialization",
17192
- provenance: "provenance",
17193
- assets: "assets",
17194
- configurationSource: "configuration_source",
17195
- hooks: "hooks",
17196
- mcpConnection: "mcp_connection",
17197
- migration: "migration",
17198
- installSteps: "install_steps",
17199
- uncaughtFailure: "uncaught_failure",
17200
- toolsFailed: "tools_failed",
17201
- doctor: "doctor"
17202
- };
17203
- var errorDetail = (err) => {
17204
- if (err instanceof Error) {
17205
- return { error_name: err.name, error: err.message };
17564
+ // packages/workit-core/src/core/workers.ts
17565
+ var same = (left, right) => {
17566
+ try {
17567
+ return canonicalJson(left) === canonicalJson(right);
17568
+ } catch {
17569
+ return false;
17570
+ }
17571
+ };
17572
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
17573
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
17574
+ var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
17575
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
17576
+ var validPath = (value) => typeof value === "string" && value.length > 0 && !Array.from(value).some((char) => {
17577
+ const code = char.charCodeAt(0);
17578
+ return code < 32 || code === 127;
17579
+ }) && !value.split(/[\\/]/).includes("..");
17580
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
17581
+ var workerIdOf = (value) => value.workerId ?? null;
17582
+ var currentWriterOwnsTask = (task, workspace, caller) => {
17583
+ const owner = workspace.writer?.owner;
17584
+ return task.status === "active" && task.workspaceId === workspace.id && owner?.taskId === task.id && owner.session.host === caller.host && owner.session.handle === caller.actor;
17585
+ };
17586
+ function assertProductWriteAllowed(input) {
17587
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
17588
+ return failure2("invalid_input", "invalid product write path");
17589
+ if (!input.store)
17590
+ return failure2("permission_denied", "write authorization requires core state");
17591
+ const task = input.store.readTask(input.task.id);
17592
+ if (!task.ok)
17593
+ return task;
17594
+ const workspace = input.store.readWorkspace();
17595
+ if (!workspace.ok)
17596
+ return workspace;
17597
+ if (!workspace.data)
17598
+ return failure2("not_found", "workspace not found");
17599
+ const workspaceRecord = workspace.data;
17600
+ if (task.data.status !== "active")
17601
+ return failure2("invalid_transition", "paused or closed tasks cannot own product writes");
17602
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
17603
+ return failure2("recovery_required", "task and workspace bindings are invalid");
17604
+ const writer = workspaceRecord.writer;
17605
+ if (!writer)
17606
+ return failure2("permission_denied", "checkout has no writer owner");
17607
+ if (writer.state === "uncertain")
17608
+ return failure2("recovery_required", "checkout writer requires recovery", {
17609
+ owner: writer.owner
17610
+ });
17611
+ const caller = callerValue(input.caller);
17612
+ const workerId = workerIdOf(input.caller);
17613
+ const owner = writer.owner;
17614
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
17615
+ return failure2("writer_conflict", "checkout is owned by another validated actor", { owner });
17616
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
17617
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
17618
+ return failure2("permission_denied", "only implementers can own product writes");
17619
+ 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))))
17620
+ return failure2("recovery_required", "worker session is not an active writer");
17621
+ return success(null, workspaceRecord.revision, owner);
17622
+ }
17623
+
17624
+ // packages/workit-core/src/core/task-evaluation.ts
17625
+ import { createHash as createHash6 } from "node:crypto";
17626
+ import * as fs8 from "node:fs";
17627
+ import { spawnSync as spawnSync3 } from "node:child_process";
17628
+ import path10 from "node:path";
17629
+
17630
+ // packages/workit-core/src/core/authority.ts
17631
+ import { createHash as createHash5 } from "node:crypto";
17632
+ import * as fs7 from "node:fs";
17633
+ import path9 from "node:path";
17634
+
17635
+ // packages/workit-core/src/core/auto-approval.ts
17636
+ import { createHash as createHash4 } from "node:crypto";
17637
+ import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
17638
+ import path8 from "node:path";
17639
+
17640
+ // packages/workit-core/src/core/workspaces.ts
17641
+ import { createHash as createHash3 } from "node:crypto";
17642
+ import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
17643
+ import path6 from "node:path";
17644
+
17645
+ // packages/workit-core/src/core/config.ts
17646
+ import {
17647
+ copyFileSync,
17648
+ cpSync,
17649
+ existsSync as existsSync3,
17650
+ mkdirSync as mkdirSync2,
17651
+ readFileSync as readFileSync2,
17652
+ readdirSync as readdirSync2,
17653
+ writeFileSync as writeFileSync2
17654
+ } from "node:fs";
17655
+ import os from "node:os";
17656
+ import path5 from "node:path";
17657
+
17658
+ // packages/workit-core/src/core/boundary.ts
17659
+ import { statSync as statSync2 } from "node:fs";
17660
+ var EVENT = {
17661
+ initialization: "initialization",
17662
+ provenance: "provenance",
17663
+ assets: "assets",
17664
+ configurationSource: "configuration_source",
17665
+ hooks: "hooks",
17666
+ mcpConnection: "mcp_connection",
17667
+ migration: "migration",
17668
+ installSteps: "install_steps",
17669
+ uncaughtFailure: "uncaught_failure",
17670
+ toolsFailed: "tools_failed",
17671
+ doctor: "doctor"
17672
+ };
17673
+ var errorDetail = (err) => {
17674
+ if (err instanceof Error) {
17675
+ return { error_name: err.name, error: err.message };
17206
17676
  }
17207
17677
  return { error_name: "unknown", error: String(err) };
17208
17678
  };
@@ -17829,13 +18299,13 @@ var gitContext = (workspaceRoot, paths = []) => {
17829
18299
  };
17830
18300
 
17831
18301
  // packages/workit-core/src/core/vcs-config.ts
17832
- import fs5 from "node:fs";
18302
+ import fs6 from "node:fs";
17833
18303
  import path7 from "node:path";
17834
- import { spawnSync } from "node:child_process";
18304
+ import { spawnSync as spawnSync2 } from "node:child_process";
17835
18305
  var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
17836
18306
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
17837
18307
  var remoteProvider = (cwd) => {
17838
- const r = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
18308
+ const r = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
17839
18309
  if (r.status !== 0)
17840
18310
  return null;
17841
18311
  const url = (r.stdout ?? "").trim();
@@ -17872,7 +18342,7 @@ var remoteEndpoint = (remote) => {
17872
18342
  var configuredSshEndpoint = (host) => {
17873
18343
  if (host.startsWith("-"))
17874
18344
  return null;
17875
- const resolved = spawnSync("ssh", ["-G", host], {
18345
+ const resolved = spawnSync2("ssh", ["-G", host], {
17876
18346
  encoding: "utf8",
17877
18347
  timeout: 5000,
17878
18348
  maxBuffer: 1024 * 1024,
@@ -17922,7 +18392,7 @@ var readVcsConfig = () => {
17922
18392
  const file = vcsConfigPath();
17923
18393
  let raw;
17924
18394
  try {
17925
- raw = fs5.readFileSync(file, "utf8");
18395
+ raw = fs6.readFileSync(file, "utf8");
17926
18396
  } catch {
17927
18397
  return { status: "missing", path: file, config: {} };
17928
18398
  }
@@ -18023,7 +18493,7 @@ function vcsCliIdentity(cwd) {
18023
18493
  PATH: process.env.PATH ?? "",
18024
18494
  ...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
18025
18495
  };
18026
- const result = spawnSync(bin, ["api", "user"], {
18496
+ const result = spawnSync2(bin, ["api", "user"], {
18027
18497
  cwd: root,
18028
18498
  encoding: "utf8",
18029
18499
  env
@@ -18065,7 +18535,7 @@ function mergedPrStyle(limit = 6, cwd) {
18065
18535
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
18066
18536
  descriptionPreview: desc.slice(0, 600)
18067
18537
  });
18068
- const remoteResult = spawnSync("git", ["remote", "get-url", "--push", "--all", "origin"], {
18538
+ const remoteResult = spawnSync2("git", ["remote", "get-url", "--push", "--all", "origin"], {
18069
18539
  cwd,
18070
18540
  encoding: "utf8"
18071
18541
  });
@@ -18081,7 +18551,7 @@ function mergedPrStyle(limit = 6, cwd) {
18081
18551
  if (!hostingApiHostMatches(remote, host))
18082
18552
  return { ok: false, error: "PR style target does not match the provider host" };
18083
18553
  if (provider === "gitlab") {
18084
- const run = (args) => spawnSync("glab", ["api", ...args], {
18554
+ const run = (args) => spawnSync2("glab", ["api", ...args], {
18085
18555
  cwd,
18086
18556
  encoding: "utf8",
18087
18557
  env
@@ -18101,7 +18571,7 @@ function mergedPrStyle(limit = 6, cwd) {
18101
18571
  });
18102
18572
  }
18103
18573
  } else if (provider === "github") {
18104
- const r = spawnSync("gh", [
18574
+ const r = spawnSync2("gh", [
18105
18575
  "pr",
18106
18576
  "list",
18107
18577
  "--repo",
@@ -18160,66 +18630,6 @@ var verifyPushIdentity = (root, provider, account) => {
18160
18630
  return { ok: true };
18161
18631
  };
18162
18632
 
18163
- // packages/workit-core/src/core/workers.ts
18164
- var same = (left, right) => {
18165
- try {
18166
- return canonicalJson(left) === canonicalJson(right);
18167
- } catch {
18168
- return false;
18169
- }
18170
- };
18171
- var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
18172
- var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
18173
- var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
18174
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
18175
- var validPath = (value) => typeof value === "string" && value.length > 0 && !Array.from(value).some((char) => {
18176
- const code = char.charCodeAt(0);
18177
- return code < 32 || code === 127;
18178
- }) && !value.split(/[\\/]/).includes("..");
18179
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
18180
- var workerIdOf = (value) => value.workerId ?? null;
18181
- var currentWriterOwnsTask = (task, workspace, caller) => {
18182
- const owner = workspace.writer?.owner;
18183
- return task.status === "active" && task.workspaceId === workspace.id && owner?.taskId === task.id && owner.session.host === caller.host && owner.session.handle === caller.actor;
18184
- };
18185
- function assertProductWriteAllowed(input) {
18186
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
18187
- return failure2("invalid_input", "invalid product write path");
18188
- if (!input.store)
18189
- return failure2("permission_denied", "write authorization requires core state");
18190
- const task = input.store.readTask(input.task.id);
18191
- if (!task.ok)
18192
- return task;
18193
- const workspace = input.store.readWorkspace();
18194
- if (!workspace.ok)
18195
- return workspace;
18196
- if (!workspace.data)
18197
- return failure2("not_found", "workspace not found");
18198
- const workspaceRecord = workspace.data;
18199
- if (task.data.status !== "active")
18200
- return failure2("invalid_transition", "paused or closed tasks cannot own product writes");
18201
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
18202
- return failure2("recovery_required", "task and workspace bindings are invalid");
18203
- const writer = workspaceRecord.writer;
18204
- if (!writer)
18205
- return failure2("permission_denied", "checkout has no writer owner");
18206
- if (writer.state === "uncertain")
18207
- return failure2("recovery_required", "checkout writer requires recovery", {
18208
- owner: writer.owner
18209
- });
18210
- const caller = callerValue(input.caller);
18211
- const workerId = workerIdOf(input.caller);
18212
- const owner = writer.owner;
18213
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
18214
- return failure2("writer_conflict", "checkout is owned by another validated actor", { owner });
18215
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
18216
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
18217
- return failure2("permission_denied", "only implementers can own product writes");
18218
- 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))))
18219
- return failure2("recovery_required", "worker session is not an active writer");
18220
- return success(null, workspaceRecord.revision, owner);
18221
- }
18222
-
18223
18633
  // packages/workit-core/src/core/auto-approval.ts
18224
18634
  var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
18225
18635
  var OPERATION_CLASS = {
@@ -18506,7 +18916,7 @@ var digestBytes2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
18506
18916
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
18507
18917
  let root;
18508
18918
  try {
18509
- root = fs6.realpathSync(checkoutRoot);
18919
+ root = fs7.realpathSync(checkoutRoot);
18510
18920
  } catch {
18511
18921
  return failure2("permission_denied", "checkout root is unavailable");
18512
18922
  }
@@ -18529,8 +18939,8 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
18529
18939
  if (!segment)
18530
18940
  continue;
18531
18941
  current = path9.join(current, segment);
18532
- const stat = fs6.lstatSync(current);
18533
- const real = fs6.realpathSync(current);
18942
+ const stat = fs7.lstatSync(current);
18943
+ const real = fs7.realpathSync(current);
18534
18944
  if (real !== root && !real.startsWith(`${root}${path9.sep}`))
18535
18945
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
18536
18946
  fields: [
@@ -18547,7 +18957,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
18547
18957
  if (current !== target && !stat.isDirectory())
18548
18958
  return failure2("invalid_input", "document reference ancestor is not a directory");
18549
18959
  }
18550
- if (digestBytes2(fs6.readFileSync(target)) !== reference.digest)
18960
+ if (digestBytes2(fs7.readFileSync(target)) !== reference.digest)
18551
18961
  return failure2("permission_denied", "approved document bytes have changed");
18552
18962
  } catch {
18553
18963
  return failure2("invalid_input", "approved document is unavailable");
@@ -18910,18 +19320,41 @@ function reconcileAction(input) {
18910
19320
  var bindingCovers = scopeCovers;
18911
19321
  var verifyDecisionContent = verifyContentRefs;
18912
19322
 
18913
- // packages/workit-core/src/core/policy-resolver.ts
18914
- var resolverInputSchema = object({
18915
- intent: intentSchema,
18916
- assessment: assessmentSchema,
18917
- constraints: array(constraintSchema),
18918
- prior: object({
18919
- decisions: array(decisionSchema),
18920
- findings: array(findingSchema),
18921
- requirements: array(requirementSchema)
18922
- }).strict(),
18923
- preferences: object({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
18924
- }).strict();
19323
+ // packages/workit-core/src/core/task-evaluation.ts
19324
+ var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
19325
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
19326
+ var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
19327
+ var canonicalPath = (value) => {
19328
+ if (typeof value !== "string" || !value)
19329
+ return null;
19330
+ if (value.includes("\\") || value.split("/").includes(".."))
19331
+ return null;
19332
+ const normalized = path10.posix.normalize(value);
19333
+ if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
19334
+ return null;
19335
+ return normalized === "." ? "." : normalized.replace(/\/$/, "");
19336
+ };
19337
+ var canonicalScope = (scope) => {
19338
+ const paths = scope.paths.length ? scope.paths : ["."];
19339
+ const normalizedPaths = paths.map(canonicalPath);
19340
+ const normalizedExclusions = scope.exclusions.map(canonicalPath);
19341
+ if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
19342
+ return null;
19343
+ return {
19344
+ description: scope.description,
19345
+ paths: [...new Set(normalizedPaths)],
19346
+ exclusions: [...new Set(normalizedExclusions)]
19347
+ };
19348
+ };
19349
+ var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
19350
+ var scopeMatches = (value, scope) => {
19351
+ const normalized = canonicalScope(scope);
19352
+ if (!normalized)
19353
+ return false;
19354
+ const paths = normalized.paths;
19355
+ return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
19356
+ };
19357
+ var pathRelevant = (value, scope) => scopeMatches(value, scope);
18925
19358
  var compareCodeUnits2 = (left, right) => {
18926
19359
  const length = Math.min(left.length, right.length);
18927
19360
  for (let index = 0;index < length; index += 1) {
@@ -18931,362 +19364,8 @@ var compareCodeUnits2 = (left, right) => {
18931
19364
  }
18932
19365
  return left.length - right.length;
18933
19366
  };
18934
- var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits2);
18935
- var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits2(canonicalJson(left), canonicalJson(right)));
18936
- var normalizedScope = (scope) => ({
18937
- description: scope.description,
18938
- paths: stableUnique(scope.paths),
18939
- exclusions: stableUnique(scope.exclusions)
18940
- });
18941
- var normalizedRefList = (values) => stableList(values);
18942
- var normalizeAssessment = (assessment) => ({
18943
- facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
18944
- signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
18945
- name,
18946
- { ...signal, refs: normalizedRefList(signal.refs) }
18947
- ])),
18948
- consequences: stableList(assessment.consequences.map((consequence) => ({
18949
- ...consequence,
18950
- fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
18951
- }))),
18952
- verification: stableList(assessment.verification.map((verification) => ({
18953
- ...verification,
18954
- scope: normalizedScope(verification.scope),
18955
- availableChecks: normalizedRefList(verification.availableChecks),
18956
- gaps: stableUnique(verification.gaps)
18957
- })))
18958
- });
18959
- var normalizeConstraint = (constraint) => ({
18960
- ...constraint,
18961
- source: constraint.source,
18962
- requires: stableList(constraint.requires.map((requirement) => ({
18963
- ...requirement,
18964
- scope: normalizedScope(requirement.scope),
18965
- refs: normalizedRefList(requirement.refs)
18966
- })))
18967
- });
18968
- var normalizeDecision = (decision) => ({
18969
- ...decision,
18970
- binding: {
18971
- ...decision.binding,
18972
- scope: normalizedScope(decision.binding.scope),
18973
- contentRefs: normalizedRefList(decision.binding.contentRefs)
18974
- },
18975
- requirementIds: stableUnique(decision.requirementIds)
18976
- });
18977
- var normalizeIntent = (intent) => ({
18978
- ...intent,
18979
- scope: normalizedScope(intent.scope),
18980
- authorityRefs: normalizedRefList(intent.authorityRefs)
18981
- });
18982
- var normalizePrior = (prior) => ({
18983
- decisions: stableList(prior.decisions.map(normalizeDecision)),
18984
- findings: stableList(prior.findings.map((finding) => ({
18985
- ...finding,
18986
- scope: normalizedScope(finding.scope),
18987
- refs: normalizedRefList(finding.refs),
18988
- resolution: finding.resolution ? {
18989
- ...finding.resolution,
18990
- evidenceIds: stableUnique(finding.resolution.evidenceIds),
18991
- decisionIds: stableUnique(finding.resolution.decisionIds)
18992
- } : null
18993
- }))),
18994
- requirements: stableList(prior.requirements.map((requirement) => ({
18995
- ...requirement,
18996
- scope: normalizedScope(requirement.scope)
18997
- })))
18998
- });
18999
- var invalidFields = (error) => ({
19000
- fields: error.issues.map((issue) => ({
19001
- path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
19002
- reason: issue.message
19003
- }))
19004
- });
19005
- function normalizeResolverInput(input) {
19006
- const parsed = resolverInputSchema.safeParse(input);
19007
- if (!parsed.success)
19008
- return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
19009
- const value = parsed.data;
19010
- const signals = value.assessment.signals;
19011
- if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
19012
- return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
19013
- fields: [
19014
- {
19015
- path: "assessment.signals",
19016
- reason: "contradictory behaviorChange and mechanicalLowRisk"
19017
- }
19018
- ]
19019
- });
19020
- const ids = value.constraints.map((constraint) => constraint.id);
19021
- if (new Set(ids).size !== ids.length)
19022
- return failure2("invalid_input", "constraint IDs must be unique", {
19023
- fields: [{ path: "constraints", reason: "duplicate constraint id" }]
19024
- });
19025
- if (value.preferences?.fast && value.preferences.thorough)
19026
- return failure2("invalid_input", "fast and thorough preferences contradict", {
19027
- fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
19028
- });
19029
- const normalized = {
19030
- intent: normalizeIntent(value.intent),
19031
- assessment: normalizeAssessment(value.assessment),
19032
- constraints: stableList(value.constraints.map(normalizeConstraint)),
19033
- prior: normalizePrior(value.prior),
19034
- ...value.preferences ? { preferences: { ...value.preferences } } : {},
19035
- policyVersion: POLICY_VERSION
19036
- };
19037
- return success(null, null, normalized);
19038
- }
19039
- var requirement = (spec) => ({
19040
- ...spec,
19041
- id: requirementId({
19042
- ruleId: spec.ruleId,
19043
- scope: spec.scope,
19044
- satisfaction: spec.satisfaction,
19045
- before: spec.before,
19046
- dependentAction: spec.dependentAction
19047
- })
19048
- });
19049
- var signalReason = (input, name) => input.assessment.signals[name].reason;
19050
- function resolveRequirements(input) {
19051
- const { signals } = input.assessment;
19052
- const scope = input.intent.scope;
19053
- const requirements = [];
19054
- const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
19055
- const hasUnknownFact = [
19056
- ...input.assessment.facts,
19057
- ...input.assessment.consequences.map(({ fact }) => fact)
19058
- ].some((fact) => fact.basis === "unknown");
19059
- if (hasUnknownSignal || hasUnknownFact)
19060
- requirements.push(requirement({
19061
- ruleId: "consequential-unknown",
19062
- dimension: "investigation",
19063
- scope,
19064
- reason: `Resolve consequential uncertainty before the dependent action: ${[
19065
- ...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
19066
- ...hasUnknownFact ? ["unknown fact"] : []
19067
- ].join(", ")}`,
19068
- satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
19069
- before: "dependent_action",
19070
- dependentAction: null,
19071
- acceptanceAllowed: false
19072
- }));
19073
- if (signals.productChoiceOpen.value === true)
19074
- requirements.push(requirement({
19075
- ruleId: "product-decision",
19076
- dimension: "decisions",
19077
- scope,
19078
- reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
19079
- satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
19080
- before: "dependent_action",
19081
- dependentAction: null,
19082
- acceptanceAllowed: false
19083
- }));
19084
- const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
19085
- if (signals.behaviorChange.value === true)
19086
- requirements.push(requirement({
19087
- ruleId: "behavioral-verification",
19088
- dimension: "testing",
19089
- scope,
19090
- reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
19091
- satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
19092
- before: "close",
19093
- dependentAction: null,
19094
- acceptanceAllowed: false
19095
- }), requirement({
19096
- ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
19097
- dimension: "review",
19098
- scope,
19099
- 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.",
19100
- 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.",
19101
- before: "close",
19102
- dependentAction: null,
19103
- acceptanceAllowed: !independentBehaviorReview
19104
- }));
19105
- else if (signals.mechanicalLowRisk.value === true)
19106
- requirements.push(requirement({
19107
- ruleId: "mechanical-existing-checks",
19108
- dimension: "verification",
19109
- scope,
19110
- reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
19111
- satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
19112
- before: "close",
19113
- dependentAction: null,
19114
- acceptanceAllowed: true
19115
- }), requirement({
19116
- ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
19117
- dimension: "review",
19118
- scope,
19119
- 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.",
19120
- 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.",
19121
- before: "close",
19122
- dependentAction: null,
19123
- acceptanceAllowed: !input.preferences?.thorough
19124
- }));
19125
- if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
19126
- requirements.push(requirement({
19127
- ruleId: "pre-pr-cleanup",
19128
- dimension: "verification",
19129
- scope,
19130
- reason: "Implementation output must be deslopped before it is delivered.",
19131
- satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
19132
- before: "dependent_action",
19133
- dependentAction: "hosting.pull_request",
19134
- acceptanceAllowed: true
19135
- }));
19136
- if (signals.durableAgreementNeeded.value === true)
19137
- requirements.push(requirement({
19138
- ruleId: "durable-spec",
19139
- dimension: "artifacts",
19140
- scope,
19141
- reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
19142
- satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
19143
- before: "write",
19144
- dependentAction: null,
19145
- acceptanceAllowed: true
19146
- }));
19147
- if (signals.coordinationPlanNeeded.value === true)
19148
- requirements.push(requirement({
19149
- ruleId: "coordination-plan",
19150
- dimension: "continuity",
19151
- scope,
19152
- reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
19153
- satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
19154
- before: "dependent_action",
19155
- dependentAction: null,
19156
- acceptanceAllowed: true
19157
- }));
19158
- if (signals.helperUseful.value === true && !input.preferences?.fast)
19159
- requirements.push(requirement({
19160
- ruleId: "helper-usefulness",
19161
- dimension: "delegation",
19162
- scope,
19163
- reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
19164
- satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
19165
- before: "dependent_action",
19166
- dependentAction: null,
19167
- acceptanceAllowed: true
19168
- }));
19169
- for (const constraint of input.constraints)
19170
- for (const obligation of constraint.requires) {
19171
- const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
19172
- requirements.push(requirement({
19173
- ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
19174
- kind: obligation.kind,
19175
- scope: normalizedScope(obligation.scope),
19176
- refs: normalizedRefList(obligation.refs),
19177
- method: obligation.method,
19178
- before: obligation.before,
19179
- dependentAction: obligation.dependentAction
19180
- })}`,
19181
- dimension,
19182
- scope: obligation.scope,
19183
- reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
19184
- satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
19185
- before: obligation.before,
19186
- dependentAction: obligation.dependentAction,
19187
- acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
19188
- }));
19189
- }
19190
- const seen = new Set;
19191
- return requirements.filter((item) => {
19192
- if (seen.has(item.id))
19193
- return false;
19194
- seen.add(item.id);
19195
- return true;
19196
- });
19197
- }
19198
- function resolvePolicy(input) {
19199
- const normalized = normalizeResolverInput(input);
19200
- if (!normalized.ok)
19201
- return normalized;
19202
- const requirements = resolveRequirements(normalized.data);
19203
- const policy = {
19204
- policyVersion: POLICY_VERSION,
19205
- inputDigest: sha256(canonicalJson(normalized.data)),
19206
- requirements
19207
- };
19208
- const parsed = policySchema.safeParse(policy);
19209
- if (!parsed.success)
19210
- return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
19211
- return success(null, null, parsed.data);
19212
- }
19213
- function diffPolicy(previous, next, reason, now) {
19214
- if (!policySchema.safeParse(previous).success && previous !== null)
19215
- throw new TypeError("previous policy is invalid");
19216
- if (!policySchema.safeParse(next).success)
19217
- throw new TypeError("next policy is invalid");
19218
- const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
19219
- const newIds = new Set(next.requirements.map((item) => item.id));
19220
- const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits2);
19221
- const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits2);
19222
- if (added.length === 0 && retired.length === 0)
19223
- return null;
19224
- const normalizedReason = reason.trim();
19225
- const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
19226
- const change = {
19227
- recordedAt: now,
19228
- fromInputDigest: previous?.inputDigest ?? null,
19229
- toInputDigest: next.inputDigest,
19230
- added,
19231
- retired,
19232
- reason: changeReason
19233
- };
19234
- const parsed = policyChangeSchema.safeParse(change);
19235
- if (!parsed.success)
19236
- throw new TypeError("policy change is invalid");
19237
- return parsed.data;
19238
- }
19239
-
19240
- // packages/workit-core/src/core/task-evaluation.ts
19241
- import { createHash as createHash6 } from "node:crypto";
19242
- import * as fs7 from "node:fs";
19243
- import { spawnSync as spawnSync2 } from "node:child_process";
19244
- import path10 from "node:path";
19245
- var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
19246
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
19247
- var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
19248
- var canonicalPath = (value) => {
19249
- if (typeof value !== "string" || !value)
19250
- return null;
19251
- if (value.includes("\\") || value.split("/").includes(".."))
19252
- return null;
19253
- const normalized = path10.posix.normalize(value);
19254
- if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
19255
- return null;
19256
- return normalized === "." ? "." : normalized.replace(/\/$/, "");
19257
- };
19258
- var canonicalScope = (scope) => {
19259
- const paths = scope.paths.length ? scope.paths : ["."];
19260
- const normalizedPaths = paths.map(canonicalPath);
19261
- const normalizedExclusions = scope.exclusions.map(canonicalPath);
19262
- if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
19263
- return null;
19264
- return {
19265
- description: scope.description,
19266
- paths: [...new Set(normalizedPaths)],
19267
- exclusions: [...new Set(normalizedExclusions)]
19268
- };
19269
- };
19270
- var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
19271
- var scopeMatches = (value, scope) => {
19272
- const normalized = canonicalScope(scope);
19273
- if (!normalized)
19274
- return false;
19275
- const paths = normalized.paths;
19276
- return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
19277
- };
19278
- var pathRelevant = (value, scope) => scopeMatches(value, scope);
19279
- var compareCodeUnits3 = (left, right) => {
19280
- const length = Math.min(left.length, right.length);
19281
- for (let index = 0;index < length; index += 1) {
19282
- const difference = left.charCodeAt(index) - right.charCodeAt(index);
19283
- if (difference !== 0)
19284
- return difference;
19285
- }
19286
- return left.length - right.length;
19287
- };
19288
19367
  var gitPaths = (root) => {
19289
- const result = spawnSync2("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
19368
+ const result = spawnSync3("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
19290
19369
  cwd: root,
19291
19370
  encoding: "buffer"
19292
19371
  });
@@ -19299,7 +19378,7 @@ var gitPaths = (root) => {
19299
19378
  };
19300
19379
  }
19301
19380
  const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")) : [];
19302
- const stagedDeleted = spawnSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
19381
+ const stagedDeleted = spawnSync3("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
19303
19382
  if (stagedDeleted.status !== 0) {
19304
19383
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
19305
19384
  if (!/not a git repository/i.test(stderr))
@@ -19312,7 +19391,7 @@ var gitPaths = (root) => {
19312
19391
  var walk = (root, directory, output) => {
19313
19392
  let names;
19314
19393
  try {
19315
- names = fs7.readdirSync(directory);
19394
+ names = fs8.readdirSync(directory);
19316
19395
  } catch {
19317
19396
  return true;
19318
19397
  }
@@ -19323,7 +19402,7 @@ var walk = (root, directory, output) => {
19323
19402
  const target = path10.join(directory, name);
19324
19403
  let stat;
19325
19404
  try {
19326
- stat = fs7.lstatSync(target);
19405
+ stat = fs8.lstatSync(target);
19327
19406
  } catch {
19328
19407
  uncertain = true;
19329
19408
  continue;
@@ -19342,10 +19421,10 @@ var scopeRoots = (root, scope) => {
19342
19421
  if (!inside(root, target))
19343
19422
  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` }] });
19344
19423
  let ancestor = target;
19345
- while (!fs7.existsSync(ancestor) && ancestor !== root)
19424
+ while (!fs8.existsSync(ancestor) && ancestor !== root)
19346
19425
  ancestor = path10.dirname(ancestor);
19347
19426
  try {
19348
- if (!inside(root, fs7.realpathSync(ancestor)))
19427
+ if (!inside(root, fs8.realpathSync(ancestor)))
19349
19428
  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` }] });
19350
19429
  } catch {
19351
19430
  return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
@@ -19357,7 +19436,7 @@ var scopeRoots = (root, scope) => {
19357
19436
  function captureCandidate(root, scope, environment = []) {
19358
19437
  let checkout;
19359
19438
  try {
19360
- checkout = fs7.realpathSync(root);
19439
+ checkout = fs8.realpathSync(root);
19361
19440
  } catch {
19362
19441
  return failure2("invalid_input", "candidate root does not exist");
19363
19442
  }
@@ -19375,11 +19454,11 @@ function captureCandidate(root, scope, environment = []) {
19375
19454
  if (item === ".git" || item === ".workit")
19376
19455
  continue;
19377
19456
  names.add(item);
19378
- if (!fs7.existsSync(target))
19457
+ if (!fs8.existsSync(target))
19379
19458
  continue;
19380
19459
  let stat;
19381
19460
  try {
19382
- stat = fs7.lstatSync(target);
19461
+ stat = fs8.lstatSync(target);
19383
19462
  } catch {
19384
19463
  uncertain = true;
19385
19464
  continue;
@@ -19397,15 +19476,15 @@ function captureCandidate(root, scope, environment = []) {
19397
19476
  continue;
19398
19477
  const target = path10.join(checkout, item);
19399
19478
  try {
19400
- const stat = fs7.lstatSync(target);
19479
+ const stat = fs8.lstatSync(target);
19401
19480
  if (stat.isSymbolicLink()) {
19402
- const link = fs7.readlinkSync(target);
19481
+ const link = fs8.readlinkSync(target);
19403
19482
  files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
19404
19483
  } else if (stat.isFile()) {
19405
19484
  files.push({
19406
19485
  path: item,
19407
19486
  kind: "file",
19408
- digest: digestBytes3(fs7.readFileSync(target)),
19487
+ digest: digestBytes3(fs8.readFileSync(target)),
19409
19488
  executable: (stat.mode & 73) !== 0
19410
19489
  });
19411
19490
  } else if (!stat.isDirectory()) {
@@ -19427,10 +19506,10 @@ function captureCandidate(root, scope, environment = []) {
19427
19506
  const namesByEnvironment = supplied.map(({ name }) => name);
19428
19507
  if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
19429
19508
  return failure2("invalid_input", "candidate environment names must be unique and non-empty");
19430
- const values = supplied.toSorted((left, right) => compareCodeUnits3(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
19509
+ const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
19431
19510
  if (values.some(({ value }) => value === null))
19432
19511
  completeness = "uncertain";
19433
- const headResult = spawnSync2("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
19512
+ const headResult = spawnSync3("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
19434
19513
  if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
19435
19514
  completeness = "uncertain";
19436
19515
  const head = headResult.status === 0 ? headResult.stdout.trim() : null;
@@ -19761,140 +19840,582 @@ var assuranceFor = (id, definition, capabilities) => {
19761
19840
  assurance: "unavailable",
19762
19841
  reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
19763
19842
  };
19764
- if (matches.some((capability) => capability.assurance === "enforced"))
19765
- return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
19766
- return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
19843
+ if (matches.some((capability) => capability.assurance === "enforced"))
19844
+ return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
19845
+ return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
19846
+ };
19847
+ function selectMethods(policy, capabilities) {
19848
+ return Object.entries(METHODS).flatMap(([id, definition]) => {
19849
+ const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
19850
+ if (requirements.length === 0)
19851
+ return [];
19852
+ const capability = assuranceFor(id, definition, capabilities);
19853
+ const reasons = [
19854
+ ...new Set(requirements.map((requirement) => requirement.reason)),
19855
+ ...capability.reasons
19856
+ ];
19857
+ if (capability.assurance === "unavailable")
19858
+ reasons.unshift("required capability is unavailable");
19859
+ return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
19860
+ });
19861
+ }
19862
+
19863
+ // packages/workit-core/src/core/task-context.ts
19864
+ function reconcileResume(view, observations = []) {
19865
+ if (observations.length > 0)
19866
+ return failure2("permission_denied", "worker observations require native host verification");
19867
+ const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
19868
+ if (!candidate.ok)
19869
+ return candidate;
19870
+ const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
19871
+ const blockers = [...view.task.progress.blockers];
19872
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
19873
+ blockers.push({
19874
+ reason: "worker state requires reconciliation",
19875
+ dependentAction: "resume",
19876
+ refs: []
19877
+ });
19878
+ return success(null, null, {
19879
+ candidate: candidate.data,
19880
+ staleEvidenceIds,
19881
+ workerUpdates: [],
19882
+ blockers,
19883
+ reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
19884
+ });
19885
+ }
19886
+ var COMPACT_MAX_BYTES = 4096;
19887
+ var COMPACT_MAX_ITEMS = 8;
19888
+ var COMPACT_TEXT_BYTES = 128;
19889
+ var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
19890
+ var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
19891
+ var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
19892
+ var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
19893
+ const separator = match.search(/[:=]/u);
19894
+ if (separator >= 0)
19895
+ return `${match.slice(0, separator)}=<redacted>`;
19896
+ const space = match.search(/\s/u);
19897
+ return `${match.slice(0, space)} <redacted>`;
19898
+ }).slice(0, limit);
19899
+ var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
19900
+ var compactTimestamp = (value) => `${value.slice(0, 19)}.${value.slice(20, -1).padEnd(9, "0")}`;
19901
+ var compactDecisionChoice = (entry) => {
19902
+ const stated = entry.data.binding.statedChoice?.text;
19903
+ const approved = entry.data.response === "approved" ? entry.data.binding.approvedContent : "";
19904
+ const source = stated?.trim() || approved.trim() || null;
19905
+ return compactText(source, COMPACT_TEXT_BYTES);
19906
+ };
19907
+ var compactReference = (ref) => {
19908
+ if (ref.kind === "file")
19909
+ return {
19910
+ kind: "file",
19911
+ path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
19912
+ };
19913
+ if (ref.kind === "record")
19914
+ return {
19915
+ kind: "record",
19916
+ collection: ref.collection,
19917
+ id: ref.id
19918
+ };
19919
+ return { kind: ref.kind };
19767
19920
  };
19768
- function selectMethods(policy, capabilities) {
19769
- return Object.entries(METHODS).flatMap(([id, definition]) => {
19770
- const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
19771
- if (requirements.length === 0)
19772
- return [];
19773
- const capability = assuranceFor(id, definition, capabilities);
19774
- const reasons = [
19775
- ...new Set(requirements.map((requirement) => requirement.reason)),
19776
- ...capability.reasons
19777
- ];
19778
- if (capability.assurance === "unavailable")
19779
- reasons.unshift("required capability is unavailable");
19780
- return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
19781
- });
19921
+ function compactTaskContext(view) {
19922
+ const decisions = view.task.decisions.slice().toSorted((left, right) => {
19923
+ const leftAt = compactTimestamp(left.recordedAt);
19924
+ const rightAt = compactTimestamp(right.recordedAt);
19925
+ if (leftAt !== rightAt)
19926
+ return leftAt > rightAt ? -1 : 1;
19927
+ return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
19928
+ }).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
19929
+ id: entry.id,
19930
+ purpose: entry.data.purpose,
19931
+ status: entry.data.response,
19932
+ digest: entry.data.digest,
19933
+ choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
19934
+ references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
19935
+ }));
19936
+ const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
19937
+ id: method.id,
19938
+ assurance: method.assurance,
19939
+ reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
19940
+ })) : [];
19941
+ const gaps = Array.from(new Set([
19942
+ ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
19943
+ ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
19944
+ ...view.task.progress.blockers.map((entry) => entry.reason)
19945
+ ])).toSorted().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
19946
+ const context = {
19947
+ objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
19948
+ status: view.task.status,
19949
+ decisions,
19950
+ methods,
19951
+ gaps,
19952
+ nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
19953
+ };
19954
+ let encoded = JSON.stringify(context);
19955
+ while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
19956
+ if (context.gaps.length)
19957
+ context.gaps.pop();
19958
+ else if (context.decisions.length)
19959
+ context.decisions.pop();
19960
+ else
19961
+ context.methods.pop();
19962
+ encoded = JSON.stringify(context);
19963
+ }
19964
+ if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
19965
+ context.objective = compactText(context.objective, 64) ?? "";
19966
+ context.nextAction = compactText(context.nextAction, 64);
19967
+ encoded = JSON.stringify(context);
19968
+ }
19969
+ return encoded;
19782
19970
  }
19971
+ var exportDigest = (bundle) => sha256({
19972
+ schemaVersion: bundle.schemaVersion,
19973
+ exportedAt: bundle.exportedAt,
19974
+ sourceWorkspaceId: bundle.sourceWorkspaceId,
19975
+ task: bundle.task
19976
+ });
19977
+ // packages/workit-core/src/core/external-action.ts
19978
+ var chainStepSchema = union([
19979
+ string2().min(1),
19980
+ object({ branch: string2().min(1) }).strict(),
19981
+ object({ pr: literal(true) }).strict()
19982
+ ]);
19983
+ var externalActionSchema = discriminatedUnion("operation", [
19984
+ object({
19985
+ operation: literal("git.branch_setup"),
19986
+ payload: object({
19987
+ action: _enum(["setup", "reapply_stash"]).optional(),
19988
+ sdd_dir: string2().optional(),
19989
+ target_branch: string2().optional(),
19990
+ stash: _enum(["yes", "no"]).optional(),
19991
+ cwd: string2().min(1).optional()
19992
+ }).strict()
19993
+ }).strict(),
19994
+ object({
19995
+ operation: literal("git.commit"),
19996
+ payload: object({
19997
+ message: string2().min(1).optional(),
19998
+ plan_steps: array(chainStepSchema).min(1).max(32).optional(),
19999
+ plan_branch: string2().min(1).optional(),
20000
+ cwd: string2().min(1).optional()
20001
+ }).strict()
20002
+ }).strict(),
20003
+ object({
20004
+ operation: literal("git.push"),
20005
+ payload: object({ branch: string2().min(1).optional(), cwd: string2().min(1).optional() }).strict()
20006
+ }).strict(),
20007
+ object({
20008
+ operation: literal("hosting.pull_request"),
20009
+ payload: object({
20010
+ title: string2().min(1),
20011
+ body: string2().optional(),
20012
+ draft: boolean2().optional(),
20013
+ target_branch: string2().optional(),
20014
+ babysit: boolean2().optional(),
20015
+ cwd: string2().min(1).optional()
20016
+ }).strict()
20017
+ }).strict(),
20018
+ object({
20019
+ operation: literal("hosting.merge"),
20020
+ payload: object({
20021
+ target_branch: string2().min(1).optional(),
20022
+ source_branch: string2().min(1).optional(),
20023
+ cwd: string2().min(1).optional()
20024
+ }).strict()
20025
+ }).strict(),
20026
+ object({
20027
+ operation: literal("hosting.delete_branch"),
20028
+ payload: object({ branch: string2().min(1), cwd: string2().min(1).optional() }).strict()
20029
+ }).strict(),
20030
+ object({
20031
+ operation: literal("youtrack.update"),
20032
+ payload: object({
20033
+ issueId: string2().min(1),
20034
+ markdown: string2().min(1),
20035
+ minutes: number2().positive().optional()
20036
+ }).strict()
20037
+ }).strict(),
20038
+ object({
20039
+ operation: literal("youtrack.time"),
20040
+ payload: object({
20041
+ issueId: string2().min(1),
20042
+ minutes: number2().positive(),
20043
+ text: string2().optional(),
20044
+ dateMs: number2().finite().optional()
20045
+ }).strict()
20046
+ }).strict(),
20047
+ object({
20048
+ operation: literal("youtrack.meeting"),
20049
+ payload: object({
20050
+ issueId: string2().min(1),
20051
+ minutes: number2().positive(),
20052
+ text: string2().min(1)
20053
+ }).strict()
20054
+ }).strict(),
20055
+ object({
20056
+ operation: literal("changelog.apply"),
20057
+ payload: object({
20058
+ entries: union([
20059
+ record(string2(), array(string2())),
20060
+ array(object({ category: string2().min(1), text: string2().min(1) }).strict())
20061
+ ]).optional(),
20062
+ path: string2().optional(),
20063
+ normalize_only: boolean2().optional()
20064
+ }).strict()
20065
+ }).strict(),
20066
+ object({
20067
+ operation: literal("context.read"),
20068
+ payload: object({
20069
+ kind: _enum([
20070
+ "git",
20071
+ "pr",
20072
+ "youtrack",
20073
+ "github_issue",
20074
+ "gitlab_issue",
20075
+ "changelog",
20076
+ "release",
20077
+ "affected"
20078
+ ]),
20079
+ range: string2().optional(),
20080
+ issueId: string2().optional(),
20081
+ issueUrl: string2().optional(),
20082
+ issueRef: string2().optional(),
20083
+ mode: string2().optional(),
20084
+ specPath: string2().optional(),
20085
+ planPath: string2().optional(),
20086
+ cwd: string2().min(1).optional()
20087
+ }).strict()
20088
+ }).strict()
20089
+ ]);
20090
+ // packages/workit-core/src/core/task-engine.ts
20091
+ import { realpathSync as realpathSync6 } from "node:fs";
19783
20092
 
19784
- // packages/workit-core/src/core/task-context.ts
19785
- function reconcileResume(view, observations = []) {
19786
- if (observations.length > 0)
19787
- return failure2("permission_denied", "worker observations require native host verification");
19788
- const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
19789
- if (!candidate.ok)
19790
- return candidate;
19791
- const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
19792
- const blockers = [...view.task.progress.blockers];
19793
- if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
19794
- blockers.push({
19795
- reason: "worker state requires reconciliation",
19796
- dependentAction: "resume",
19797
- refs: []
20093
+ // packages/workit-core/src/core/policy-resolver.ts
20094
+ var resolverInputSchema = object({
20095
+ intent: intentSchema,
20096
+ assessment: assessmentSchema,
20097
+ constraints: array(constraintSchema),
20098
+ prior: object({
20099
+ decisions: array(decisionSchema),
20100
+ findings: array(findingSchema),
20101
+ requirements: array(requirementSchema)
20102
+ }).strict(),
20103
+ preferences: object({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
20104
+ }).strict();
20105
+ var compareCodeUnits3 = (left, right) => {
20106
+ const length = Math.min(left.length, right.length);
20107
+ for (let index = 0;index < length; index += 1) {
20108
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
20109
+ if (difference !== 0)
20110
+ return difference;
20111
+ }
20112
+ return left.length - right.length;
20113
+ };
20114
+ var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits3);
20115
+ var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits3(canonicalJson(left), canonicalJson(right)));
20116
+ var normalizedScope = (scope) => ({
20117
+ description: scope.description,
20118
+ paths: stableUnique(scope.paths),
20119
+ exclusions: stableUnique(scope.exclusions)
20120
+ });
20121
+ var normalizedRefList = (values) => stableList(values);
20122
+ var normalizeAssessment = (assessment) => ({
20123
+ facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
20124
+ signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
20125
+ name,
20126
+ { ...signal, refs: normalizedRefList(signal.refs) }
20127
+ ])),
20128
+ consequences: stableList(assessment.consequences.map((consequence) => ({
20129
+ ...consequence,
20130
+ fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
20131
+ }))),
20132
+ verification: stableList(assessment.verification.map((verification) => ({
20133
+ ...verification,
20134
+ scope: normalizedScope(verification.scope),
20135
+ availableChecks: normalizedRefList(verification.availableChecks),
20136
+ gaps: stableUnique(verification.gaps)
20137
+ })))
20138
+ });
20139
+ var normalizeConstraint = (constraint) => ({
20140
+ ...constraint,
20141
+ source: constraint.source,
20142
+ requires: stableList(constraint.requires.map((requirement) => ({
20143
+ ...requirement,
20144
+ scope: normalizedScope(requirement.scope),
20145
+ refs: normalizedRefList(requirement.refs)
20146
+ })))
20147
+ });
20148
+ var normalizeDecision = (decision) => ({
20149
+ ...decision,
20150
+ binding: {
20151
+ ...decision.binding,
20152
+ scope: normalizedScope(decision.binding.scope),
20153
+ contentRefs: normalizedRefList(decision.binding.contentRefs)
20154
+ },
20155
+ requirementIds: stableUnique(decision.requirementIds)
20156
+ });
20157
+ var normalizeIntent = (intent) => ({
20158
+ ...intent,
20159
+ scope: normalizedScope(intent.scope),
20160
+ authorityRefs: normalizedRefList(intent.authorityRefs)
20161
+ });
20162
+ var normalizePrior = (prior) => ({
20163
+ decisions: stableList(prior.decisions.map(normalizeDecision)),
20164
+ findings: stableList(prior.findings.map((finding) => ({
20165
+ ...finding,
20166
+ scope: normalizedScope(finding.scope),
20167
+ refs: normalizedRefList(finding.refs),
20168
+ resolution: finding.resolution ? {
20169
+ ...finding.resolution,
20170
+ evidenceIds: stableUnique(finding.resolution.evidenceIds),
20171
+ decisionIds: stableUnique(finding.resolution.decisionIds)
20172
+ } : null
20173
+ }))),
20174
+ requirements: stableList(prior.requirements.map((requirement) => ({
20175
+ ...requirement,
20176
+ scope: normalizedScope(requirement.scope)
20177
+ })))
20178
+ });
20179
+ var invalidFields = (error) => ({
20180
+ fields: error.issues.map((issue) => ({
20181
+ path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
20182
+ reason: issue.message
20183
+ }))
20184
+ });
20185
+ function normalizeResolverInput(input) {
20186
+ const parsed = resolverInputSchema.safeParse(input);
20187
+ if (!parsed.success)
20188
+ return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
20189
+ const value = parsed.data;
20190
+ const signals = value.assessment.signals;
20191
+ if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
20192
+ return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
20193
+ fields: [
20194
+ {
20195
+ path: "assessment.signals",
20196
+ reason: "contradictory behaviorChange and mechanicalLowRisk"
20197
+ }
20198
+ ]
19798
20199
  });
19799
- return success(null, null, {
19800
- candidate: candidate.data,
19801
- staleEvidenceIds,
19802
- workerUpdates: [],
19803
- blockers,
19804
- reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
19805
- });
20200
+ const ids = value.constraints.map((constraint) => constraint.id);
20201
+ if (new Set(ids).size !== ids.length)
20202
+ return failure2("invalid_input", "constraint IDs must be unique", {
20203
+ fields: [{ path: "constraints", reason: "duplicate constraint id" }]
20204
+ });
20205
+ if (value.preferences?.fast && value.preferences.thorough)
20206
+ return failure2("invalid_input", "fast and thorough preferences contradict", {
20207
+ fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
20208
+ });
20209
+ const normalized = {
20210
+ intent: normalizeIntent(value.intent),
20211
+ assessment: normalizeAssessment(value.assessment),
20212
+ constraints: stableList(value.constraints.map(normalizeConstraint)),
20213
+ prior: normalizePrior(value.prior),
20214
+ ...value.preferences ? { preferences: { ...value.preferences } } : {},
20215
+ policyVersion: POLICY_VERSION
20216
+ };
20217
+ return success(null, null, normalized);
19806
20218
  }
19807
- var COMPACT_MAX_BYTES = 4096;
19808
- var COMPACT_MAX_ITEMS = 8;
19809
- var COMPACT_TEXT_BYTES = 128;
19810
- var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
19811
- var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
19812
- var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
19813
- var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
19814
- const separator = match.search(/[:=]/u);
19815
- if (separator >= 0)
19816
- return `${match.slice(0, separator)}=<redacted>`;
19817
- const space = match.search(/\s/u);
19818
- return `${match.slice(0, space)} <redacted>`;
19819
- }).slice(0, limit);
19820
- var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
19821
- var compactTimestamp = (value) => `${value.slice(0, 19)}.${value.slice(20, -1).padEnd(9, "0")}`;
19822
- var compactDecisionChoice = (entry) => {
19823
- const stated = entry.data.binding.statedChoice?.text;
19824
- const approved = entry.data.response === "approved" ? entry.data.binding.approvedContent : "";
19825
- const source = stated?.trim() || approved.trim() || null;
19826
- return compactText(source, COMPACT_TEXT_BYTES);
19827
- };
19828
- var compactReference = (ref) => {
19829
- if (ref.kind === "file")
19830
- return {
19831
- kind: "file",
19832
- path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
19833
- };
19834
- if (ref.kind === "record")
19835
- return {
19836
- kind: "record",
19837
- collection: ref.collection,
19838
- id: ref.id
19839
- };
19840
- return { kind: ref.kind };
19841
- };
19842
- function compactTaskContext(view) {
19843
- const decisions = view.task.decisions.slice().toSorted((left, right) => {
19844
- const leftAt = compactTimestamp(left.recordedAt);
19845
- const rightAt = compactTimestamp(right.recordedAt);
19846
- if (leftAt !== rightAt)
19847
- return leftAt > rightAt ? -1 : 1;
19848
- return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
19849
- }).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
19850
- id: entry.id,
19851
- purpose: entry.data.purpose,
19852
- status: entry.data.response,
19853
- digest: entry.data.digest,
19854
- choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
19855
- references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
19856
- }));
19857
- const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
19858
- id: method.id,
19859
- assurance: method.assurance,
19860
- reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
19861
- })) : [];
19862
- const gaps = Array.from(new Set([
19863
- ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
19864
- ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
19865
- ...view.task.progress.blockers.map((entry) => entry.reason)
19866
- ])).toSorted().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
19867
- const context = {
19868
- objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
19869
- status: view.task.status,
19870
- decisions,
19871
- methods,
19872
- gaps,
19873
- nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
20219
+ var requirement = (spec) => ({
20220
+ ...spec,
20221
+ id: requirementId({
20222
+ ruleId: spec.ruleId,
20223
+ scope: spec.scope,
20224
+ satisfaction: spec.satisfaction,
20225
+ before: spec.before,
20226
+ dependentAction: spec.dependentAction
20227
+ })
20228
+ });
20229
+ var signalReason = (input, name) => input.assessment.signals[name].reason;
20230
+ function resolveRequirements(input) {
20231
+ const { signals } = input.assessment;
20232
+ const scope = input.intent.scope;
20233
+ const requirements = [];
20234
+ const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
20235
+ const hasUnknownFact = [
20236
+ ...input.assessment.facts,
20237
+ ...input.assessment.consequences.map(({ fact }) => fact)
20238
+ ].some((fact) => fact.basis === "unknown");
20239
+ if (hasUnknownSignal || hasUnknownFact)
20240
+ requirements.push(requirement({
20241
+ ruleId: "consequential-unknown",
20242
+ dimension: "investigation",
20243
+ scope,
20244
+ reason: `Resolve consequential uncertainty before the dependent action: ${[
20245
+ ...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
20246
+ ...hasUnknownFact ? ["unknown fact"] : []
20247
+ ].join(", ")}`,
20248
+ satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
20249
+ before: "dependent_action",
20250
+ dependentAction: null,
20251
+ acceptanceAllowed: false
20252
+ }));
20253
+ if (signals.productChoiceOpen.value === true)
20254
+ requirements.push(requirement({
20255
+ ruleId: "product-decision",
20256
+ dimension: "decisions",
20257
+ scope,
20258
+ reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
20259
+ satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
20260
+ before: "dependent_action",
20261
+ dependentAction: null,
20262
+ acceptanceAllowed: false
20263
+ }));
20264
+ const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
20265
+ if (signals.behaviorChange.value === true)
20266
+ requirements.push(requirement({
20267
+ ruleId: "behavioral-verification",
20268
+ dimension: "testing",
20269
+ scope,
20270
+ reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
20271
+ satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
20272
+ before: "close",
20273
+ dependentAction: null,
20274
+ acceptanceAllowed: false
20275
+ }), requirement({
20276
+ ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
20277
+ dimension: "review",
20278
+ scope,
20279
+ 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.",
20280
+ 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.",
20281
+ before: "close",
20282
+ dependentAction: null,
20283
+ acceptanceAllowed: !independentBehaviorReview
20284
+ }));
20285
+ else if (signals.mechanicalLowRisk.value === true)
20286
+ requirements.push(requirement({
20287
+ ruleId: "mechanical-existing-checks",
20288
+ dimension: "verification",
20289
+ scope,
20290
+ reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
20291
+ satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
20292
+ before: "close",
20293
+ dependentAction: null,
20294
+ acceptanceAllowed: true
20295
+ }), requirement({
20296
+ ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
20297
+ dimension: "review",
20298
+ scope,
20299
+ 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.",
20300
+ 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.",
20301
+ before: "close",
20302
+ dependentAction: null,
20303
+ acceptanceAllowed: !input.preferences?.thorough
20304
+ }));
20305
+ if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
20306
+ requirements.push(requirement({
20307
+ ruleId: "pre-pr-cleanup",
20308
+ dimension: "verification",
20309
+ scope,
20310
+ reason: "Implementation output must be deslopped before it is delivered.",
20311
+ satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
20312
+ before: "dependent_action",
20313
+ dependentAction: "hosting.pull_request",
20314
+ acceptanceAllowed: true
20315
+ }));
20316
+ if (signals.durableAgreementNeeded.value === true)
20317
+ requirements.push(requirement({
20318
+ ruleId: "durable-spec",
20319
+ dimension: "artifacts",
20320
+ scope,
20321
+ reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
20322
+ satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
20323
+ before: "write",
20324
+ dependentAction: null,
20325
+ acceptanceAllowed: true
20326
+ }));
20327
+ if (signals.coordinationPlanNeeded.value === true)
20328
+ requirements.push(requirement({
20329
+ ruleId: "coordination-plan",
20330
+ dimension: "continuity",
20331
+ scope,
20332
+ reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
20333
+ satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
20334
+ before: "dependent_action",
20335
+ dependentAction: null,
20336
+ acceptanceAllowed: true
20337
+ }));
20338
+ if (signals.helperUseful.value === true && !input.preferences?.fast)
20339
+ requirements.push(requirement({
20340
+ ruleId: "helper-usefulness",
20341
+ dimension: "delegation",
20342
+ scope,
20343
+ reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
20344
+ satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
20345
+ before: "dependent_action",
20346
+ dependentAction: null,
20347
+ acceptanceAllowed: true
20348
+ }));
20349
+ for (const constraint of input.constraints)
20350
+ for (const obligation of constraint.requires) {
20351
+ const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
20352
+ requirements.push(requirement({
20353
+ ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
20354
+ kind: obligation.kind,
20355
+ scope: normalizedScope(obligation.scope),
20356
+ refs: normalizedRefList(obligation.refs),
20357
+ method: obligation.method,
20358
+ before: obligation.before,
20359
+ dependentAction: obligation.dependentAction
20360
+ })}`,
20361
+ dimension,
20362
+ scope: obligation.scope,
20363
+ reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
20364
+ satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
20365
+ before: obligation.before,
20366
+ dependentAction: obligation.dependentAction,
20367
+ acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
20368
+ }));
20369
+ }
20370
+ const seen = new Set;
20371
+ return requirements.filter((item) => {
20372
+ if (seen.has(item.id))
20373
+ return false;
20374
+ seen.add(item.id);
20375
+ return true;
20376
+ });
20377
+ }
20378
+ function resolvePolicy(input) {
20379
+ const normalized = normalizeResolverInput(input);
20380
+ if (!normalized.ok)
20381
+ return normalized;
20382
+ const requirements = resolveRequirements(normalized.data);
20383
+ const policy = {
20384
+ policyVersion: POLICY_VERSION,
20385
+ inputDigest: sha256(canonicalJson(normalized.data)),
20386
+ requirements
19874
20387
  };
19875
- let encoded = JSON.stringify(context);
19876
- while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
19877
- if (context.gaps.length)
19878
- context.gaps.pop();
19879
- else if (context.decisions.length)
19880
- context.decisions.pop();
19881
- else
19882
- context.methods.pop();
19883
- encoded = JSON.stringify(context);
19884
- }
19885
- if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
19886
- context.objective = compactText(context.objective, 64) ?? "";
19887
- context.nextAction = compactText(context.nextAction, 64);
19888
- encoded = JSON.stringify(context);
19889
- }
19890
- return encoded;
20388
+ const parsed = policySchema.safeParse(policy);
20389
+ if (!parsed.success)
20390
+ return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
20391
+ return success(null, null, parsed.data);
20392
+ }
20393
+ function diffPolicy(previous, next, reason, now) {
20394
+ if (!policySchema.safeParse(previous).success && previous !== null)
20395
+ throw new TypeError("previous policy is invalid");
20396
+ if (!policySchema.safeParse(next).success)
20397
+ throw new TypeError("next policy is invalid");
20398
+ const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
20399
+ const newIds = new Set(next.requirements.map((item) => item.id));
20400
+ const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits3);
20401
+ const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits3);
20402
+ if (added.length === 0 && retired.length === 0)
20403
+ return null;
20404
+ const normalizedReason = reason.trim();
20405
+ const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
20406
+ const change = {
20407
+ recordedAt: now,
20408
+ fromInputDigest: previous?.inputDigest ?? null,
20409
+ toInputDigest: next.inputDigest,
20410
+ added,
20411
+ retired,
20412
+ reason: changeReason
20413
+ };
20414
+ const parsed = policyChangeSchema.safeParse(change);
20415
+ if (!parsed.success)
20416
+ throw new TypeError("policy change is invalid");
20417
+ return parsed.data;
19891
20418
  }
19892
- var exportDigest = (bundle) => sha256({
19893
- schemaVersion: bundle.schemaVersion,
19894
- exportedAt: bundle.exportedAt,
19895
- sourceWorkspaceId: bundle.sourceWorkspaceId,
19896
- task: bundle.task
19897
- });
19898
20419
 
19899
20420
  // packages/workit-core/src/core/task-engine.ts
19900
20421
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
@@ -21876,122 +22397,6 @@ class WorkitCore {
21876
22397
  return this.summary(changed.data.task);
21877
22398
  }
21878
22399
  }
21879
-
21880
- // packages/workit-core/src/core/session-context.ts
21881
- var cache = new Map;
21882
- // packages/workit-core/src/core/external-action.ts
21883
- var chainStepSchema = union([
21884
- string2().min(1),
21885
- object({ branch: string2().min(1) }).strict(),
21886
- object({ pr: literal(true) }).strict()
21887
- ]);
21888
- var externalActionSchema = discriminatedUnion("operation", [
21889
- object({
21890
- operation: literal("git.branch_setup"),
21891
- payload: object({
21892
- action: _enum(["setup", "reapply_stash"]).optional(),
21893
- sdd_dir: string2().optional(),
21894
- target_branch: string2().optional(),
21895
- stash: _enum(["yes", "no"]).optional(),
21896
- cwd: string2().min(1).optional()
21897
- }).strict()
21898
- }).strict(),
21899
- object({
21900
- operation: literal("git.commit"),
21901
- payload: object({
21902
- message: string2().min(1).optional(),
21903
- plan_steps: array(chainStepSchema).min(1).max(32).optional(),
21904
- plan_branch: string2().min(1).optional(),
21905
- cwd: string2().min(1).optional()
21906
- }).strict()
21907
- }).strict(),
21908
- object({
21909
- operation: literal("git.push"),
21910
- payload: object({ branch: string2().min(1).optional(), cwd: string2().min(1).optional() }).strict()
21911
- }).strict(),
21912
- object({
21913
- operation: literal("hosting.pull_request"),
21914
- payload: object({
21915
- title: string2().min(1),
21916
- body: string2().optional(),
21917
- draft: boolean2().optional(),
21918
- target_branch: string2().optional(),
21919
- babysit: boolean2().optional(),
21920
- cwd: string2().min(1).optional()
21921
- }).strict()
21922
- }).strict(),
21923
- object({
21924
- operation: literal("hosting.merge"),
21925
- payload: object({
21926
- target_branch: string2().min(1).optional(),
21927
- source_branch: string2().min(1).optional(),
21928
- cwd: string2().min(1).optional()
21929
- }).strict()
21930
- }).strict(),
21931
- object({
21932
- operation: literal("hosting.delete_branch"),
21933
- payload: object({ branch: string2().min(1), cwd: string2().min(1).optional() }).strict()
21934
- }).strict(),
21935
- object({
21936
- operation: literal("youtrack.update"),
21937
- payload: object({
21938
- issueId: string2().min(1),
21939
- markdown: string2().min(1),
21940
- minutes: number2().positive().optional()
21941
- }).strict()
21942
- }).strict(),
21943
- object({
21944
- operation: literal("youtrack.time"),
21945
- payload: object({
21946
- issueId: string2().min(1),
21947
- minutes: number2().positive(),
21948
- text: string2().optional(),
21949
- dateMs: number2().finite().optional()
21950
- }).strict()
21951
- }).strict(),
21952
- object({
21953
- operation: literal("youtrack.meeting"),
21954
- payload: object({
21955
- issueId: string2().min(1),
21956
- minutes: number2().positive(),
21957
- text: string2().min(1)
21958
- }).strict()
21959
- }).strict(),
21960
- object({
21961
- operation: literal("changelog.apply"),
21962
- payload: object({
21963
- entries: union([
21964
- record(string2(), array(string2())),
21965
- array(object({ category: string2().min(1), text: string2().min(1) }).strict())
21966
- ]).optional(),
21967
- path: string2().optional(),
21968
- normalize_only: boolean2().optional()
21969
- }).strict()
21970
- }).strict(),
21971
- object({
21972
- operation: literal("context.read"),
21973
- payload: object({
21974
- kind: _enum([
21975
- "git",
21976
- "pr",
21977
- "youtrack",
21978
- "github_issue",
21979
- "gitlab_issue",
21980
- "changelog",
21981
- "release",
21982
- "affected"
21983
- ]),
21984
- range: string2().optional(),
21985
- issueId: string2().optional(),
21986
- issueUrl: string2().optional(),
21987
- issueRef: string2().optional(),
21988
- mode: string2().optional(),
21989
- specPath: string2().optional(),
21990
- planPath: string2().optional(),
21991
- cwd: string2().min(1).optional()
21992
- }).strict()
21993
- }).strict()
21994
- ]);
21995
22400
  // packages/workit-core/src/core.ts
21996
22401
  var revision2 = /^[A-Za-z0-9@][A-Za-z0-9@._/~^{}-]*$/;
21997
22402
  function gitRevisionParts(value) {
@@ -22035,7 +22440,7 @@ function resolveInside(root, candidate) {
22035
22440
  }
22036
22441
  function run2(root, executable, args, env = {}) {
22037
22442
  const cwd = realpathSync7(root);
22038
- const result = spawnSync3(executable, args, {
22443
+ const result = spawnSync4(executable, args, {
22039
22444
  cwd,
22040
22445
  encoding: "utf8",
22041
22446
  env: { ...process.env, ...env }
@@ -24495,9 +24900,9 @@ var applyPatterns = (value) => {
24495
24900
  var redactSecrets = (value) => applyPatterns(value);
24496
24901
 
24497
24902
  // packages/workit-core/src/core/pr-create.ts
24498
- import fs8 from "node:fs";
24903
+ import fs9 from "node:fs";
24499
24904
  import path12 from "node:path";
24500
- import { spawnSync as spawnSync4 } from "node:child_process";
24905
+ import { spawnSync as spawnSync5 } from "node:child_process";
24501
24906
  function parseGhIssue(value) {
24502
24907
  const m = /issues\/(\d+)/.exec(value);
24503
24908
  return m ? m[1] : String(value).trim().replace(/^#/, "");
@@ -24510,7 +24915,7 @@ function whichOnPath(tool) {
24510
24915
  for (const name of names) {
24511
24916
  const candidate = path12.join(dir, name);
24512
24917
  try {
24513
- fs8.accessSync(candidate, fs8.constants.X_OK);
24918
+ fs9.accessSync(candidate, fs9.constants.X_OK);
24514
24919
  return candidate;
24515
24920
  } catch {}
24516
24921
  }
@@ -24519,7 +24924,7 @@ function whichOnPath(tool) {
24519
24924
  }
24520
24925
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
24521
24926
  var pushRemote = (cwd) => {
24522
- const result = spawnSync4("git", ["remote", "get-url", "--push", "--all", "origin"], {
24927
+ const result = spawnSync5("git", ["remote", "get-url", "--push", "--all", "origin"], {
24523
24928
  cwd,
24524
24929
  encoding: "utf8"
24525
24930
  });
@@ -24530,7 +24935,7 @@ var pushRemote = (cwd) => {
24530
24935
  };
24531
24936
 
24532
24937
  // packages/workit-core/src/core/repo-context.ts
24533
- import { spawnSync as spawnSync5 } from "node:child_process";
24938
+ import { spawnSync as spawnSync6 } from "node:child_process";
24534
24939
  import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
24535
24940
  import path13 from "node:path";
24536
24941
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
@@ -24559,7 +24964,7 @@ var invalidRange = (cwd) => ({
24559
24964
  cwd
24560
24965
  });
24561
24966
  var runGit = (cwd, args) => {
24562
- const result = spawnSync5("git", args, { cwd, encoding: "utf8" });
24967
+ const result = spawnSync6("git", args, { cwd, encoding: "utf8" });
24563
24968
  return {
24564
24969
  stdout: (result.stdout ?? "").trimEnd(),
24565
24970
  stderr: (result.stderr ?? "").trimEnd(),
@@ -24732,7 +25137,7 @@ var documentationFiles = (cwd) => {
24732
25137
  walk(cwd, 1);
24733
25138
  return matches.toSorted().slice(0, 200);
24734
25139
  };
24735
- var readTrimmed = (file, maxLines) => {
25140
+ var readTrimmed2 = (file, maxLines) => {
24736
25141
  if (!existsSync6(file))
24737
25142
  return "";
24738
25143
  const lines = readFileSync7(file, "utf8").split(`
@@ -24799,7 +25204,7 @@ ${status.stderr}` : "");
24799
25204
  stdout += `template_path: ${template}
24800
25205
 
24801
25206
  `;
24802
- stdout += readTrimmed(path13.join(cwd, template), 220);
25207
+ stdout += readTrimmed2(path13.join(cwd, template), 220);
24803
25208
  } else {
24804
25209
  stdout += `template_path: none
24805
25210
 
@@ -24871,7 +25276,7 @@ function changelogContext(root, range) {
24871
25276
  stdout += printSection("Existing CHANGELOG.md");
24872
25277
  const changelogPath = path13.join(cwd, "CHANGELOG.md");
24873
25278
  if (existsSync6(changelogPath)) {
24874
- stdout += readTrimmed(changelogPath, 260) + `
25279
+ stdout += readTrimmed2(changelogPath, 260) + `
24875
25280
  `;
24876
25281
  } else {
24877
25282
  stdout += `CHANGELOG.md not found.
@@ -24913,7 +25318,7 @@ function docsRefreshContext(root, range) {
24913
25318
  stdout += printSection("README Preview");
24914
25319
  const readme = path13.join(cwd, "README.md");
24915
25320
  if (existsSync6(readme)) {
24916
- stdout += readTrimmed(readme, 220) + `
25321
+ stdout += readTrimmed2(readme, 220) + `
24917
25322
  `;
24918
25323
  } else {
24919
25324
  stdout += `README.md not found.
@@ -25000,7 +25405,7 @@ function releaseNotesContext(root, rangeOrTag) {
25000
25405
  }
25001
25406
 
25002
25407
  // packages/workit-core/src/core/tracker-issues.ts
25003
- import { spawnSync as spawnSync6 } from "node:child_process";
25408
+ import { spawnSync as spawnSync7 } from "node:child_process";
25004
25409
  var repoPathFromRemote = (remote) => {
25005
25410
  let rest = (remote || "").trim().replace(/\/+$/, "");
25006
25411
  if (!rest)
@@ -25021,7 +25426,7 @@ var defaultCli = (bin, available) => {
25021
25426
  if (!available)
25022
25427
  return null;
25023
25428
  return (args, root) => {
25024
- const out = spawnSync6(bin, args, {
25429
+ const out = spawnSync7(bin, args, {
25025
25430
  cwd: root,
25026
25431
  encoding: "utf8",
25027
25432
  env: { ...process.env, PATH: process.env.PATH ?? "" }
@@ -25099,7 +25504,7 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
25099
25504
  };
25100
25505
 
25101
25506
  // packages/workit-core/src/core/youtrack.ts
25102
- import fs9 from "node:fs";
25507
+ import fs10 from "node:fs";
25103
25508
  import path14 from "node:path";
25104
25509
 
25105
25510
  // packages/workit-core/src/core/templates.ts
@@ -25118,17 +25523,17 @@ var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? p
25118
25523
  var youTrackTokenModeOk = (p) => {
25119
25524
  if (process.platform === "win32")
25120
25525
  return true;
25121
- const mode = fs9.statSync(p).mode & 511;
25526
+ const mode = fs10.statSync(p).mode & 511;
25122
25527
  return mode === 384;
25123
25528
  };
25124
25529
  function readYouTrackConfig(required) {
25125
25530
  const cfgPath = youTrackConfigPath();
25126
- if (!fs9.existsSync(cfgPath)) {
25531
+ if (!fs10.existsSync(cfgPath)) {
25127
25532
  return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
25128
25533
  }
25129
25534
  let parsed;
25130
25535
  try {
25131
- parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
25536
+ parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
25132
25537
  } catch {
25133
25538
  return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
25134
25539
  }
@@ -25139,11 +25544,11 @@ function readYouTrackConfig(required) {
25139
25544
  }
25140
25545
  var readYouTrackContextConfig = () => {
25141
25546
  const cfgPath = youTrackReadOnlyConfigPath();
25142
- if (!fs9.existsSync(cfgPath))
25547
+ if (!fs10.existsSync(cfgPath))
25143
25548
  return { data: {} };
25144
25549
  let parsed;
25145
25550
  try {
25146
- parsed = JSON.parse(fs9.readFileSync(cfgPath, "utf8"));
25551
+ parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
25147
25552
  } catch {
25148
25553
  return { error: "YouTrack configuration is unavailable" };
25149
25554
  }
@@ -25183,11 +25588,11 @@ var youTrackToken = () => {
25183
25588
  return loaded;
25184
25589
  const tokenFile = String(loaded.config.tokenFile ?? "");
25185
25590
  const tokenPath = tokenFile ? path14.isAbsolute(tokenFile) ? path14.resolve(tokenFile) : path14.resolve(process.cwd(), tokenFile) : "";
25186
- if (!tokenPath || !fs9.existsSync(tokenPath))
25591
+ if (!tokenPath || !fs10.existsSync(tokenPath))
25187
25592
  return { error: "missing youtrack.token" };
25188
25593
  if (!youTrackTokenModeOk(tokenPath))
25189
25594
  return { error: "youtrack.token mode must be 0600" };
25190
- const token = fs9.readFileSync(tokenPath, "utf8").trim();
25595
+ const token = fs10.readFileSync(tokenPath, "utf8").trim();
25191
25596
  if (!token)
25192
25597
  return { error: "empty token file" };
25193
25598
  if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
@@ -25252,9 +25657,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
25252
25657
  } catch {
25253
25658
  continue;
25254
25659
  }
25255
- if (!fs9.existsSync(full))
25660
+ if (!fs10.existsSync(full))
25256
25661
  continue;
25257
- const text = fs9.readFileSync(full, "utf8");
25662
+ const text = fs10.readFileSync(full, "utf8");
25258
25663
  const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
25259
25664
  if (m)
25260
25665
  return m[1];
@@ -25797,123 +26202,166 @@ async function runStdioServer(host, contextProvider = defaultContextProvider(hos
25797
26202
  await server.connect(new StdioServerTransport);
25798
26203
  return server;
25799
26204
  }
26205
+ // packages/workit-core/src/hooks/descriptor.ts
26206
+ var support = (descriptor, axis) => {
26207
+ if (axis.startsWith("event:"))
26208
+ return descriptor.events[axis.slice("event:".length)].support;
26209
+ if (axis === "shellPolicy.deny")
26210
+ return descriptor.shellPolicy.deny;
26211
+ if (axis === "stopControl" || axis === "shellAvailable")
26212
+ return descriptor[axis];
26213
+ const [group, key] = axis.split(".");
26214
+ return descriptor[group][key];
26215
+ };
26216
+ var usable = (value) => value === "native" || value === "partial";
26217
+ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map((rule) => {
26218
+ const levels = rule.requires.map((axis) => support(descriptor, axis));
26219
+ const available = rule.assurance !== "unavailable" && levels.every(usable) && (rule.observed ?? []).every((flag) => observed[flag] === true);
26220
+ const assurance = !available ? "unavailable" : rule.assurance === "enforced" && (levels.length === 0 || levels.some((level) => level !== "native")) ? "agent_guided" : rule.assurance;
26221
+ return {
26222
+ name: rule.name,
26223
+ surface: rule.surface,
26224
+ assurance,
26225
+ reason: assurance === "unavailable" ? rule.unavailableReason ?? rule.reason : rule.reason,
26226
+ refs: rule.refs.map((handle) => ({ kind: "host", host: descriptor.host, handle }))
26227
+ };
26228
+ });
26229
+ // packages/workit-core/src/hooks/context.ts
26230
+ var cache = new Map;
26231
+ // packages/workit-core/src/hooks/handle.ts
26232
+ var offered = new Set;
26233
+ // packages/workit-core/src/hooks/hosts/fields.ts
26234
+ var SHELLS = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
25800
26235
 
25801
- // packages/workit-codex/hooks/workit-hook.ts
25802
- var ref = (host, handle) => ({ kind: "host", host, handle });
26236
+ // packages/workit-core/src/hooks/hosts/claude-code.ts
26237
+ var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
26238
+ var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
26239
+ var CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit", "SubagentStart"]);
26240
+
26241
+ // packages/workit-core/src/hooks/hosts/codex.ts
25803
26242
  function detectCodexSurface(env) {
25804
26243
  return env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || Boolean(env.CODEX_ELECTRON_RESOURCES_PATH) ? "codex_desktop" : "codex_cli";
25805
26244
  }
25806
- var codexCapabilities = (host, availability = {}) => {
25807
- const has = (key) => availability[key] === true;
25808
- return [
26245
+ var undocumented = { support: "undocumented", native: null };
26246
+ var CODEX_DESCRIPTOR = {
26247
+ host: "codex_cli",
26248
+ label: "Codex",
26249
+ verifiedAgainst: "codex-cli 0.153.4; Codex Desktop 26.901.20858",
26250
+ docs: ["https://developers.openai.com/codex/hooks"],
26251
+ transport: "hook-process",
26252
+ events: {
26253
+ "session.start": { support: "native", native: "SessionStart" },
26254
+ "context.turn": undocumented,
26255
+ "shell.pre": { support: "native", native: "PreToolUse" },
26256
+ "tool.pre": { support: "native", native: "PreToolUse" },
26257
+ "shell.post": undocumented,
26258
+ "subagent.start": { support: "native", native: "SubagentStart" },
26259
+ "subagent.stop": { support: "native", native: "SubagentStop" },
26260
+ "prompt.submit": undocumented,
26261
+ "compact.pre": { support: "none", native: null },
26262
+ stop: undocumented
26263
+ },
26264
+ shellPolicy: { deny: "native", channel: "permissionDecision", failClosed: false },
26265
+ context: {
26266
+ sessionStart: "native",
26267
+ perTurn: "undocumented",
26268
+ afterCompact: "native",
26269
+ task: "single-active"
26270
+ },
26271
+ subagents: {
26272
+ identity: "native",
26273
+ parentBinding: "none",
26274
+ blockStart: "none",
26275
+ worktreeIsolation: "undocumented",
26276
+ maxConcurrency: "undocumented"
26277
+ },
26278
+ provenance: { sessionId: "native", agentIdOnTool: "partial", postToolObserve: "undocumented" },
26279
+ interaction: { questions: "none", writeBoundary: "partial" },
26280
+ stopControl: "undocumented",
26281
+ shellAvailable: "native",
26282
+ perEventCost: "low",
26283
+ capabilities: [
25809
26284
  {
25810
26285
  name: "interactive_decision",
25811
26286
  surface: "Question",
26287
+ refs: ["Question"],
26288
+ requires: [],
25812
26289
  assurance: "agent_guided",
25813
- reason: "Codex hooks expose no native arbitrary-question answer receipt",
25814
- refs: [ref(host, "Question")]
26290
+ reason: "Codex hooks expose no native arbitrary-question answer receipt"
25815
26291
  },
25816
26292
  {
25817
26293
  name: "known_product_writes",
25818
26294
  surface: "PreToolUse",
26295
+ refs: ["PreToolUse"],
26296
+ requires: [],
25819
26297
  assurance: "unavailable",
25820
- reason: "file writes are host-policy; PreToolUse allows write tools",
25821
- refs: [ref(host, "PreToolUse")]
26298
+ reason: "file writes are host-policy; PreToolUse allows write tools"
25822
26299
  },
25823
26300
  {
25824
26301
  name: "fresh-context-review",
25825
26302
  surface: "SubagentStart",
25826
- assurance: has("subagentStart") ? "agent_guided" : "unavailable",
25827
- reason: has("subagentStart") ? "independent review runs as a bounded subagent; evidence evaluation enforces reviewer exclusivity, and stops remain untrusted" : "Codex subagent lifecycle hooks are unavailable for independent review",
25828
- refs: [ref(host, "SubagentStart")]
26303
+ refs: ["SubagentStart"],
26304
+ requires: ["event:subagent.start"],
26305
+ observed: ["subagent.start"],
26306
+ assurance: "agent_guided",
26307
+ reason: "independent review runs as a bounded subagent; evidence evaluation enforces reviewer exclusivity, and stops remain untrusted",
26308
+ unavailableReason: "Codex subagent lifecycle hooks are unavailable for independent review"
25829
26309
  },
25830
26310
  {
25831
26311
  name: "native_subagents",
25832
26312
  surface: "SubagentStart/SubagentStop",
25833
- assurance: has("subagentStart") && has("subagentStop") ? "agent_guided" : "unavailable",
25834
- reason: has("subagentStart") && has("subagentStop") ? "Codex reports stable child identities, but cannot block creation or bind a writer" : "Codex subagent lifecycle hooks are untrusted or incomplete",
25835
- refs: [ref(host, "SubagentStart"), ref(host, "SubagentStop")]
26313
+ refs: ["SubagentStart", "SubagentStop"],
26314
+ requires: ["event:subagent.start", "event:subagent.stop"],
26315
+ observed: ["subagent.start", "subagent.stop"],
26316
+ assurance: "agent_guided",
26317
+ reason: "Codex reports stable child identities, but cannot block creation or bind a writer",
26318
+ unavailableReason: "Codex subagent lifecycle hooks are untrusted or incomplete"
25836
26319
  },
25837
26320
  {
25838
26321
  name: "native_subagent_start",
25839
26322
  surface: "SubagentStart",
25840
- assurance: has("subagentStart") ? "agent_guided" : "unavailable",
25841
- reason: has("subagentStart") ? "SubagentStart supplies identity and bounded read-only guidance; continue:false cannot stop creation" : "SubagentStart hook is untrusted or unavailable",
25842
- refs: [ref(host, "SubagentStart")]
26323
+ refs: ["SubagentStart"],
26324
+ requires: ["event:subagent.start"],
26325
+ observed: ["subagent.start"],
26326
+ assurance: "agent_guided",
26327
+ reason: "SubagentStart supplies identity and bounded read-only guidance; continue:false cannot stop creation",
26328
+ unavailableReason: "SubagentStart hook is untrusted or unavailable"
25843
26329
  },
25844
26330
  {
25845
26331
  name: "arbitrary_shell_write",
25846
26332
  surface: "unobservable_shell",
26333
+ refs: ["PreToolUse"],
26334
+ requires: [],
25847
26335
  assurance: "unavailable",
25848
- reason: "Only covered known tool inputs are interceptable; specialized and write_stdin paths are not complete",
25849
- refs: [ref(host, "PreToolUse")]
26336
+ reason: "Only covered known tool inputs are interceptable; specialized and write_stdin paths are not complete"
25850
26337
  },
25851
26338
  {
25852
26339
  name: "compact_context",
25853
26340
  surface: "SessionStart",
25854
- assurance: has("sessionStart") ? "agent_guided" : "unavailable",
25855
- reason: "SessionStart source=compact is the single restore path",
25856
- refs: [ref(host, "SessionStart")]
26341
+ refs: ["SessionStart"],
26342
+ requires: ["context.afterCompact"],
26343
+ observed: ["session.start"],
26344
+ assurance: "agent_guided",
26345
+ reason: "SessionStart source=compact is the single restore path"
25857
26346
  }
25858
- ];
25859
- };
25860
- var events = new Set([
25861
- "SessionStart",
25862
- "PreToolUse",
25863
- "SubagentStart",
25864
- "SubagentStop"
25865
- ]);
25866
- var allowedKeys = {
25867
- SessionStart: new Set([
25868
- "hook_event_name",
25869
- "session_id",
25870
- "cwd",
25871
- "model",
25872
- "permission_mode",
25873
- "transcript_path",
25874
- "source"
25875
- ]),
25876
- PreToolUse: new Set([
25877
- "hook_event_name",
25878
- "session_id",
25879
- "cwd",
25880
- "model",
25881
- "permission_mode",
25882
- "transcript_path",
25883
- "turn_id",
25884
- "tool_name",
25885
- "tool_input",
25886
- "tool_use_id",
25887
- "agent_id",
25888
- "agent_type"
25889
- ]),
25890
- SubagentStart: new Set([
25891
- "hook_event_name",
25892
- "session_id",
25893
- "cwd",
25894
- "model",
25895
- "permission_mode",
25896
- "transcript_path",
25897
- "turn_id",
25898
- "agent_id",
25899
- "agent_type"
25900
- ]),
25901
- SubagentStop: new Set([
25902
- "hook_event_name",
25903
- "session_id",
25904
- "cwd",
25905
- "model",
25906
- "permission_mode",
25907
- "transcript_path",
25908
- "turn_id",
25909
- "agent_id",
25910
- "agent_type",
25911
- "agent_transcript_path",
25912
- "last_assistant_message",
25913
- "stop_hook_active"
25914
- ])
26347
+ ]
25915
26348
  };
25916
- var historyOfferSessions = new Set;
26349
+ var codexDescriptor = (host) => ({ ...CODEX_DESCRIPTOR, host });
26350
+ var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
26351
+ var SOURCES2 = new Set(["startup", "resume", "clear", "compact"]);
26352
+
26353
+ // packages/workit-core/src/hooks/hosts/cursor.ts
26354
+ var BLOCKING = new Set(["preToolUse", "beforeShellExecution", "subagentStart"]);
26355
+
26356
+ // packages/workit-core/src/hooks/run.ts
26357
+ if (false) {}
26358
+ // packages/workit-codex/hooks/workit-hook.ts
26359
+ var codexCapabilities = (host, availability = {}) => capabilitiesFor(codexDescriptor(host), {
26360
+ "session.start": availability.sessionStart,
26361
+ "shell.pre": availability.preToolUse,
26362
+ "subagent.start": availability.subagentStart,
26363
+ "subagent.stop": availability.subagentStop
26364
+ });
25917
26365
  if (false)
25918
26366
  ;
25919
26367