@brainervirus/workit-codex 1.0.9 → 1.0.11

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.
@@ -3,8 +3,8 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // packages/workit-codex/hooks/workit-hook.ts
6
- import { existsSync as existsSync3, realpathSync as realpathSync5, statSync } from "node:fs";
7
- import path6 from "node:path";
6
+ import { existsSync as existsSync4, realpathSync as realpathSync5, statSync } from "node:fs";
7
+ import path7 from "node:path";
8
8
 
9
9
  // packages/workit-core/src/core/task-contract.ts
10
10
  import { createHash, randomUUID } from "node:crypto";
@@ -7573,10 +7573,12 @@ var decisionSchema = object({
7573
7573
  scope: scopeSchema,
7574
7574
  presented: text,
7575
7575
  approvedContent: text,
7576
- contentRefs: array(refSchema)
7576
+ displayed: text.optional(),
7577
+ contentRefs: array(refSchema),
7578
+ statedChoice: object({ ref: text, text }).strict().optional()
7577
7579
  }).strict(),
7578
7580
  digest,
7579
- response: _enum(["approved", "rejected"]),
7581
+ response: _enum(["approved", "rejected", "stated"]),
7580
7582
  requirementIds: array(digest),
7581
7583
  revoked: object({ at: utc, reason: text }).strict().nullable(),
7582
7584
  consumption: object({
@@ -7651,6 +7653,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
7651
7653
  path: ["decisionId"]
7652
7654
  });
7653
7655
  });
