@brainervirus/workit-codex 1.0.8 → 1.0.10

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,6 +7573,7 @@ var decisionSchema = object({
7573
7573
  scope: scopeSchema,
7574
7574
  presented: text,
7575
7575
  approvedContent: text,
7576
+ displayed: text.optional(),
7576
7577
  contentRefs: array(refSchema)
7577
7578
  }).strict(),
7578
7579
  digest,
@@ -7611,7 +7612,7 @@ var workerReportSchema = object({
7611
7612
  }).strict();
7612
7613
  var workerSchema = object({
7613
7614
  assignment: assignmentSchema,
7614
- state: _enum(["assigned", "running", "cancelling", "stopped", "unknown"]),
7615
+ state: _enum(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
7615
7616
  session: refSchema.nullable(),
7616
7617
  report: workerReportSchema.nullable()
7617
7618
  }).strict();
@@ -7651,6 +7652,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
7651
7652
  path: ["decisionId"]
7652
7653
  });
7653
7654
  });
7655
+ var runtimeSchema = object({
7656
+ createdWith: text.nullable(),
7657
+ updatedWith: text
7658
+ }).strict();
7654
7659
  var taskRecordSchema = object({
7655
7660
  schemaVersion: literal(1),
7656
7661
  id,
@@ -7664,6 +7669,8 @@ var taskRecordSchema = object({
7664
7669
  status: _enum(["active", "paused", "closed"]),
7665
7670
  closure: closureSchema.nullable(),
7666
7671
  progress: progressSchema,
7672
+ pauseReason: text.nullable().optional(),
7673
+ runtime: runtimeSchema.optional(),
7667
7674
  assessments: array(entrySchema(assessmentSchema)),
7668
7675
  policy: policySchema.nullable(),
7669
7676
  policyChanges: array(policyChangeSchema),
@@ -7679,6 +7686,7 @@ var workspaceRecordSchema = object({
7679
7686
  id,
7680
7687
  revision,
7681
7688
  root: nonEmpty,
7689
+ runtime: runtimeSchema.optional(),
7682
7690
  writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
7683
7691
  }).strict();
7684
7692
  var capabilitySchema = object({
@@ -7706,6 +7714,9 @@ var taskSummarySchema = object({
7706
7714
  id,
7707
7715
  revision,
7708
7716
  workspaceRevision: revision,
7717
+ createdAt: utc,
7718
+ updatedAt: utc,
7719
+ runtime: runtimeSchema.nullable(),
7709
7720
  objective: text,
7710
7721
  status: _enum(["active", "paused", "closed"]),
7711
7722
  closure: closureSchema.nullable(),
@@ -7776,7 +7787,7 @@ var taskOperations = {
7776
7787
  expectedWorkspaceRevision: revision.optional(),
7777
7788
  outcome: outcomeSchema,
7778
7789
  summary: text,
7779
- decisionIds: array(id)
7790
+ decisionIds: array(id).optional()
7780
7791
  })
7781
7792
  };
7782
7793
  var policyOperations = {
@@ -7868,14 +7879,15 @@ var writerOperations = {
7868
7879
  ...taskId,
7869
7880
  expectedRevision: revision.optional(),
7870
7881
  expectedWorkspaceRevision: revision.optional(),
7871
- workerId: nullableId.optional()
7882
+ workerId: nullableId.optional(),
7883
+ reason: text.optional()
7872
7884
  }),
7873
7885
  release: operation({
7874
7886
  action: literal("release"),
7875
7887
  ...taskId,
7876
7888
  expectedRevision: revision.optional(),
7877
7889
  expectedWorkspaceRevision: revision.optional(),
7878
- reason: text
7890
+ reason: text.optional()
7879
7891
  })
7880
7892
  };
7881
7893
  var stateOperations = {
@@ -8026,7 +8038,21 @@ function requirementId(input) {
8026
8038
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
8027
8039
  import * as fs4 from "node:fs";
8028
8040
  import { hostname } from "node:os";
8029
- import path3 from "node:path";
8041
+ import path4 from "node:path";
8042
+
8043
+ // packages/workit-core/src/core/package-root.ts
8044
+ import { existsSync } from "node:fs";
8045
+ import path from "node:path";
8046
+ import { fileURLToPath } from "node:url";
8047
+ var packageRoot = () => {
8048
+ let dir = path.dirname(fileURLToPath(import.meta.url));
8049
+ while (true) {
8050
+ const parent = path.dirname(dir);
8051
+ if (existsSync(path.join(dir, "package.json")) || parent === dir)
8052
+ return dir;
8053
+ dir = parent;
8054
+ }
8055
+ };
8030
8056
 
8031
8057
  // node_modules/@openclaw/fs-safe/dist/errors.js
8032
8058
  var OPERATIONAL_CODES = new Set([
@@ -8077,7 +8103,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
8077
8103
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
8078
8104
  import { randomBytes } from "node:crypto";
8079
8105
  import fsSync from "node:fs";
8080
- import path from "node:path";
8106
+ import path2 from "node:path";
8081
8107
 
8082
8108
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
8083
8109
  import fs from "node:fs";
@@ -8167,13 +8193,13 @@ ${ownershipToken}
8167
8193
  return { raw, ownershipToken };
8168
8194
  }
8169
8195
  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)) {
8196
+ const resolved = path2.resolve(lockPath);
8197
+ const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
8198
+ const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
8199
+ if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
8174
8200
  throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
8175
8201
  }
8176
- return relative.split(path.sep).join(path.posix.sep);
8202
+ return relative.split(path2.sep).join(path2.posix.sep);
8177
8203
  }
8178
8204
  function parseSidecarLockPayload(raw, parser) {
8179
8205
  if (parser) {
@@ -8320,7 +8346,7 @@ function getFsSafeLockConfig() {
8320
8346
 
8321
8347
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
8322
8348
  import fs3 from "node:fs";
8323
- import path2 from "node:path";
8349
+ import path3 from "node:path";
8324
8350
 
8325
8351
  // node_modules/@openclaw/fs-safe/dist/timing.js
8326
8352
  function sleepSync(ms) {
@@ -8411,26 +8437,26 @@ function canonicalLockParentSync(parent) {
8411
8437
  return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
8412
8438
  }
8413
8439
  function normalizeTargetPath(targetPath) {
8414
- const resolved = path2.resolve(targetPath);
8415
- fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
8440
+ const resolved = path3.resolve(targetPath);
8441
+ fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
8416
8442
  try {
8417
- return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
8443
+ return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
8418
8444
  } catch {
8419
8445
  return resolved;
8420
8446
  }
8421
8447
  }
8422
8448
  function boundedLockPath(lockPath, lockRoot) {
8423
- const resolved = path2.resolve(lockPath);
8449
+ const resolved = path3.resolve(lockPath);
8424
8450
  if (!lockRoot)
8425
8451
  return resolved;
8426
8452
  relativeSidecarLockPath(lockRoot, resolved);
8427
- const parent = path2.dirname(resolved);
8453
+ const parent = path3.dirname(resolved);
8428
8454
  const parentReal = canonicalLockParentSync(parent);
8429
- const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
8430
- if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
8455
+ const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
8456
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
8431
8457
  throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
8432
8458
  }
8433
- return path2.join(parentReal, path2.basename(resolved));
8459
+ return path3.join(parentReal, path3.basename(resolved));
8434
8460
  }
8435
8461
  function defaultShouldReclaim(snapshot, staleMs, nowMs) {
8436
8462
  const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
@@ -8643,6 +8669,33 @@ var metadataLockSchema = object({
8643
8669
  nonce: string2().min(1)
8644
8670
  }).strict();
8645
8671
  var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
8672
+ var cachedRuntimeVersion = null;
8673
+ var runtimeVersion = () => {
8674
+ if (cachedRuntimeVersion === null) {
8675
+ try {
8676
+ const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
8677
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
8678
+ } catch {
8679
+ cachedRuntimeVersion = "0.0.0";
8680
+ }
8681
+ }
8682
+ return cachedRuntimeVersion;
8683
+ };
8684
+ var stampNew = (record) => ({
8685
+ createdWith: record.runtime?.createdWith ?? null,
8686
+ updatedWith: runtimeVersion()
8687
+ });
8688
+ var isNewerVersion = (candidate, current) => {
8689
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
8690
+ const [next, now] = [parts(candidate), parts(current)];
8691
+ for (let index = 0;index < 3; index += 1) {
8692
+ const left = next[index] ?? 0;
8693
+ const right = now[index] ?? 0;
8694
+ if (left !== right)
8695
+ return left > right;
8696
+ }
8697
+ return false;
8698
+ };
8646
8699
  var jsonBytes = (value) => `${canonicalJson(value)}
8647
8700
  `;
8648
8701
  var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
@@ -8669,12 +8722,12 @@ var sameMetadataLock = (left, right) => {
8669
8722
  class TaskStore {
8670
8723
  root;
8671
8724
  constructor(root) {
8672
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path3.resolve(root);
8725
+ this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
8673
8726
  }
8674
8727
  readTask(taskId) {
8675
8728
  if (!validId(taskId))
8676
8729
  return failure("invalid_input", "task ID is invalid", { taskId });
8677
- const result = this.readRecord(path3.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
8730
+ const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
8678
8731
  if (!result.exists)
8679
8732
  return failure("not_found", "task not found", { taskId });
8680
8733
  if (result.result.ok && result.result.data.id !== taskId)
@@ -8708,7 +8761,7 @@ class TaskStore {
8708
8761
  });
8709
8762
  const tasks = [];
8710
8763
  for (const name of names.sort()) {
8711
- const item = this.readRecord(path3.join(this.tasksDir, name), taskRecordSchema);
8764
+ const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
8712
8765
  if (!item.exists)
8713
8766
  continue;
8714
8767
  if (!item.result.ok)
@@ -8742,7 +8795,7 @@ class TaskStore {
8742
8795
  if (!current.ok)
8743
8796
  return current;
8744
8797
  if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
8745
- return failure("revision_conflict", "workspace revision does not match", {
8798
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
8746
8799
  expectedWorkspaceRevision: value.expectedWorkspaceRevision,
8747
8800
  actualWorkspaceRevision: current.data?.revision ?? null
8748
8801
  });
@@ -8754,11 +8807,17 @@ class TaskStore {
8754
8807
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
8755
8808
  if (current.data && !previousWorkspaceBytes)
8756
8809
  return failure("storage_error", "workspace snapshot disappeared during creation");
8757
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
8810
+ const workspace = current.data ? {
8811
+ ...current.data,
8812
+ revision: newRevision(),
8813
+ root: this.root,
8814
+ runtime: stampNew(current.data)
8815
+ } : {
8758
8816
  schemaVersion: SCHEMA_VERSION,
8759
8817
  id: newId(),
8760
8818
  revision: newRevision(),
8761
8819
  root: this.root,
8820
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8762
8821
  writer: null
8763
8822
  };
8764
8823
  const timestamp = value.now ?? now();
@@ -8780,6 +8839,7 @@ class TaskStore {
8780
8839
  status: "active",
8781
8840
  closure: null,
8782
8841
  progress: { summary: "", nextAction: null, blockers: [] },
8842
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8783
8843
  assessments: [],
8784
8844
  policy: null,
8785
8845
  policyChanges: [],
@@ -8810,7 +8870,7 @@ class TaskStore {
8810
8870
  if (!current.ok)
8811
8871
  return current;
8812
8872
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
8813
- return failure("revision_conflict", "workspace revision does not match", {
8873
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
8814
8874
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
8815
8875
  actualWorkspaceRevision: current.data?.revision ?? null
8816
8876
  });
@@ -8818,11 +8878,17 @@ class TaskStore {
8818
8878
  if (current.data && !previousWorkspaceBytes)
8819
8879
  return failure("storage_error", "workspace snapshot disappeared during import");
8820
8880
  const timestamp = input.now ?? now();
8821
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
8881
+ const workspace = current.data ? {
8882
+ ...current.data,
8883
+ revision: newRevision(),
8884
+ root: this.root,
8885
+ runtime: stampNew(current.data)
8886
+ } : {
8822
8887
  schemaVersion: SCHEMA_VERSION,
8823
8888
  id: input.workspaceId ?? newId(),
8824
8889
  revision: newRevision(),
8825
8890
  root: this.root,
8891
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8826
8892
  writer: null
8827
8893
  };
8828
8894
  const task = {
@@ -8830,7 +8896,11 @@ class TaskStore {
8830
8896
  workspaceId: workspace.id,
8831
8897
  revision: newRevision(),
8832
8898
  createdAt: timestamp,
8833
- updatedAt: timestamp
8899
+ updatedAt: timestamp,
8900
+ runtime: {
8901
+ createdWith: input.task.runtime?.createdWith ?? null,
8902
+ updatedWith: runtimeVersion()
8903
+ }
8834
8904
  };
8835
8905
  const validTask = taskRecordSchema.safeParse(task);
8836
8906
  if (!validTask.success)
@@ -8879,7 +8949,8 @@ class TaskStore {
8879
8949
  workspaceId: current.data.workspaceId,
8880
8950
  createdAt: current.data.createdAt,
8881
8951
  revision: context.revision,
8882
- updatedAt: context.now
8952
+ updatedAt: context.now,
8953
+ runtime: stampNew(current.data)
8883
8954
  };
8884
8955
  const valid = taskRecordSchema.safeParse(record);
8885
8956
  if (!valid.success)
@@ -8913,7 +8984,8 @@ class TaskStore {
8913
8984
  ...changed.data,
8914
8985
  id: current.data.id,
8915
8986
  root: this.root,
8916
- revision: context.revision
8987
+ revision: context.revision,
8988
+ runtime: stampNew(current.data)
8917
8989
  };
8918
8990
  const valid = workspaceRecordSchema.safeParse(record);
8919
8991
  if (!valid.success)
@@ -8956,7 +9028,8 @@ class TaskStore {
8956
9028
  ...changedWorkspace.data,
8957
9029
  id: workspace.data.id,
8958
9030
  root: this.root,
8959
- revision: workspaceContext.revision
9031
+ revision: workspaceContext.revision,
9032
+ runtime: stampNew(workspace.data)
8960
9033
  });
8961
9034
  if (!nextWorkspace.success)
8962
9035
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -8981,7 +9054,8 @@ class TaskStore {
8981
9054
  workspaceId: task.data.workspaceId,
8982
9055
  createdAt: task.data.createdAt,
8983
9056
  revision: taskContext.revision,
8984
- updatedAt: taskContext.now
9057
+ updatedAt: taskContext.now,
9058
+ runtime: stampNew(task.data)
8985
9059
  });
8986
9060
  if (!nextTask.success) {
8987
9061
  this.markUncertain(nextWorkspace.data);
@@ -9008,7 +9082,7 @@ class TaskStore {
9008
9082
  if (match)
9009
9083
  candidates.push({
9010
9084
  target: match[1],
9011
- path: path3.join(this.recoveryDir, name),
9085
+ path: path4.join(this.recoveryDir, name),
9012
9086
  digest: match[3]
9013
9087
  });
9014
9088
  }
@@ -9201,7 +9275,8 @@ class TaskStore {
9201
9275
  const value = {
9202
9276
  ...workspace,
9203
9277
  writer: { ...workspace.writer, state: "uncertain" },
9204
- revision: newRevision()
9278
+ revision: newRevision(),
9279
+ runtime: stampNew(workspace)
9205
9280
  };
9206
9281
  this.replaceSnapshot(this.workspacePath, value, workspace);
9207
9282
  }
@@ -9279,7 +9354,7 @@ class TaskStore {
9279
9354
  }
9280
9355
  fs4.renameSync(temporary, file);
9281
9356
  temporary = undefined;
9282
- this.fsyncDirectory(path3.dirname(file));
9357
+ this.fsyncDirectory(path4.dirname(file));
9283
9358
  return success(null, null, value);
9284
9359
  } catch (error) {
9285
9360
  return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
@@ -9293,9 +9368,9 @@ class TaskStore {
9293
9368
  }
9294
9369
  }
9295
9370
  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`);
9371
+ const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
9372
+ const id = target === "task" ? path4.basename(file, ".json") : "workspace";
9373
+ const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
9299
9374
  if (fs4.existsSync(destination)) {
9300
9375
  if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
9301
9376
  return;
@@ -9373,7 +9448,7 @@ class TaskStore {
9373
9448
  for (const name of fs4.readdirSync(this.tasksDir)) {
9374
9449
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
9375
9450
  return null;
9376
- const parsed = this.parseBytes(fs4.readFileSync(path3.join(this.tasksDir, name)), taskRecordSchema);
9451
+ const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
9377
9452
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
9378
9453
  return null;
9379
9454
  ids.add(parsed.data.workspaceId);
@@ -9415,7 +9490,12 @@ class TaskStore {
9415
9490
  if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
9416
9491
  return failure("unsupported_version", "unsupported snapshot schema version");
9417
9492
  const parsed = schema.safeParse(value);
9418
- return parsed.success ? success(null, null, parsed.data) : failure("recovery_required", "snapshot does not satisfy its schema");
9493
+ if (parsed.success)
9494
+ return success(null, null, parsed.data);
9495
+ const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
9496
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
9497
+ return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
9498
+ return failure("recovery_required", "snapshot does not satisfy its schema");
9419
9499
  }
9420
9500
  processStart(pid) {
9421
9501
  try {
@@ -9425,43 +9505,98 @@ class TaskStore {
9425
9505
  }
9426
9506
  }
9427
9507
  conflict(expected, actual) {
9428
- return failure("revision_conflict", "snapshot revision does not match", {
9508
+ return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
9429
9509
  expectedRevision: expected,
9430
9510
  actualRevision: actual
9431
9511
  });
9432
9512
  }
9433
9513
  taskPath(taskId) {
9434
- return path3.join(this.tasksDir, `${taskId}.json`);
9514
+ return path4.join(this.tasksDir, `${taskId}.json`);
9435
9515
  }
9436
9516
  get workitDir() {
9437
- return path3.join(this.root, ".workit");
9517
+ return path4.join(this.root, ".workit");
9438
9518
  }
9439
9519
  get tasksDir() {
9440
- return path3.join(this.workitDir, "tasks");
9520
+ return path4.join(this.workitDir, "tasks");
9441
9521
  }
9442
9522
  get recoveryDir() {
9443
- return path3.join(this.workitDir, "recovery");
9523
+ return path4.join(this.workitDir, "recovery");
9444
9524
  }
9445
9525
  get workspacePath() {
9446
- return path3.join(this.workitDir, "workspace.json");
9526
+ return path4.join(this.workitDir, "workspace.json");
9447
9527
  }
9448
9528
  get lockPath() {
9449
- return path3.join(this.workitDir, "metadata.lock");
9529
+ return path4.join(this.workitDir, "metadata.lock");
9450
9530
  }
9451
9531
  get gitignorePath() {
9452
- return path3.join(this.workitDir, ".gitignore");
9532
+ return path4.join(this.workitDir, ".gitignore");
9453
9533
  }
9454
9534
  }
9535
+ // packages/workit-core/src/core/workers.ts
9536
+ var same = (left, right) => {
9537
+ try {
9538
+ return canonicalJson(left) === canonicalJson(right);
9539
+ } catch {
9540
+ return false;
9541
+ }
9542
+ };
9543
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
9544
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
9545
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
9546
+ var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
9547
+ const code = char.charCodeAt(0);
9548
+ return code < 32 || code === 127;
9549
+ }) && !value.split(/[\\/]/).includes("..");
9550
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
9551
+ var workerIdOf = (value) => value.workerId ?? null;
9552
+ function assertProductWriteAllowed(input) {
9553
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
9554
+ return failure("invalid_input", "invalid product write path");
9555
+ if (!input.store)
9556
+ return failure("permission_denied", "write authorization requires core state");
9557
+ const task = input.store.readTask(input.task.id);
9558
+ if (!task.ok)
9559
+ return task;
9560
+ const workspace = input.store.readWorkspace();
9561
+ if (!workspace.ok)
9562
+ return workspace;
9563
+ if (!workspace.data)
9564
+ return failure("not_found", "workspace not found");
9565
+ const workspaceRecord = workspace.data;
9566
+ if (task.data.status !== "active")
9567
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
9568
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
9569
+ return failure("recovery_required", "task and workspace bindings are invalid");
9570
+ const writer = workspaceRecord.writer;
9571
+ if (!writer)
9572
+ return failure("permission_denied", "checkout has no writer owner");
9573
+ if (writer.state === "uncertain")
9574
+ return failure("recovery_required", "checkout writer requires recovery", {
9575
+ owner: writer.owner
9576
+ });
9577
+ const caller = callerValue(input.caller);
9578
+ const workerId = workerIdOf(input.caller);
9579
+ const owner = writer.owner;
9580
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
9581
+ return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
9582
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
9583
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
9584
+ return failure("permission_denied", "only implementers can own product writes");
9585
+ 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))))
9586
+ return failure("recovery_required", "worker session is not an active writer");
9587
+ return success(null, workspaceRecord.revision, owner);
9588
+ }
9589
+
9455
9590
  // packages/workit-core/src/core/task-evaluation.ts
9456
9591
  import { createHash as createHash4 } from "node:crypto";
9457
9592
  import * as fs6 from "node:fs";
9458
9593
  import { spawnSync } from "node:child_process";
9459
- import path5 from "node:path";
9594
+ import path6 from "node:path";
9460
9595
 
9461
9596
  // packages/workit-core/src/core/authority.ts
9462
9597
  import { createHash as createHash3 } from "node:crypto";
9463
9598
  import * as fs5 from "node:fs";
9464
- import path4 from "node:path";
9599
+ import path5 from "node:path";
9465
9600
  var verifiedAuthorities = new WeakMap;
9466
9601
  var trustedAuthority = (kind, provenance, expected, binding, caller) => {
9467
9602
  const token = {};
@@ -9588,20 +9723,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
9588
9723
  continue;
9589
9724
  if (!reference.digest)
9590
9725
  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");
9726
+ const target = path5.resolve(root, reference.path);
9727
+ if (target !== root && !target.startsWith(`${root}${path5.sep}`))
9728
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
9729
+ fields: [
9730
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
9731
+ ]
9732
+ });
9594
9733
  try {
9595
- const relative = path4.relative(root, target);
9734
+ const relative = path5.relative(root, target);
9596
9735
  let current = root;
9597
- for (const segment of relative.split(path4.sep)) {
9736
+ for (const segment of relative.split(path5.sep)) {
9598
9737
  if (!segment)
9599
9738
  continue;
9600
- current = path4.join(current, segment);
9739
+ current = path5.join(current, segment);
9601
9740
  const stat = fs5.lstatSync(current);
9602
9741
  const real = fs5.realpathSync(current);
9603
- if (real !== root && !real.startsWith(`${root}${path4.sep}`))
9604
- return failure("invalid_input", "document reference escapes checkout");
9742
+ if (real !== root && !real.startsWith(`${root}${path5.sep}`))
9743
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
9744
+ fields: [
9745
+ {
9746
+ path: "contentRefs.path",
9747
+ reason: `${reference.path} resolves outside the checkout`
9748
+ }
9749
+ ]
9750
+ });
9605
9751
  if (stat.isSymbolicLink())
9606
9752
  return failure("invalid_input", "document reference uses a symlink");
9607
9753
  if (current === target && !stat.isFile())
@@ -9948,14 +10094,14 @@ var verifyDecisionContent = verifyContentRefs;
9948
10094
 
9949
10095
  // packages/workit-core/src/core/task-evaluation.ts
9950
10096
  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("/") || ".";
10097
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
10098
+ var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
9953
10099
  var canonicalPath = (value) => {
9954
10100
  if (typeof value !== "string" || !value)
9955
10101
  return null;
9956
10102
  if (value.includes("\\") || value.split("/").includes(".."))
9957
10103
  return null;
9958
- const normalized = path5.posix.normalize(value);
10104
+ const normalized = path6.posix.normalize(value);
9959
10105
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
9960
10106
  return null;
9961
10107
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -10003,14 +10149,14 @@ var gitPaths = (root) => {
10003
10149
  git: /not a git repository/i.test(stderr) ? false : true
10004
10150
  };
10005
10151
  }
10006
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")) : [];
10152
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
10007
10153
  const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
10008
10154
  if (stagedDeleted.status !== 0) {
10009
10155
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
10010
10156
  if (!/not a git repository/i.test(stderr))
10011
10157
  return { paths, uncertain: true, git: true };
10012
10158
  } else if (stagedDeleted.stdout) {
10013
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")));
10159
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
10014
10160
  }
10015
10161
  return { paths: [...new Set(paths)], uncertain: false, git: true };
10016
10162
  };
@@ -10025,7 +10171,7 @@ var walk = (root, directory, output) => {
10025
10171
  for (const name of names) {
10026
10172
  if (name === ".git" || name === ".workit")
10027
10173
  continue;
10028
- const target = path5.join(directory, name);
10174
+ const target = path6.join(directory, name);
10029
10175
  let stat;
10030
10176
  try {
10031
10177
  stat = fs6.lstatSync(target);
@@ -10043,17 +10189,17 @@ var walk = (root, directory, output) => {
10043
10189
  var scopeRoots = (root, scope) => {
10044
10190
  const roots = [];
10045
10191
  for (const item of scope.paths.length ? scope.paths : ["."]) {
10046
- const target = path5.resolve(root, item);
10192
+ const target = path6.resolve(root, item);
10047
10193
  if (!inside(root, target))
10048
- return failure("invalid_input", "candidate scope escapes checkout");
10194
+ 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
10195
  let ancestor = target;
10050
10196
  while (!fs6.existsSync(ancestor) && ancestor !== root)
10051
- ancestor = path5.dirname(ancestor);
10197
+ ancestor = path6.dirname(ancestor);
10052
10198
  try {
10053
10199
  if (!inside(root, fs6.realpathSync(ancestor)))
10054
- return failure("invalid_input", "candidate scope escapes checkout");
10200
+ 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
10201
  } catch {
10056
- return failure("invalid_input", "candidate scope cannot be inspected");
10202
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
10057
10203
  }
10058
10204
  roots.push(target);
10059
10205
  }
@@ -10068,7 +10214,7 @@ function captureCandidate(root, scope, environment = []) {
10068
10214
  }
10069
10215
  const normalizedScope = canonicalScope(scope);
10070
10216
  if (!normalizedScope)
10071
- return failure("invalid_input", "candidate scope is invalid");
10217
+ 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
10218
  const roots = scopeRoots(checkout, normalizedScope);
10073
10219
  if (!roots.ok)
10074
10220
  return roots;
@@ -10100,7 +10246,7 @@ function captureCandidate(root, scope, environment = []) {
10100
10246
  for (const item of [...names].sort()) {
10101
10247
  if (!scopeMatches(item, normalizedScope))
10102
10248
  continue;
10103
- const target = path5.join(checkout, item);
10249
+ const target = path6.join(checkout, item);
10104
10250
  try {
10105
10251
  const stat = fs6.lstatSync(target);
10106
10252
  if (stat.isSymbolicLink()) {
@@ -10257,7 +10403,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10257
10403
  return true;
10258
10404
  const reviewSession = sessionFromRef(entry.data.reviewContext);
10259
10405
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
10260
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
10406
+ const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
10261
10407
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
10262
10408
  });
10263
10409
  if (passed.length) {
@@ -10353,7 +10499,17 @@ function evaluateClosure(requestedOutcome, view) {
10353
10499
  const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
10354
10500
  if (requestedOutcome !== "stopped" && blocking.length)
10355
10501
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
10356
- requirementIds: blocking.map((item) => item.requirementId)
10502
+ requirementIds: blocking.map((item) => item.requirementId),
10503
+ requirements: blocking.map((item) => {
10504
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
10505
+ return {
10506
+ requirementId: item.requirementId,
10507
+ ruleId: requirement?.ruleId ?? "unknown",
10508
+ reason: item.reason,
10509
+ satisfaction: requirement?.satisfaction ?? "",
10510
+ dependentAction: requirement?.dependentAction ?? null
10511
+ };
10512
+ })
10357
10513
  });
10358
10514
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
10359
10515
  if (requestedOutcome === "verified" && accepted.length)
@@ -10380,6 +10536,106 @@ function evaluateClosure(requestedOutcome, view) {
10380
10536
  });
10381
10537
  }
10382
10538
 
10539
+ // packages/workit-core/src/core/methods.ts
10540
+ var METHODS = {
10541
+ "workit-challenge": { dimensions: ["challenge", "decisions"] },
10542
+ "workit-behavioral-tdd": { dimensions: ["testing"] },
10543
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
10544
+ "workit-plan": { dimensions: ["artifacts", "continuity"] },
10545
+ "workit-implement": { dimensions: ["delegation"] },
10546
+ "workit-debug": { ruleIds: ["root-cause-investigation"] },
10547
+ "workit-handoff": { ruleIds: ["durable-handoff"] },
10548
+ "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
10549
+ };
10550
+ var methodMatches = (id, definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true && !(id === "workit-review" && requirement.ruleId === "self-review") || definition.ruleIds?.includes(requirement.ruleId) === true;
10551
+ var assuranceFor = (id, definition, capabilities) => {
10552
+ const keys = new Set([
10553
+ id,
10554
+ ...definition.dimensions ?? [],
10555
+ ...definition.ruleIds ?? []
10556
+ ]);
10557
+ const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
10558
+ if (id === "workit-review" && matches.length === 0)
10559
+ return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
10560
+ if (matches.some((capability) => capability.assurance === "unavailable"))
10561
+ return {
10562
+ assurance: "unavailable",
10563
+ reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
10564
+ };
10565
+ if (matches.some((capability) => capability.assurance === "enforced"))
10566
+ return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
10567
+ return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
10568
+ };
10569
+ function selectMethods(policy, capabilities) {
10570
+ return Object.entries(METHODS).flatMap(([id, definition]) => {
10571
+ const requirements = policy.requirements.filter((requirement) => methodMatches(id, definition, requirement));
10572
+ if (requirements.length === 0)
10573
+ return [];
10574
+ const capability = assuranceFor(id, definition, capabilities);
10575
+ const reasons = [
10576
+ ...new Set(requirements.map((requirement) => requirement.reason)),
10577
+ ...capability.reasons
10578
+ ];
10579
+ if (capability.assurance === "unavailable")
10580
+ reasons.unshift("required capability is unavailable");
10581
+ return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
10582
+ });
10583
+ }
10584
+ var invariantBootstrap = () => `
10585
+ Workit keeps one accountable lead and one shared task state. Inspect current task
10586
+ state before acting. An empty task.list means no session yet, not permission to
10587
+ skip Workit; for user-requested product, debug, or behavior work with no active
10588
+ or paused task, run task.start then policy.assess before other product mutations.
10589
+ Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
10590
+ to the current records; explicit values are still concurrency-checked, so never
10591
+ copy revisions between calls. Happy path: task.list, task.start {intent} with no
10592
+ revisions, policy.assess {assessment: {facts, signals, consequences,
10593
+ verification}} where facts are inferred or observed with file refs,
10594
+ writer.acquire {taskId} before product writes, evidence.record {evidence} where
10595
+ check and review kinds auto-bind the current tree and need no digests. Record
10596
+ self-review requirements with kind review and reviewContext matching your own
10597
+ host session; the lead may also record checks. Fresh-context review requires a
10598
+ session distinct from the task creator and every other evidence recorder. Record
10599
+ verification evidence last: later evidence or commits can reopen fixed findings
10600
+ for re-verification, so verify, resolve, then close with nothing in between.
10601
+ decision.record {binding with taskId/workspaceId from inspect plus presented and
10602
+ approvedContent} only through a native approval question asked receipt-shaped:
10603
+ header Workit decision: <purpose>, the same label repeated in the question
10604
+ text (some host UIs do not render headers), exactly two options approved and
10605
+ rejected; ask once, never re-ask to mint a receipt. Before asking, present the
10606
+ item in the conversation (durable artifacts: complete digest plus exact path;
10607
+ inline plans: the content) and keep the question to one short scoped sentence;
10608
+ never ask to approve something unseen. An answer with custom text is steering,
10609
+ not approval: adjust, re-present, then ask a new scoped question. After the user
10610
+ approves a spec or plan, execute the remaining tasks continuously with one
10611
+ atomic commit per task and do not ask whether to continue; stop only for a new
10612
+ product decision, a failed safety or verification gate, a conflicting edit, or
10613
+ missing authority. Record the plan's commit list once through the plan-scoped
10614
+ action approval (git.commit with plan_steps and plan_branch) so each listed
10615
+ commit executes without a new question; an unlisted message needs a fresh
10616
+ approval. task.close {outcome,
10617
+ summary, decisionIds}. A completed native subagent run stops its bound worker
10618
+ by itself; if a worker strands in cancelling with its run verifiably over,
10619
+ repeat worker.cancel {reason} to confirm the stop. Check and review evidence
10620
+ without a bound candidate goes stale; findings close only as fixed with passing
10621
+ verification, dismissed with supporting evidence, or deferred under an approved
10622
+ limitation. Use only the
10623
+ shared operations for task, policy, evidence, finding, decision, worker, writer,
10624
+ and state changes. Authority is bounded by the requested scope, current
10625
+ revision, caller/session provenance, and observed capabilities. Never claim host
10626
+ enforcement or evidence that the host cannot provide. Shell-executed writes are
10627
+ unattested agent-guided work even when a writer is held. Preserve unresolved
10628
+ requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
10629
+ always load on demand. Model-invoked: new instructions, interruptions, or
10630
+ forgotten items mid-task → load workit-steer before responding; about to open
10631
+ a PR or fresh from implementing → load workit-deslop; CI red → load
10632
+ workit-green-run; small-looking change before close or merge → load
10633
+ workit-blast-radius; ambiguous, consequential, or disputed proposal before
10634
+ recommending → load workit-challenge; spec or plan needs a flow diagram →
10635
+ load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
10636
+ act from memory of it.
10637
+ `.trim();
10638
+
10383
10639
  // packages/workit-core/src/core/task-context.ts
10384
10640
  function reconcileResume(view, observations = []) {
10385
10641
  if (observations.length > 0)
@@ -10389,7 +10645,7 @@ function reconcileResume(view, observations = []) {
10389
10645
  return candidate;
10390
10646
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
10391
10647
  const blockers = [...view.task.progress.blockers];
10392
- if (view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
10648
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
10393
10649
  blockers.push({
10394
10650
  reason: "worker state requires reconciliation",
10395
10651
  dependentAction: "resume",
@@ -10439,6 +10695,11 @@ function compactTaskContext(view) {
10439
10695
  digest: entry.data.digest,
10440
10696
  references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
10441
10697
  }));
10698
+ const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
10699
+ id: method.id,
10700
+ assurance: method.assurance,
10701
+ reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
10702
+ })) : [];
10442
10703
  const gaps = Array.from(new Set([
10443
10704
  ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
10444
10705
  ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
@@ -10448,15 +10709,18 @@ function compactTaskContext(view) {
10448
10709
  objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
10449
10710
  status: view.task.status,
10450
10711
  decisions,
10712
+ methods,
10451
10713
  gaps,
10452
10714
  nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
10453
10715
  };
10454
10716
  let encoded = JSON.stringify(context);
10455
- while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.gaps.length)) {
10717
+ while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
10456
10718
  if (context.gaps.length)
10457
10719
  context.gaps.pop();
10458
- else
10720
+ else if (context.decisions.length)
10459
10721
  context.decisions.pop();
10722
+ else
10723
+ context.methods.pop();
10460
10724
  encoded = JSON.stringify(context);
10461
10725
  }
10462
10726
  if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
@@ -10472,50 +10736,47 @@ var exportDigest = (bundle) => sha256({
10472
10736
  sourceWorkspaceId: bundle.sourceWorkspaceId,
10473
10737
  task: bundle.task
10474
10738
  });
10475
- // packages/workit-core/src/core/methods.ts
10476
- var invariantBootstrap = () => `
10477
- Workit keeps one accountable lead and one shared task state. Inspect current task
10478
- state before acting. An empty task.list means no session yet, not permission to
10479
- skip Workit; for user-requested product, debug, or behavior work with no active
10480
- or paused task, run task.start then policy.assess before other product mutations.
10481
- Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
10482
- to the current records; explicit values are still concurrency-checked, so never
10483
- copy revisions between calls. Happy path: task.list, task.start {intent} with no
10484
- revisions, policy.assess {assessment: {facts, signals, consequences,
10485
- verification}} where facts are inferred or observed with file refs,
10486
- writer.acquire {taskId} before product writes, evidence.record {evidence} where
10487
- check and review kinds auto-bind the current tree and need no digests. Record
10488
- self-review requirements with kind review and reviewContext matching your own
10489
- host session; the lead may also record checks. Fresh-context review requires a
10490
- session distinct from the task creator and every other evidence recorder. Record
10491
- verification evidence last: later evidence or commits can reopen fixed findings
10492
- for re-verification, so verify, resolve, then close with nothing in between.
10493
- decision.record {binding with taskId/workspaceId from inspect plus presented and
10494
- approvedContent} only through a native approval question asked receipt-shaped:
10495
- header Workit decision: <purpose>, the same label repeated in the question
10496
- text (some host UIs do not render headers), exactly two options approved and
10497
- rejected; ask once, never re-ask to mint a receipt. task.close {outcome,
10498
- summary, decisionIds}. A completed native subagent run stops its bound worker
10499
- by itself; if a worker strands in cancelling with its run verifiably over,
10500
- repeat worker.cancel {reason} to confirm the stop. Check and review evidence
10501
- without a bound candidate goes stale; findings close only as fixed with passing
10502
- verification, dismissed with supporting evidence, or deferred under an approved
10503
- limitation. Use only the
10504
- shared operations for task, policy, evidence, finding, decision, worker, writer,
10505
- and state changes. Authority is bounded by the requested scope, current
10506
- revision, caller/session provenance, and observed capabilities. Never claim host
10507
- enforcement or evidence that the host cannot provide. Shell-executed writes are
10508
- unattested agent-guided work even when a writer is held. Preserve unresolved
10509
- requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
10510
- always load on demand. Model-invoked: new instructions, interruptions, or
10511
- forgotten items mid-task → load workit-steer before responding; about to open
10512
- a PR or fresh from implementing → load workit-deslop; CI red → load
10513
- workit-green-run; small-looking change before close or merge → load
10514
- workit-blast-radius; ambiguous, consequential, or disputed proposal before
10515
- recommending → load workit-challenge; spec or plan needs a flow diagram →
10516
- load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
10517
- act from memory of it.
10518
- `.trim();
10739
+ // packages/workit-core/src/core/route-intent.ts
10740
+ var GUIDANCE = {
10741
+ "git.branch_setup": "use the Workit git.branch_setup action (workit_external_action in hosts, or `workit action git.branch_setup --payload '{}' --confirm --json` in the CLI)",
10742
+ "hosting.pull_request": "use the Workit hosting.pull_request action (workit_external_action in hosts, or `workit action hosting.pull_request --payload '{...}' --confirm --json` in the CLI), then drive the created PR through workit-babysit"
10743
+ };
10744
+ var unbalancedQuotes = (segment) => {
10745
+ const text = segment.replace(/\\./gu, "");
10746
+ return (text.match(/"/gu)?.length ?? 0) % 2 !== 0 || (text.match(/'/gu)?.length ?? 0) % 2 !== 0;
10747
+ };
10748
+ var segmentTokens = (segment) => {
10749
+ if (segment.includes("`") || segment.includes("$("))
10750
+ return null;
10751
+ if (unbalancedQuotes(segment))
10752
+ return null;
10753
+ const parts = segment.trim().split(/\s+/u);
10754
+ while (parts.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/u.test(parts[0]))
10755
+ parts.shift();
10756
+ return parts.length > 0 ? parts : null;
10757
+ };
10758
+ var unquote = (token) => token.replace(/^['"]|['"]$/gu, "");
10759
+ function shellRouteIntent(command) {
10760
+ if (typeof command !== "string" || command.trim() === "")
10761
+ return null;
10762
+ for (const raw of command.split(/&&|\|\||;|\n/u)) {
10763
+ const parts = segmentTokens(raw);
10764
+ if (!parts)
10765
+ continue;
10766
+ const head = unquote(parts[0]);
10767
+ const second = unquote(parts[1] ?? "");
10768
+ const third = unquote(parts[2] ?? "");
10769
+ if (head === "git" && second === "switch" && (third === "-c" || third === "--create"))
10770
+ return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
10771
+ if (head === "git" && second === "checkout" && (third === "-b" || third === "-B"))
10772
+ return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
10773
+ if (head === "gh" && second === "pr" && third === "create")
10774
+ return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
10775
+ if (head === "glab" && second === "mr" && third === "create")
10776
+ return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
10777
+ }
10778
+ return null;
10779
+ }
10519
10780
  // packages/workit-core/src/core/external-action.ts
10520
10781
  var externalActionSchema = discriminatedUnion("operation", [
10521
10782
  object({
@@ -10529,7 +10790,11 @@ var externalActionSchema = discriminatedUnion("operation", [
10529
10790
  }).strict(),
10530
10791
  object({
10531
10792
  operation: literal("git.commit"),
10532
- payload: object({ message: string2().min(1) }).strict()
10793
+ payload: object({
10794
+ message: string2().min(1).optional(),
10795
+ plan_steps: array(string2().min(1)).optional(),
10796
+ plan_branch: string2().min(1).optional()
10797
+ }).strict()
10533
10798
  }).strict(),
10534
10799
  object({
10535
10800
  operation: literal("git.push"),
@@ -10764,7 +11029,7 @@ function resolveRequirements(input) {
10764
11029
  ].join(", ")}`,
10765
11030
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
10766
11031
  before: "dependent_action",
10767
- dependentAction: "dependent-action",
11032
+ dependentAction: null,
10768
11033
  acceptanceAllowed: false
10769
11034
  }));
10770
11035
  if (signals.productChoiceOpen.value === true)
@@ -10775,7 +11040,7 @@ function resolveRequirements(input) {
10775
11040
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
10776
11041
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
10777
11042
  before: "dependent_action",
10778
- dependentAction: "dependent-action",
11043
+ dependentAction: null,
10779
11044
  acceptanceAllowed: false
10780
11045
  }));
10781
11046
  if (signals.behaviorChange.value === true)
@@ -10933,59 +11198,6 @@ function diffPolicy(previous, next, reason, now) {
10933
11198
  return parsed.data;
10934
11199
  }
10935
11200
 
10936
- // packages/workit-core/src/core/workers.ts
10937
- var same = (left, right) => {
10938
- try {
10939
- return canonicalJson(left) === canonicalJson(right);
10940
- } catch {
10941
- return false;
10942
- }
10943
- };
10944
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
10945
- var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
10946
- const code = char.charCodeAt(0);
10947
- return code < 32 || code === 127;
10948
- }) && !value.split(/[\\/]/).includes("..");
10949
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
10950
- var workerIdOf = (value) => value.workerId ?? null;
10951
- function assertProductWriteAllowed(input) {
10952
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
10953
- return failure("invalid_input", "invalid product write path");
10954
- if (!input.store)
10955
- return failure("permission_denied", "write authorization requires core state");
10956
- const task = input.store.readTask(input.task.id);
10957
- if (!task.ok)
10958
- return task;
10959
- const workspace = input.store.readWorkspace();
10960
- if (!workspace.ok)
10961
- return workspace;
10962
- if (!workspace.data)
10963
- return failure("not_found", "workspace not found");
10964
- const workspaceRecord = workspace.data;
10965
- if (task.data.status !== "active")
10966
- return failure("invalid_transition", "paused or closed tasks cannot own product writes");
10967
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
10968
- return failure("recovery_required", "task and workspace bindings are invalid");
10969
- const writer = workspaceRecord.writer;
10970
- if (!writer)
10971
- return failure("permission_denied", "checkout has no writer owner");
10972
- if (writer.state === "uncertain")
10973
- return failure("recovery_required", "checkout writer requires recovery", {
10974
- owner: writer.owner
10975
- });
10976
- const caller = callerValue(input.caller);
10977
- const workerId = workerIdOf(input.caller);
10978
- const owner = writer.owner;
10979
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
10980
- return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
10981
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
10982
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
10983
- return failure("permission_denied", "only implementers can own product writes");
10984
- 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))))
10985
- return failure("recovery_required", "worker session is not an active writer");
10986
- return success(null, workspaceRecord.revision, owner);
10987
- }
10988
-
10989
11201
  // packages/workit-core/src/core/task-engine.ts
10990
11202
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
10991
11203
  kind,
@@ -11104,10 +11316,10 @@ var applyWorkerLifecycle = (input) => {
11104
11316
  const notStarted = input.dispatch === true;
11105
11317
  if (notStarted && (input.state !== "stopped" || input.session !== null))
11106
11318
  return failure("invalid_input", "a never-dispatched worker stops with no session");
11107
- if (notStarted && (entry.data.state === "stopped" || entry.data.state === "unknown"))
11108
- return failure("invalid_transition", "worker is no longer awaiting dispatch");
11109
- if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
11319
+ if (notStarted && entry.data.session !== null)
11110
11320
  return failure("invalid_transition", "worker already has an observed session");
11321
+ if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
11322
+ return failure("invalid_transition", "worker launch was not claimed");
11111
11323
  if ((input.state === "running" || input.state === "cancelling" || input.state === "stopped" && !notStarted) && !input.session)
11112
11324
  return failure("invalid_input", "running worker observations require a session");
11113
11325
  if (entry.data.session && !sameValue(entry.data.session, input.session))
@@ -11116,15 +11328,20 @@ var applyWorkerLifecycle = (input) => {
11116
11328
  return failure("invalid_transition", "worker has not started");
11117
11329
  if (input.state === "running" && entry.data.state === "stopped")
11118
11330
  return failure("invalid_transition", "stopped worker cannot run again");
11331
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
11119
11332
  const nextEntry = {
11120
11333
  ...entry,
11121
11334
  recordedAt: input.now ?? entry.recordedAt,
11122
11335
  provenance: authority.provenance,
11123
- data: { ...entry.data, state: input.state, session: input.session }
11336
+ data: {
11337
+ ...entry.data,
11338
+ state: terminalCancel ? "cancelling" : input.state,
11339
+ session: input.session
11340
+ }
11124
11341
  };
11125
11342
  const shouldClear = input.state === "stopped" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
11126
11343
  const shouldUncertain = input.state === "unknown" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
11127
- if (entry.data.state === input.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
11344
+ if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
11128
11345
  return success(task.data.revision, workspace.data.revision, entry);
11129
11346
  const changed = input.store.mutateTaskAndWorkspace({
11130
11347
  taskId: task.data.id,
@@ -11494,7 +11711,7 @@ class WorkitCore {
11494
11711
  activeWorkerBlocker(task, workspace) {
11495
11712
  if (workspace.writer)
11496
11713
  return failure("recovery_required", "writer ownership must be released first");
11497
- if (task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
11714
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
11498
11715
  return failure("recovery_required", "worker state requires reconciliation");
11499
11716
  return null;
11500
11717
  }
@@ -11731,11 +11948,6 @@ class WorkitCore {
11731
11948
  if (helper && !helper.ok)
11732
11949
  return helper;
11733
11950
  const evidence = input.evidence;
11734
- if (helper?.ok) {
11735
- const assignment = helper.data.data.assignment;
11736
- 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))
11737
- return failure("permission_denied", "evidence is outside the worker assignment");
11738
- }
11739
11951
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
11740
11952
  return failure("invalid_input", "missing or skipped evidence requires a reason");
11741
11953
  if (evidence.kind === "review") {
@@ -11752,6 +11964,11 @@ class WorkitCore {
11752
11964
  evidence.beforeCandidateId ??= null;
11753
11965
  evidence.candidateId ??= null;
11754
11966
  }
11967
+ if (helper?.ok) {
11968
+ const assignment = helper.data.data.assignment;
11969
+ 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))
11970
+ return failure("permission_denied", "evidence is outside the worker assignment");
11971
+ }
11755
11972
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
11756
11973
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
11757
11974
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -12258,7 +12475,12 @@ class WorkitCore {
12258
12475
  return failure("invalid_transition", "worker is not awaiting dispatch");
12259
12476
  return success(mutation.revision, null, {
12260
12477
  ...current,
12261
- workers: current.workers.map((item) => item.id === input.workerId ? { ...item, recordedAt: mutation.now, provenance: attested.data } : item)
12478
+ workers: current.workers.map((item) => item.id === input.workerId ? {
12479
+ ...item,
12480
+ recordedAt: mutation.now,
12481
+ provenance: attested.data,
12482
+ data: { ...item.data, state: "dispatching" }
12483
+ } : item)
12262
12484
  });
12263
12485
  }
12264
12486
  });
@@ -12642,6 +12864,9 @@ class WorkitCore {
12642
12864
  id: task.id,
12643
12865
  revision: task.revision,
12644
12866
  workspaceRevision: workspace.data.revision,
12867
+ createdAt: task.createdAt,
12868
+ updatedAt: task.updatedAt,
12869
+ runtime: task.runtime ?? null,
12645
12870
  objective: task.intent.data.objective,
12646
12871
  status: task.status,
12647
12872
  closure: task.closure,
@@ -12727,7 +12952,7 @@ class WorkitCore {
12727
12952
  const base = reconcileResume(effectiveView);
12728
12953
  if (!base.ok)
12729
12954
  return base;
12730
- const needsReconciliation = view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
12955
+ const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
12731
12956
  const blockers = [...base.data.blockers];
12732
12957
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
12733
12958
  blockers.push({
@@ -12794,7 +13019,8 @@ class WorkitCore {
12794
13019
  ...current,
12795
13020
  status,
12796
13021
  candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
12797
- progress: status === "paused" ? { ...current.progress, summary: input.reason } : current.progress
13022
+ progress: current.progress,
13023
+ pauseReason: status === "paused" ? input.reason ?? null : null
12798
13024
  })
12799
13025
  });
12800
13026
  if (!changed.ok)
@@ -12830,7 +13056,7 @@ class WorkitCore {
12830
13056
  return workspace;
12831
13057
  if (!workspace.data)
12832
13058
  return failure("not_found", "workspace not found");
12833
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
13059
+ if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
12834
13060
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
12835
13061
  this.fillRevisions(input, task.data, workspace.data);
12836
13062
  const changed = this.store.mutateTaskAndWorkspace({
@@ -12941,6 +13167,13 @@ var codexCapabilities = (host, availability = {}) => {
12941
13167
  reason: "file writes are host-policy; PreToolUse allows write tools",
12942
13168
  refs: [ref(host, "PreToolUse")]
12943
13169
  },
13170
+ {
13171
+ name: "fresh-context-review",
13172
+ surface: "SubagentStart",
13173
+ assurance: has("subagentStart") ? "agent_guided" : "unavailable",
13174
+ 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",
13175
+ refs: [ref(host, "SubagentStart")]
13176
+ },
12944
13177
  {
12945
13178
  name: "native_subagents",
12946
13179
  surface: "SubagentStart/SubagentStop",
@@ -13044,7 +13277,7 @@ var parseCodexHookInput = (value) => {
13044
13277
  return { ok: false, error: "permission_mode is invalid" };
13045
13278
  if (!(value.transcript_path === null || nonEmpty2(value.transcript_path)))
13046
13279
  return { ok: false, error: "transcript_path must be a string or null" };
13047
- if (!nonEmpty2(value.cwd) || !path6.isAbsolute(value.cwd) || !existsSync3(value.cwd))
13280
+ if (!nonEmpty2(value.cwd) || !path7.isAbsolute(value.cwd) || !existsSync4(value.cwd))
13048
13281
  return { ok: false, error: "cwd must be an existing absolute path" };
13049
13282
  try {
13050
13283
  if (!statSync(value.cwd).isDirectory())
@@ -13151,6 +13384,12 @@ var handleCodexHook = (raw) => {
13151
13384
  return output("SessionStart", { additionalContext: sessionContext(input) });
13152
13385
  }
13153
13386
  if (input.hook_event_name === "PreToolUse") {
13387
+ if (["bash", "unified-exec"].includes(String(input.tool_name).toLowerCase())) {
13388
+ const command = record2(input.tool_input) ? input.tool_input.command : undefined;
13389
+ const route = typeof command === "string" ? shellRouteIntent(command) : null;
13390
+ if (route)
13391
+ return denied("PreToolUse", `direct branch or PR creation bypasses the Workit route; ${route.guidance}`);
13392
+ }
13154
13393
  return output("PreToolUse", { permissionDecision: "allow" });
13155
13394
  }
13156
13395
  if (input.hook_event_name === "SubagentStart")