7656
+ var runtimeSchema = object({
7657
+ createdWith: text.nullable(),
7658
+ updatedWith: text
7659
+ }).strict();
7654
7660
  var taskRecordSchema = object({
7655
7661
  schemaVersion: literal(1),
7656
7662
  id,
@@ -7664,6 +7670,8 @@ var taskRecordSchema = object({
7664
7670
  status: _enum(["active", "paused", "closed"]),
7665
7671
  closure: closureSchema.nullable(),
7666
7672
  progress: progressSchema,
7673
+ pauseReason: text.nullable().optional(),
7674
+ runtime: runtimeSchema.optional(),
7667
7675
  assessments: array(entrySchema(assessmentSchema)),
7668
7676
  policy: policySchema.nullable(),
7669
7677
  policyChanges: array(policyChangeSchema),
@@ -7679,6 +7687,7 @@ var workspaceRecordSchema = object({
7679
7687
  id,
7680
7688
  revision,
7681
7689
  root: nonEmpty,
7690
+ runtime: runtimeSchema.optional(),
7682
7691
  writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
7683
7692
  }).strict();
7684
7693
  var capabilitySchema = object({
@@ -7706,6 +7715,9 @@ var taskSummarySchema = object({
7706
7715
  id,
7707
7716
  revision,
7708
7717
  workspaceRevision: revision,
7718
+ createdAt: utc,
7719
+ updatedAt: utc,
7720
+ runtime: runtimeSchema.nullable(),
7709
7721
  objective: text,
7710
7722
  status: _enum(["active", "paused", "closed"]),
7711
7723
  closure: closureSchema.nullable(),
@@ -7776,7 +7788,7 @@ var taskOperations = {
7776
7788
  expectedWorkspaceRevision: revision.optional(),
7777
7789
  outcome: outcomeSchema,
7778
7790
  summary: text,
7779
- decisionIds: array(id)
7791
+ decisionIds: array(id).optional()
7780
7792
  })
7781
7793
  };
7782
7794
  var policyOperations = {
@@ -7868,14 +7880,15 @@ var writerOperations = {
7868
7880
  ...taskId,
7869
7881
  expectedRevision: revision.optional(),
7870
7882
  expectedWorkspaceRevision: revision.optional(),
7871
- workerId: nullableId.optional()
7883
+ workerId: nullableId.optional(),
7884
+ reason: text.optional()
7872
7885
  }),
7873
7886
  release: operation({
7874
7887
  action: literal("release"),
7875
7888
  ...taskId,
7876
7889
  expectedRevision: revision.optional(),
7877
7890
  expectedWorkspaceRevision: revision.optional(),
7878
- reason: text
7891
+ reason: text.optional()
7879
7892
  })
7880
7893
  };
7881
7894
  var stateOperations = {
@@ -8026,7 +8039,21 @@ function requirementId(input) {
8026
8039
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
8027
8040
  import * as fs4 from "node:fs";
8028
8041
  import { hostname } from "node:os";
8029
- import path3 from "node:path";
8042
+ import path4 from "node:path";
8043
+
8044
+ // packages/workit-core/src/core/package-root.ts
8045
+ import { existsSync } from "node:fs";
8046
+ import path from "node:path";
8047
+ import { fileURLToPath } from "node:url";
8048
+ var packageRoot = () => {
8049
+ let dir = path.dirname(fileURLToPath(import.meta.url));
8050
+ while (true) {
8051
+ const parent = path.dirname(dir);
8052
+ if (existsSync(path.join(dir, "package.json")) || parent === dir)
8053
+ return dir;
8054
+ dir = parent;
8055
+ }
8056
+ };
8030
8057
 
8031
8058
  // node_modules/@openclaw/fs-safe/dist/errors.js
8032
8059
  var OPERATIONAL_CODES = new Set([
@@ -8077,7 +8104,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
8077
8104
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
8078
8105
  import { randomBytes } from "node:crypto";
8079
8106
  import fsSync from "node:fs";
8080
- import path from "node:path";
8107
+ import path2 from "node:path";
8081
8108
 
8082
8109
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
8083
8110
  import fs from "node:fs";
@@ -8167,13 +8194,13 @@ ${ownershipToken}
8167
8194
  return { raw, ownershipToken };
8168
8195
  }
8169
8196
  function relativeSidecarLockPath(lockRoot, lockPath) {
8170
- const resolved = path.resolve(lockPath);
8171
- const lexicalRelative = path.relative(lockRoot.rootDir, resolved);
8172
- const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path.sep}`) && !path.isAbsolute(lexicalRelative) ? lexicalRelative : path.relative(lockRoot.rootReal, resolved);
8173
- if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
8197
+ const resolved = path2.resolve(lockPath);
8198
+ const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
8199
+ const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
8200
+ if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
8174
8201
  throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
8175
8202
  }
8176
- return relative.split(path.sep).join(path.posix.sep);
8203
+ return relative.split(path2.sep).join(path2.posix.sep);
8177
8204
  }
8178
8205
  function parseSidecarLockPayload(raw, parser) {
8179
8206
  if (parser) {
@@ -8320,7 +8347,7 @@ function getFsSafeLockConfig() {
8320
8347
 
8321
8348
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
8322
8349
  import fs3 from "node:fs";
8323
- import path2 from "node:path";
8350
+ import path3 from "node:path";
8324
8351
 
8325
8352
  // node_modules/@openclaw/fs-safe/dist/timing.js
8326
8353
  function sleepSync(ms) {
@@ -8411,26 +8438,26 @@ function canonicalLockParentSync(parent) {
8411
8438
  return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
8412
8439
  }
8413
8440
  function normalizeTargetPath(targetPath) {
8414
- const resolved = path2.resolve(targetPath);
8415
- fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
8441
+ const resolved = path3.resolve(targetPath);
8442
+ fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
8416
8443
  try {
8417
- return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
8444
+ return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
8418
8445
  } catch {
8419
8446
  return resolved;
8420
8447
  }
8421
8448
  }
8422
8449
  function boundedLockPath(lockPath, lockRoot) {
8423
- const resolved = path2.resolve(lockPath);
8450
+ const resolved = path3.resolve(lockPath);
8424
8451
  if (!lockRoot)
8425
8452
  return resolved;
8426
8453
  relativeSidecarLockPath(lockRoot, resolved);
8427
- const parent = path2.dirname(resolved);
8454
+ const parent = path3.dirname(resolved);
8428
8455
  const parentReal = canonicalLockParentSync(parent);
8429
- const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
8430
- if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
8456
+ const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
8457
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
8431
8458
  throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
8432
8459
  }
8433
- return path2.join(parentReal, path2.basename(resolved));
8460
+ return path3.join(parentReal, path3.basename(resolved));
8434
8461
  }
8435
8462
  function defaultShouldReclaim(snapshot, staleMs, nowMs) {
8436
8463
  const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
@@ -8643,6 +8670,33 @@ var metadataLockSchema = object({
8643
8670
  nonce: string2().min(1)
8644
8671
  }).strict();
8645
8672
  var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
8673
+ var cachedRuntimeVersion = null;
8674
+ var runtimeVersion = () => {
8675
+ if (cachedRuntimeVersion === null) {
8676
+ try {
8677
+ const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
8678
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
8679
+ } catch {
8680
+ cachedRuntimeVersion = "0.0.0";
8681
+ }
8682
+ }
8683
+ return cachedRuntimeVersion;
8684
+ };
8685
+ var stampNew = (record) => ({
8686
+ createdWith: record.runtime?.createdWith ?? null,
8687
+ updatedWith: runtimeVersion()
8688
+ });
8689
+ var isNewerVersion = (candidate, current) => {
8690
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
8691
+ const [next, now] = [parts(candidate), parts(current)];
8692
+ for (let index = 0;index < 3; index += 1) {
8693
+ const left = next[index] ?? 0;
8694
+ const right = now[index] ?? 0;
8695
+ if (left !== right)
8696
+ return left > right;
8697
+ }
8698
+ return false;
8699
+ };
8646
8700
  var jsonBytes = (value) => `${canonicalJson(value)}
8647
8701
  `;
8648
8702
  var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
@@ -8669,12 +8723,12 @@ var sameMetadataLock = (left, right) => {
8669
8723
  class TaskStore {
8670
8724
  root;
8671
8725
  constructor(root) {
8672
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path3.resolve(root);
8726
+ this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
8673
8727
  }
8674
8728
  readTask(taskId) {
8675
8729
  if (!validId(taskId))
8676
8730
  return failure("invalid_input", "task ID is invalid", { taskId });
8677
- const result = this.readRecord(path3.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
8731
+ const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
8678
8732
  if (!result.exists)
8679
8733
  return failure("not_found", "task not found", { taskId });
8680
8734
  if (result.result.ok && result.result.data.id !== taskId)
@@ -8708,7 +8762,7 @@ class TaskStore {
8708
8762
  });
8709
8763
  const tasks = [];
8710
8764
  for (const name of names.sort()) {
8711
- const item = this.readRecord(path3.join(this.tasksDir, name), taskRecordSchema);
8765
+ const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
8712
8766
  if (!item.exists)
8713
8767
  continue;
8714
8768
  if (!item.result.ok)
@@ -8742,7 +8796,7 @@ class TaskStore {
8742
8796
  if (!current.ok)
8743
8797
  return current;
8744
8798
  if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
8745
- return failure("revision_conflict", "workspace revision does not match", {
8799
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
8746
8800
  expectedWorkspaceRevision: value.expectedWorkspaceRevision,
8747
8801
  actualWorkspaceRevision: current.data?.revision ?? null
8748
8802
  });
@@ -8754,11 +8808,17 @@ class TaskStore {
8754
8808
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
8755
8809
  if (current.data && !previousWorkspaceBytes)
8756
8810
  return failure("storage_error", "workspace snapshot disappeared during creation");
8757
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
8811
+ const workspace = current.data ? {
8812
+ ...current.data,
8813
+ revision: newRevision(),
8814
+ root: this.root,
8815
+ runtime: stampNew(current.data)
8816
+ } : {
8758
8817
  schemaVersion: SCHEMA_VERSION,
8759
8818
  id: newId(),
8760
8819
  revision: newRevision(),
8761
8820
  root: this.root,
8821
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8762
8822
  writer: null
8763
8823
  };
8764
8824
  const timestamp = value.now ?? now();
@@ -8780,6 +8840,7 @@ class TaskStore {
8780
8840
  status: "active",
8781
8841
  closure: null,
8782
8842
  progress: { summary: "", nextAction: null, blockers: [] },
8843
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8783
8844
  assessments: [],
8784
8845
  policy: null,
8785
8846
  policyChanges: [],
@@ -8810,7 +8871,7 @@ class TaskStore {
8810
8871
  if (!current.ok)
8811
8872
  return current;
8812
8873
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
8813
- return failure("revision_conflict", "workspace revision does not match", {
8874
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
8814
8875
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
8815
8876
  actualWorkspaceRevision: current.data?.revision ?? null
8816
8877
  });
@@ -8818,11 +8879,17 @@ class TaskStore {
8818
8879
  if (current.data && !previousWorkspaceBytes)
8819
8880
  return failure("storage_error", "workspace snapshot disappeared during import");
8820
8881
  const timestamp = input.now ?? now();
8821
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
8882
+ const workspace = current.data ? {
8883
+ ...current.data,
8884
+ revision: newRevision(),
8885
+ root: this.root,
8886
+ runtime: stampNew(current.data)
8887
+ } : {
8822
8888
  schemaVersion: SCHEMA_VERSION,
8823
8889
  id: input.workspaceId ?? newId(),
8824
8890
  revision: newRevision(),
8825
8891
  root: this.root,
8892
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8826
8893
  writer: null
8827
8894
  };
8828
8895
  const task = {
@@ -8830,7 +8897,11 @@ class TaskStore {
8830
8897
  workspaceId: workspace.id,
8831
8898
  revision: newRevision(),
8832
8899
  createdAt: timestamp,
8833
- updatedAt: timestamp
8900
+ updatedAt: timestamp,
8901
+ runtime: {
8902
+ createdWith: input.task.runtime?.createdWith ?? null,
8903
+ updatedWith: runtimeVersion()
8904
+ }
8834
8905
  };
8835
8906
  const validTask = taskRecordSchema.safeParse(task);
8836
8907
  if (!validTask.success)
@@ -8879,7 +8950,8 @@ class TaskStore {
8879
8950
  workspaceId: current.data.workspaceId,
8880
8951
  createdAt: current.data.createdAt,
8881
8952
  revision: context.revision,
8882
- updatedAt: context.now
8953
+ updatedAt: context.now,
8954
+ runtime: stampNew(current.data)
8883
8955
  };
8884
8956
  const valid = taskRecordSchema.safeParse(record);
8885
8957
  if (!valid.success)
@@ -8913,7 +8985,8 @@ class TaskStore {
8913
8985
  ...changed.data,
8914
8986
  id: current.data.id,
8915
8987
  root: this.root,
8916
- revision: context.revision
8988
+ revision: context.revision,
8989
+ runtime: stampNew(current.data)
8917
8990
  };
8918
8991
  const valid = workspaceRecordSchema.safeParse(record);
8919
8992
  if (!valid.success)
@@ -8956,7 +9029,8 @@ class TaskStore {
8956
9029
  ...changedWorkspace.data,
8957
9030
  id: workspace.data.id,
8958
9031
  root: this.root,
8959
- revision: workspaceContext.revision
9032
+ revision: workspaceContext.revision,
9033
+ runtime: stampNew(workspace.data)
8960
9034
  });
8961
9035
  if (!nextWorkspace.success)
8962
9036
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -8981,7 +9055,8 @@ class TaskStore {
8981
9055
  workspaceId: task.data.workspaceId,
8982
9056
  createdAt: task.data.createdAt,
8983
9057
  revision: taskContext.revision,
8984
- updatedAt: taskContext.now
9058
+ updatedAt: taskContext.now,
9059
+ runtime: stampNew(task.data)
8985
9060
  });
8986
9061
  if (!nextTask.success) {
8987
9062
  this.markUncertain(nextWorkspace.data);
@@ -9008,7 +9083,7 @@ class TaskStore {
9008
9083
  if (match)
9009
9084
  candidates.push({
9010
9085
  target: match[1],
9011
- path: path3.join(this.recoveryDir, name),
9086
+ path: path4.join(this.recoveryDir, name),
9012
9087
  digest: match[3]
9013
9088
  });
9014
9089
  }
@@ -9201,7 +9276,8 @@ class TaskStore {
9201
9276
  const value = {
9202
9277
  ...workspace,
9203
9278
  writer: { ...workspace.writer, state: "uncertain" },
9204
- revision: newRevision()
9279
+ revision: newRevision(),
9280
+ runtime: stampNew(workspace)
9205
9281
  };
9206
9282
  this.replaceSnapshot(this.workspacePath, value, workspace);
9207
9283
  }
@@ -9279,7 +9355,7 @@ class TaskStore {
9279
9355
  }
9280
9356
  fs4.renameSync(temporary, file);
9281
9357
  temporary = undefined;
9282
- this.fsyncDirectory(path3.dirname(file));
9358
+ this.fsyncDirectory(path4.dirname(file));
9283
9359
  return success(null, null, value);
9284
9360
  } catch (error) {
9285
9361
  return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
@@ -9293,9 +9369,9 @@ class TaskStore {
9293
9369
  }
9294
9370
  }
9295
9371
  saveRecovery(file, bytes) {
9296
- const target = path3.basename(file) === "workspace.json" ? "workspace" : "task";
9297
- const id = target === "task" ? path3.basename(file, ".json") : "workspace";
9298
- const destination = path3.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
9372
+ const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
9373
+ const id = target === "task" ? path4.basename(file, ".json") : "workspace";
9374
+ const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
9299
9375
  if (fs4.existsSync(destination)) {
9300
9376
  if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
9301
9377
  return;
@@ -9373,7 +9449,7 @@ class TaskStore {
9373
9449
  for (const name of fs4.readdirSync(this.tasksDir)) {
9374
9450
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
9375
9451
  return null;
9376
- const parsed = this.parseBytes(fs4.readFileSync(path3.join(this.tasksDir, name)), taskRecordSchema);
9452
+ const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
9377
9453
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
9378
9454
  return null;
9379
9455
  ids.add(parsed.data.workspaceId);
@@ -9415,7 +9491,12 @@ class TaskStore {
9415
9491
  if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
9416
9492
  return failure("unsupported_version", "unsupported snapshot schema version");
9417
9493
  const parsed = schema.safeParse(value);
9418
- return parsed.success ? success(null, null, parsed.data) : failure("recovery_required", "snapshot does not satisfy its schema");
9494
+ if (parsed.success)
9495
+ return success(null, null, parsed.data);
9496
+ const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
9497
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
9498
+ return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
9499
+ return failure("recovery_required", "snapshot does not satisfy its schema");
9419
9500
  }
9420
9501
  processStart(pid) {
9421
9502
  try {
@@ -9425,43 +9506,98 @@ class TaskStore {
9425
9506
  }
9426
9507
  }
9427
9508
  conflict(expected, actual) {
9428
- return failure("revision_conflict", "snapshot revision does not match", {
9509
+ return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
9429
9510
  expectedRevision: expected,
9430
9511
  actualRevision: actual
9431
9512
  });
9432
9513
  }
9433
9514
  taskPath(taskId) {
9434
- return path3.join(this.tasksDir, `${taskId}.json`);
9515
+ return path4.join(this.tasksDir, `${taskId}.json`);
9435
9516
  }
9436
9517
  get workitDir() {
9437
- return path3.join(this.root, ".workit");
9518
+ return path4.join(this.root, ".workit");
9438
9519
  }
9439
9520
  get tasksDir() {
9440
- return path3.join(this.workitDir, "tasks");
9521
+ return path4.join(this.workitDir, "tasks");
9441
9522
  }
9442
9523
  get recoveryDir() {
9443
- return path3.join(this.workitDir, "recovery");
9524
+ return path4.join(this.workitDir, "recovery");
9444
9525
  }
9445
9526
  get workspacePath() {
9446
- return path3.join(this.workitDir, "workspace.json");
9527
+ return path4.join(this.workitDir, "workspace.json");
9447
9528
  }
9448
9529
  get lockPath() {
9449
- return path3.join(this.workitDir, "metadata.lock");
9530
+ return path4.join(this.workitDir, "metadata.lock");
9450
9531
  }
9451
9532
  get gitignorePath() {
9452
- return path3.join(this.workitDir, ".gitignore");
9533
+ return path4.join(this.workitDir, ".gitignore");
9453
9534
  }
9454
9535
  }
9536
+ // packages/workit-core/src/core/workers.ts
9537
+ var same = (left, right) => {
9538
+ try {
9539
+ return canonicalJson(left) === canonicalJson(right);
9540
+ } catch {
9541
+ return false;
9542
+ }
9543
+ };
9544
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
9545
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
9546
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
9547
+ var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
9548
+ const code = char.charCodeAt(0);
9549
+ return code < 32 || code === 127;
9550
+ }) && !value.split(/[\\/]/).includes("..");
9551
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
9552
+ var workerIdOf = (value) => value.workerId ?? null;
9553
+ function assertProductWriteAllowed(input) {
9554
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
9555
+ return failure("invalid_input", "invalid product write path");
9556
+ if (!input.store)
9557
+ return failure("permission_denied", "write authorization requires core state");
9558
+ const task = input.store.readTask(input.task.id);
9559
+ if (!task.ok)
9560
+ return task;
9561
+ const workspace = input.store.readWorkspace();
9562
+ if (!workspace.ok)
9563
+ return workspace;
9564
+ if (!workspace.data)
9565
+ return failure("not_found", "workspace not found");
9566
+ const workspaceRecord = workspace.data;
9567
+ if (task.data.status !== "active")
9568
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
9569
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
9570
+ return failure("recovery_required", "task and workspace bindings are invalid");
9571
+ const writer = workspaceRecord.writer;
9572
+ if (!writer)
9573
+ return failure("permission_denied", "checkout has no writer owner");
9574
+ if (writer.state === "uncertain")
9575
+ return failure("recovery_required", "checkout writer requires recovery", {
9576
+ owner: writer.owner
9577
+ });
9578
+ const caller = callerValue(input.caller);
9579
+ const workerId = workerIdOf(input.caller);
9580
+ const owner = writer.owner;
9581
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
9582
+ return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
9583
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
9584
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
9585
+ return failure("permission_denied", "only implementers can own product writes");
9586
+ 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))))
9587
+ return failure("recovery_required", "worker session is not an active writer");
9588
+ return success(null, workspaceRecord.revision, owner);
9589
+ }
9590
+
9455
9591
  // packages/workit-core/src/core/task-evaluation.ts
9456
9592
  import { createHash as createHash4 } from "node:crypto";
9457
9593
  import * as fs6 from "node:fs";
9458
9594
  import { spawnSync } from "node:child_process";
9459
- import path5 from "node:path";
9595
+ import path6 from "node:path";
9460
9596
 
9461
9597
  // packages/workit-core/src/core/authority.ts
9462
9598
  import { createHash as createHash3 } from "node:crypto";
9463
9599
  import * as fs5 from "node:fs";
9464
- import path4 from "node:path";
9600
+ import path5 from "node:path";
9465
9601
  var verifiedAuthorities = new WeakMap;
9466
9602
  var trustedAuthority = (kind, provenance, expected, binding, caller) => {
9467
9603
  const token = {};
@@ -9588,20 +9724,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
9588
9724
  continue;
9589
9725
  if (!reference.digest)
9590
9726
  return failure("invalid_input", "document references require a byte digest");
9591
- const target = path4.resolve(root, reference.path);
9592
- if (target !== root && !target.startsWith(`${root}${path4.sep}`))
9593
- return failure("invalid_input", "document reference escapes checkout");
9727
+ const target = path5.resolve(root, reference.path);
9728
+ if (target !== root && !target.startsWith(`${root}${path5.sep}`))
9729
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
9730
+ fields: [
9731
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
9732
+ ]
9733
+ });
9594
9734
  try {
9595
- const relative = path4.relative(root, target);
9735
+ const relative = path5.relative(root, target);
9596
9736
  let current = root;
9597
- for (const segment of relative.split(path4.sep)) {
9737
+ for (const segment of relative.split(path5.sep)) {
9598
9738
  if (!segment)
9599
9739
  continue;
9600
- current = path4.join(current, segment);
9740
+ current = path5.join(current, segment);
9601
9741
  const stat = fs5.lstatSync(current);
9602
9742
  const real = fs5.realpathSync(current);
9603
- if (real !== root && !real.startsWith(`${root}${path4.sep}`))
9604
- return failure("invalid_input", "document reference escapes checkout");
9743
+ if (real !== root && !real.startsWith(`${root}${path5.sep}`))
9744
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
9745
+ fields: [
9746
+ {
9747
+ path: "contentRefs.path",
9748
+ reason: `${reference.path} resolves outside the checkout`
9749
+ }
9750
+ ]
9751
+ });
9605
9752
  if (stat.isSymbolicLink())
9606
9753
  return failure("invalid_input", "document reference uses a symlink");
9607
9754
  if (current === target && !stat.isFile())
@@ -9948,14 +10095,14 @@ var verifyDecisionContent = verifyContentRefs;
9948
10095
 
9949
10096
  // packages/workit-core/src/core/task-evaluation.ts
9950
10097
  var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
9951
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path5.sep}`);
9952
- var relative = (root, candidate) => path5.relative(root, candidate).split(path5.sep).join("/") || ".";
10098
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
10099
+ var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
9953
10100
  var canonicalPath = (value) => {
9954
10101
  if (typeof value !== "string" || !value)
9955
10102
  return null;
9956
10103
  if (value.includes("\\") || value.split("/").includes(".."))
9957
10104
  return null;
9958
- const normalized = path5.posix.normalize(value);
10105
+ const normalized = path6.posix.normalize(value);
9959
10106
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
9960
10107
  return null;
9961
10108
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -10003,14 +10150,14 @@ var gitPaths = (root) => {
10003
10150
  git: /not a git repository/i.test(stderr) ? false : true
10004
10151
  };
10005
10152
  }
10006
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")) : [];
10153
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
10007
10154
  const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
10008
10155
  if (stagedDeleted.status !== 0) {
10009
10156
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
10010
10157
  if (!/not a git repository/i.test(stderr))
10011
10158
  return { paths, uncertain: true, git: true };
10012
10159
  } else if (stagedDeleted.stdout) {
10013
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")));
10160
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
10014
10161
  }
10015
10162
  return { paths: [...new Set(paths)], uncertain: false, git: true };
10016
10163
  };
@@ -10025,7 +10172,7 @@ var walk = (root, directory, output) => {
10025
10172
  for (const name of names) {
10026
10173
  if (name === ".git" || name === ".workit")
10027
10174
  continue;
10028
- const target = path5.join(directory, name);
10175
+ const target = path6.join(directory, name);
10029
10176
  let stat;
10030
10177
  try {
10031
10178
  stat = fs6.lstatSync(target);
@@ -10043,17 +10190,17 @@ var walk = (root, directory, output) => {
10043
10190
  var scopeRoots = (root, scope) => {
10044
10191
  const roots = [];
10045
10192
  for (const item of scope.paths.length ? scope.paths : ["."]) {
10046
- const target = path5.resolve(root, item);
10193
+ const target = path6.resolve(root, item);
10047
10194
  if (!inside(root, target))
10048
- return failure("invalid_input", "candidate scope escapes checkout");
10195
+ return failure("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` }] });
10049
10196
  let ancestor = target;
10050
10197
  while (!fs6.existsSync(ancestor) && ancestor !== root)
10051
- ancestor = path5.dirname(ancestor);
10198
+ ancestor = path6.dirname(ancestor);
10052
10199
  try {
10053
10200
  if (!inside(root, fs6.realpathSync(ancestor)))
10054
- return failure("invalid_input", "candidate scope escapes checkout");
10201
+ return failure("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` }] });
10055
10202
  } catch {
10056
- return failure("invalid_input", "candidate scope cannot be inspected");
10203
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
10057
10204
  }
10058
10205
  roots.push(target);
10059
10206
  }
@@ -10068,7 +10215,7 @@ function captureCandidate(root, scope, environment = []) {
10068
10215
  }
10069
10216
  const normalizedScope = canonicalScope(scope);
10070
10217
  if (!normalizedScope)
10071
- return failure("invalid_input", "candidate scope is invalid");
10218
+ return failure("invalid_input", "candidate scope is invalid: paths must be checkout-relative — to work in another repository, keep this task here and coordinate a separate linked task in that checkout", { fields: [{ path: "scope.paths", reason: "must be checkout-relative" }] });
10072
10219
  const roots = scopeRoots(checkout, normalizedScope);
10073
10220
  if (!roots.ok)
10074
10221
  return roots;
@@ -10100,7 +10247,7 @@ function captureCandidate(root, scope, environment = []) {
10100
10247
  for (const item of [...names].sort()) {
10101
10248
  if (!scopeMatches(item, normalizedScope))
10102
10249
  continue;
10103
- const target = path5.join(checkout, item);
10250
+ const target = path6.join(checkout, item);
10104
10251
  try {
10105
10252
  const stat = fs6.lstatSync(target);
10106
10253
  if (stat.isSymbolicLink()) {
@@ -10222,7 +10369,7 @@ function evaluateEvidence(task, candidate) {
10222
10369
  }
10223
10370
  var scopeCovers2 = scopeCovers;
10224
10371
  var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
10225
- var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && data.response === "approved" && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
10372
+ var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
10226
10373
  var evidenceMatchesRequirement = (kind, evidenceKind, dimension) => {
10227
10374
  if (kind !== "passed")
10228
10375
  return false;
@@ -10257,7 +10404,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10257
10404
  return true;
10258
10405
  const reviewSession = sessionFromRef(entry.data.reviewContext);
10259
10406
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
10260
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
10407
+ const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
10261
10408
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
10262
10409
  });
10263
10410
  if (passed.length) {
@@ -10298,7 +10445,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10298
10445
  status: "satisfied",
10299
10446
  evidenceIds: [],
10300
10447
  decisionIds: decisions.map((decision) => decision.id),
10301
- reason: "an applicable approved decision satisfies the requirement"
10448
+ reason: "an applicable approved or stated decision satisfies the requirement"
10302
10449
  };
10303
10450
  const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
10304
10451
  if (limitations.length)
@@ -10353,7 +10500,17 @@ function evaluateClosure(requestedOutcome, view) {
10353
10500
  const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
10354
10501
  if (requestedOutcome !== "stopped" && blocking.length)
10355
10502
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
10356
- requirementIds: blocking.map((item) => item.requirementId)
10503
+ requirementIds: blocking.map((item) => item.requirementId),
10504
+ requirements: blocking.map((item) => {
10505
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
10506
+ return {
10507
+ requirementId: item.requirementId,
10508
+ ruleId: requirement?.ruleId ?? "unknown",
10509
+ reason: item.reason,
10510
+ satisfaction: requirement?.satisfaction ?? "",
10511
+ dependentAction: requirement?.dependentAction ?? null
10512
+ };
10513
+ })
10357
10514
  });
10358
10515
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
10359
10516
  if (requestedOutcome === "verified" && accepted.length)
@@ -10384,7 +10541,7 @@ function evaluateClosure(requestedOutcome, view) {
10384
10541
  var METHODS = {
10385
10542
  "workit-challenge": { dimensions: ["challenge", "decisions"] },
10386
10543
  "workit-behavioral-tdd": { dimensions: ["testing"] },
10387
- "workit-review": { dimensions: ["review"] },
10544
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
10388
10545
  "workit-plan": { dimensions: ["artifacts", "continuity"] },
10389
10546
  "workit-implement": { dimensions: ["delegation"] },
10390
10547
  "workit-debug": { ruleIds: ["root-cause-investigation"] },
@@ -10446,7 +10603,18 @@ decision.record {binding with taskId/workspaceId from inspect plus presented and
10446
10603
  approvedContent} only through a native approval question asked receipt-shaped:
10447
10604
  header Workit decision: <purpose>, the same label repeated in the question
10448
10605
  text (some host UIs do not render headers), exactly two options approved and
10449
- rejected; ask once, never re-ask to mint a receipt. task.close {outcome,
10606
+ rejected; ask once, never re-ask to mint a receipt. Before asking, present the
10607
+ item in the conversation (durable artifacts: complete digest plus exact path;
10608
+ inline plans: the content) and keep the question to one short scoped sentence;
10609
+ never ask to approve something unseen. An answer with custom text is steering,
10610
+ not approval: adjust, re-present, then ask a new scoped question. After the user
10611
+ approves a spec or plan, execute the remaining tasks continuously with one
10612
+ atomic commit per task and do not ask whether to continue; stop only for a new
10613
+ product decision, a failed safety or verification gate, a conflicting edit, or
10614
+ missing authority. Record the plan's commit list once through the plan-scoped
10615
+ action approval (git.commit with plan_steps and plan_branch) so each listed
10616
+ commit executes without a new question; an unlisted message needs a fresh
10617
+ approval. task.close {outcome,
10450
10618
  summary, decisionIds}. A completed native subagent run stops its bound worker
10451
10619
  by itself; if a worker strands in cancelling with its run verifiably over,
10452
10620
  repeat worker.cancel {reason} to confirm the stop. Check and review evidence
@@ -10478,7 +10646,7 @@ function reconcileResume(view, observations = []) {
10478
10646
  return candidate;
10479
10647
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
10480
10648
  const blockers = [...view.task.progress.blockers];
10481
- if (view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
10649
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
10482
10650
  blockers.push({
10483
10651
  reason: "worker state requires reconciliation",
10484
10652
  dependentAction: "resume",
@@ -10610,6 +10778,23 @@ function shellRouteIntent(command) {
10610
10778
  }
10611
10779
  return null;
10612
10780
  }
10781
+ function hasLiveWorkitTask(dir) {
10782
+ try {
10783
+ if (typeof dir !== "string" || dir === "")
10784
+ return false;
10785
+ const listed = new TaskStore(dir).listTasks();
10786
+ if (!listed.ok)
10787
+ return false;
10788
+ return listed.data.some((task) => task.status === "active" || task.status === "paused");
10789
+ } catch {
10790
+ return false;
10791
+ }
10792
+ }
10793
+ function shouldDenyShellRoute(dir, command) {
10794
+ if (!hasLiveWorkitTask(dir))
10795
+ return null;
10796
+ return shellRouteIntent(command);
10797
+ }
10613
10798
  // packages/workit-core/src/core/external-action.ts
10614
10799
  var externalActionSchema = discriminatedUnion("operation", [
10615
10800
  object({
@@ -10623,7 +10808,11 @@ var externalActionSchema = discriminatedUnion("operation", [
10623
10808
  }).strict(),
10624
10809
  object({
10625
10810
  operation: literal("git.commit"),
10626
- payload: object({ message: string2().min(1) }).strict()
10811
+ payload: object({
10812
+ message: string2().min(1).optional(),
10813
+ plan_steps: array(string2().min(1)).optional(),
10814
+ plan_branch: string2().min(1).optional()
10815
+ }).strict()
10627
10816
  }).strict(),
10628
10817
  object({
10629
10818
  operation: literal("git.push"),
@@ -10858,7 +11047,7 @@ function resolveRequirements(input) {
10858
11047
  ].join(", ")}`,
10859
11048
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
10860
11049
  before: "dependent_action",
10861
- dependentAction: "dependent-action",
11050
+ dependentAction: null,
10862
11051
  acceptanceAllowed: false
10863
11052
  }));
10864
11053
  if (signals.productChoiceOpen.value === true)
@@ -10869,7 +11058,7 @@ function resolveRequirements(input) {
10869
11058
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
10870
11059
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
10871
11060
  before: "dependent_action",
10872
- dependentAction: "dependent-action",
11061
+ dependentAction: null,
10873
11062
  acceptanceAllowed: false
10874
11063
  }));
10875
11064
  if (signals.behaviorChange.value === true)
@@ -11027,59 +11216,6 @@ function diffPolicy(previous, next, reason, now) {
11027
11216
  return parsed.data;
11028
11217
  }
11029
11218
 
11030
- // packages/workit-core/src/core/workers.ts
11031
- var same = (left, right) => {
11032
- try {
11033
- return canonicalJson(left) === canonicalJson(right);
11034
- } catch {
11035
- return false;
11036
- }
11037
- };
11038
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
11039
- var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
11040
- const code = char.charCodeAt(0);
11041
- return code < 32 || code === 127;
11042
- }) && !value.split(/[\\/]/).includes("..");
11043
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
11044
- var workerIdOf = (value) => value.workerId ?? null;
11045
- function assertProductWriteAllowed(input) {
11046
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
11047
- return failure("invalid_input", "invalid product write path");
11048
- if (!input.store)
11049
- return failure("permission_denied", "write authorization requires core state");
11050
- const task = input.store.readTask(input.task.id);
11051
- if (!task.ok)
11052
- return task;
11053
- const workspace = input.store.readWorkspace();
11054
- if (!workspace.ok)
11055
- return workspace;
11056
- if (!workspace.data)
11057
- return failure("not_found", "workspace not found");
11058
- const workspaceRecord = workspace.data;
11059
- if (task.data.status !== "active")
11060
- return failure("invalid_transition", "paused or closed tasks cannot own product writes");
11061
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
11062
- return failure("recovery_required", "task and workspace bindings are invalid");
11063
- const writer = workspaceRecord.writer;
11064
- if (!writer)
11065
- return failure("permission_denied", "checkout has no writer owner");
11066
- if (writer.state === "uncertain")
11067
- return failure("recovery_required", "checkout writer requires recovery", {
11068
- owner: writer.owner
11069
- });
11070
- const caller = callerValue(input.caller);
11071
- const workerId = workerIdOf(input.caller);
11072
- const owner = writer.owner;
11073
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
11074
- return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
11075
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
11076
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
11077
- return failure("permission_denied", "only implementers can own product writes");
11078
- 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))))
11079
- return failure("recovery_required", "worker session is not an active writer");
11080
- return success(null, workspaceRecord.revision, owner);
11081
- }
11082
-
11083
11219
  // packages/workit-core/src/core/task-engine.ts
11084
11220
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
11085
11221
  kind,
@@ -11210,15 +11346,20 @@ var applyWorkerLifecycle = (input) => {
11210
11346
  return failure("invalid_transition", "worker has not started");
11211
11347
  if (input.state === "running" && entry.data.state === "stopped")
11212
11348
  return failure("invalid_transition", "stopped worker cannot run again");
11349
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
11213
11350
  const nextEntry = {
11214
11351
  ...entry,
11215
11352
  recordedAt: input.now ?? entry.recordedAt,
11216
11353
  provenance: authority.provenance,
11217
- data: { ...entry.data, state: input.state, session: input.session }
11354
+ data: {
11355
+ ...entry.data,
11356
+ state: terminalCancel ? "cancelling" : input.state,
11357
+ session: input.session
11358
+ }
11218
11359
  };
11219
11360
  const shouldClear = input.state === "stopped" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
11220
11361
  const shouldUncertain = input.state === "unknown" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
11221
- if (entry.data.state === input.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
11362
+ if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
11222
11363
  return success(task.data.revision, workspace.data.revision, entry);
11223
11364
  const changed = input.store.mutateTaskAndWorkspace({
11224
11365
  taskId: task.data.id,
@@ -11588,7 +11729,7 @@ class WorkitCore {
11588
11729
  activeWorkerBlocker(task, workspace) {
11589
11730
  if (workspace.writer)
11590
11731
  return failure("recovery_required", "writer ownership must be released first");
11591
- if (task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
11732
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
11592
11733
  return failure("recovery_required", "worker state requires reconciliation");
11593
11734
  return null;
11594
11735
  }
@@ -11825,11 +11966,6 @@ class WorkitCore {
11825
11966
  if (helper && !helper.ok)
11826
11967
  return helper;
11827
11968
  const evidence = input.evidence;
11828
- if (helper?.ok) {
11829
- const assignment = helper.data.data.assignment;
11830
- if (evidence.requirementIds.some((id) => !assignment.requirementIds.includes(id)) || assignment.candidateId !== null && evidence.candidateId !== assignment.candidateId && evidence.beforeCandidateId !== assignment.candidateId || !refsWithinScope(evidence.refs, assignment.scope) || !refsWithinScope(evidence.refs, task.data.intent.data.scope))
11831
- return failure("permission_denied", "evidence is outside the worker assignment");
11832
- }
11833
11969
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
11834
11970
  return failure("invalid_input", "missing or skipped evidence requires a reason");
11835
11971
  if (evidence.kind === "review") {
@@ -11846,6 +11982,11 @@ class WorkitCore {
11846
11982
  evidence.beforeCandidateId ??= null;
11847
11983
  evidence.candidateId ??= null;
11848
11984
  }
11985
+ if (helper?.ok) {
11986
+ const assignment = helper.data.data.assignment;
11987
+ if (evidence.requirementIds.some((id) => !assignment.requirementIds.includes(id)) || assignment.candidateId !== null && evidence.candidateId !== assignment.candidateId && evidence.beforeCandidateId !== assignment.candidateId || !refsWithinScope(evidence.refs, assignment.scope) || !refsWithinScope(evidence.refs, task.data.intent.data.scope))
11988
+ return failure("permission_denied", "evidence is outside the worker assignment");
11989
+ }
11849
11990
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
11850
11991
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
11851
11992
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -11899,7 +12040,12 @@ class WorkitCore {
11899
12040
  const input = parsed.data;
11900
12041
  if (nativeRequired && input.action !== "record")
11901
12042
  return failure("permission_denied", "native observation is only valid for recording decisions");
11902
- if (nativeRequired && nativeObservation === undefined)
12043
+ const stated = input.action === "record" && input.response === "stated";
12044
+ if (stated && input.purpose === "action")
12045
+ return failure("permission_denied", "stated choices cannot authorize mutating actions");
12046
+ if (stated && !input.binding?.statedChoice)
12047
+ return failure("invalid_input", "stated choices require binding.statedChoice");
12048
+ if (nativeRequired && nativeObservation === undefined && !stated)
11903
12049
  return failure("permission_denied", "native decision observation is required");
11904
12050
  const task = this.store.readTask(input.taskId);
11905
12051
  if (!task.ok)
@@ -11932,7 +12078,7 @@ class WorkitCore {
11932
12078
  consumption: null
11933
12079
  };
11934
12080
  const data = { ...base, digest: decisionDigest(base) };
11935
- const native = nativeRequired ? verifyNativeDecision(this.context.nativeAuthority, {
12081
+ const native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
11936
12082
  observation: nativeObservation,
11937
12083
  expected: {
11938
12084
  taskId: task.data.id,
@@ -12741,6 +12887,9 @@ class WorkitCore {
12741
12887
  id: task.id,
12742
12888
  revision: task.revision,
12743
12889
  workspaceRevision: workspace.data.revision,
12890
+ createdAt: task.createdAt,
12891
+ updatedAt: task.updatedAt,
12892
+ runtime: task.runtime ?? null,
12744
12893
  objective: task.intent.data.objective,
12745
12894
  status: task.status,
12746
12895
  closure: task.closure,
@@ -12826,7 +12975,7 @@ class WorkitCore {
12826
12975
  const base = reconcileResume(effectiveView);
12827
12976
  if (!base.ok)
12828
12977
  return base;
12829
- const needsReconciliation = view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
12978
+ const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
12830
12979
  const blockers = [...base.data.blockers];
12831
12980
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
12832
12981
  blockers.push({
@@ -12893,7 +13042,8 @@ class WorkitCore {
12893
13042
  ...current,
12894
13043
  status,
12895
13044
  candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
12896
- progress: status === "paused" ? { ...current.progress, summary: input.reason } : current.progress
13045
+ progress: current.progress,
13046
+ pauseReason: status === "paused" ? input.reason ?? null : null
12897
13047
  })
12898
13048
  });
12899
13049
  if (!changed.ok)
@@ -12929,7 +13079,7 @@ class WorkitCore {
12929
13079
  return workspace;
12930
13080
  if (!workspace.data)
12931
13081
  return failure("not_found", "workspace not found");
12932
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
13082
+ if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
12933
13083
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
12934
13084
  this.fillRevisions(input, task.data, workspace.data);
12935
13085
  const changed = this.store.mutateTaskAndWorkspace({
@@ -13040,6 +13190,13 @@ var codexCapabilities = (host, availability = {}) => {
13040
13190
  reason: "file writes are host-policy; PreToolUse allows write tools",
13041
13191
  refs: [ref(host, "PreToolUse")]
13042
13192
  },
13193
+ {
13194
+ name: "fresh-context-review",
13195
+ surface: "SubagentStart",
13196
+ assurance: has("subagentStart") ? "agent_guided" : "unavailable",
13197
+ 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",
13198
+ refs: [ref(host, "SubagentStart")]
13199
+ },
13043
13200
  {
13044
13201
  name: "native_subagents",
13045
13202
  surface: "SubagentStart/SubagentStop",
@@ -13143,7 +13300,7 @@ var parseCodexHookInput = (value) => {
13143
13300
  return { ok: false, error: "permission_mode is invalid" };
13144
13301
  if (!(value.transcript_path === null || nonEmpty2(value.transcript_path)))
13145
13302
  return { ok: false, error: "transcript_path must be a string or null" };
13146
- if (!nonEmpty2(value.cwd) || !path6.isAbsolute(value.cwd) || !existsSync3(value.cwd))
13303
+ if (!nonEmpty2(value.cwd) || !path7.isAbsolute(value.cwd) || !existsSync4(value.cwd))
13147
13304
  return { ok: false, error: "cwd must be an existing absolute path" };
13148
13305
  try {
13149
13306
  if (!statSync(value.cwd).isDirectory())
@@ -13252,7 +13409,7 @@ var handleCodexHook = (raw) => {
13252
13409
  if (input.hook_event_name === "PreToolUse") {
13253
13410
  if (["bash", "unified-exec"].includes(String(input.tool_name).toLowerCase())) {
13254
13411
  const command = record2(input.tool_input) ? input.tool_input.command : undefined;
13255
- const route = typeof command === "string" ? shellRouteIntent(command) : null;
13412
+ const route = typeof command === "string" ? shouldDenyShellRoute(input.cwd, command) : null;
13256
13413
  if (route)
13257
13414
  return denied("PreToolUse", `direct branch or PR creation bypasses the Workit route; ${route.guidance}`);
13258
13415
  }