@brainervirus/workit-mcp 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.
Files changed (3) hide show
  1. package/dist/index.js +357 -221
  2. package/package.json +2 -2
  3. package/src/server.ts +51 -8
package/dist/index.js CHANGED
@@ -12477,8 +12477,8 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
12477
12477
 
12478
12478
  // packages/workit-core/src/core.ts
12479
12479
  import { spawnSync as spawnSync2 } from "node:child_process";
12480
- import { existsSync as existsSync3, realpathSync as realpathSync5 } from "node:fs";
12481
- import path6 from "node:path";
12480
+ import { existsSync as existsSync4, realpathSync as realpathSync5 } from "node:fs";
12481
+ import path7 from "node:path";
12482
12482
 
12483
12483
  // packages/workit-core/src/core/task-contract.ts
12484
12484
  import { createHash, randomUUID } from "node:crypto";
@@ -20270,6 +20270,7 @@ var decisionSchema = object4({
20270
20270
  scope: scopeSchema,
20271
20271
  presented: text,
20272
20272
  approvedContent: text,
20273
+ displayed: text.optional(),
20273
20274
  contentRefs: array3(refSchema)
20274
20275
  }).strict(),
20275
20276
  digest,
@@ -20308,7 +20309,7 @@ var workerReportSchema = object4({
20308
20309
  }).strict();
20309
20310
  var workerSchema = object4({
20310
20311
  assignment: assignmentSchema,
20311
- state: _enum2(["assigned", "running", "cancelling", "stopped", "unknown"]),
20312
+ state: _enum2(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
20312
20313
  session: refSchema.nullable(),
20313
20314
  report: workerReportSchema.nullable()
20314
20315
  }).strict();
@@ -20348,6 +20349,10 @@ var actionProgressListSchema = array3(actionProgressSchema).check((ctx) => {
20348
20349
  path: ["decisionId"]
20349
20350
  });
20350
20351
  });
20352
+ var runtimeSchema = object4({
20353
+ createdWith: text.nullable(),
20354
+ updatedWith: text
20355
+ }).strict();
20351
20356
  var taskRecordSchema = object4({
20352
20357
  schemaVersion: literal3(1),
20353
20358
  id,
@@ -20361,6 +20366,8 @@ var taskRecordSchema = object4({
20361
20366
  status: _enum2(["active", "paused", "closed"]),
20362
20367
  closure: closureSchema.nullable(),
20363
20368
  progress: progressSchema,
20369
+ pauseReason: text.nullable().optional(),
20370
+ runtime: runtimeSchema.optional(),
20364
20371
  assessments: array3(entrySchema(assessmentSchema)),
20365
20372
  policy: policySchema.nullable(),
20366
20373
  policyChanges: array3(policyChangeSchema),
@@ -20376,6 +20383,7 @@ var workspaceRecordSchema = object4({
20376
20383
  id,
20377
20384
  revision,
20378
20385
  root: nonEmpty,
20386
+ runtime: runtimeSchema.optional(),
20379
20387
  writer: object4({ state: _enum2(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
20380
20388
  }).strict();
20381
20389
  var capabilitySchema = object4({
@@ -20403,6 +20411,9 @@ var taskSummarySchema = object4({
20403
20411
  id,
20404
20412
  revision,
20405
20413
  workspaceRevision: revision,
20414
+ createdAt: utc,
20415
+ updatedAt: utc,
20416
+ runtime: runtimeSchema.nullable(),
20406
20417
  objective: text,
20407
20418
  status: _enum2(["active", "paused", "closed"]),
20408
20419
  closure: closureSchema.nullable(),
@@ -20473,7 +20484,7 @@ var taskOperations = {
20473
20484
  expectedWorkspaceRevision: revision.optional(),
20474
20485
  outcome: outcomeSchema,
20475
20486
  summary: text,
20476
- decisionIds: array3(id)
20487
+ decisionIds: array3(id).optional()
20477
20488
  })
20478
20489
  };
20479
20490
  var policyOperations = {
@@ -20565,14 +20576,15 @@ var writerOperations = {
20565
20576
  ...taskId,
20566
20577
  expectedRevision: revision.optional(),
20567
20578
  expectedWorkspaceRevision: revision.optional(),
20568
- workerId: nullableId.optional()
20579
+ workerId: nullableId.optional(),
20580
+ reason: text.optional()
20569
20581
  }),
20570
20582
  release: operation({
20571
20583
  action: literal3("release"),
20572
20584
  ...taskId,
20573
20585
  expectedRevision: revision.optional(),
20574
20586
  expectedWorkspaceRevision: revision.optional(),
20575
- reason: text
20587
+ reason: text.optional()
20576
20588
  })
20577
20589
  };
20578
20590
  var stateOperations = {
@@ -20779,7 +20791,26 @@ function requirementId(input) {
20779
20791
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
20780
20792
  import * as fs4 from "node:fs";
20781
20793
  import { hostname as hostname2 } from "node:os";
20782
- import path3 from "node:path";
20794
+ import path4 from "node:path";
20795
+
20796
+ // packages/workit-core/src/core/package-root.ts
20797
+ import { existsSync } from "node:fs";
20798
+ import path from "node:path";
20799
+ import { fileURLToPath } from "node:url";
20800
+ var packageRoot = () => {
20801
+ let dir = path.dirname(fileURLToPath(import.meta.url));
20802
+ while (true) {
20803
+ const parent = path.dirname(dir);
20804
+ if (existsSync(path.join(dir, "package.json")) || parent === dir)
20805
+ return dir;
20806
+ dir = parent;
20807
+ }
20808
+ };
20809
+ var assetRoot = () => {
20810
+ const root = packageRoot();
20811
+ const assets = path.join(root, "assets");
20812
+ return existsSync(assets) ? assets : root;
20813
+ };
20783
20814
 
20784
20815
  // node_modules/@openclaw/fs-safe/dist/errors.js
20785
20816
  var OPERATIONAL_CODES = new Set([
@@ -20830,7 +20861,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
20830
20861
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
20831
20862
  import { randomBytes } from "node:crypto";
20832
20863
  import fsSync from "node:fs";
20833
- import path from "node:path";
20864
+ import path2 from "node:path";
20834
20865
 
20835
20866
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
20836
20867
  import fs from "node:fs";
@@ -20920,13 +20951,13 @@ ${ownershipToken}
20920
20951
  return { raw, ownershipToken };
20921
20952
  }
20922
20953
  function relativeSidecarLockPath(lockRoot, lockPath) {
20923
- const resolved = path.resolve(lockPath);
20924
- const lexicalRelative = path.relative(lockRoot.rootDir, resolved);
20925
- const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path.sep}`) && !path.isAbsolute(lexicalRelative) ? lexicalRelative : path.relative(lockRoot.rootReal, resolved);
20926
- if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
20954
+ const resolved = path2.resolve(lockPath);
20955
+ const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
20956
+ const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
20957
+ if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
20927
20958
  throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
20928
20959
  }
20929
- return relative.split(path.sep).join(path.posix.sep);
20960
+ return relative.split(path2.sep).join(path2.posix.sep);
20930
20961
  }
20931
20962
  function parseSidecarLockPayload(raw, parser) {
20932
20963
  if (parser) {
@@ -21073,7 +21104,7 @@ function getFsSafeLockConfig() {
21073
21104
 
21074
21105
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
21075
21106
  import fs3 from "node:fs";
21076
- import path2 from "node:path";
21107
+ import path3 from "node:path";
21077
21108
 
21078
21109
  // node_modules/@openclaw/fs-safe/dist/timing.js
21079
21110
  function sleepSync(ms) {
@@ -21164,26 +21195,26 @@ function canonicalLockParentSync(parent) {
21164
21195
  return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
21165
21196
  }
21166
21197
  function normalizeTargetPath(targetPath) {
21167
- const resolved = path2.resolve(targetPath);
21168
- fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
21198
+ const resolved = path3.resolve(targetPath);
21199
+ fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
21169
21200
  try {
21170
- return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
21201
+ return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
21171
21202
  } catch {
21172
21203
  return resolved;
21173
21204
  }
21174
21205
  }
21175
21206
  function boundedLockPath(lockPath, lockRoot) {
21176
- const resolved = path2.resolve(lockPath);
21207
+ const resolved = path3.resolve(lockPath);
21177
21208
  if (!lockRoot)
21178
21209
  return resolved;
21179
21210
  relativeSidecarLockPath(lockRoot, resolved);
21180
- const parent = path2.dirname(resolved);
21211
+ const parent = path3.dirname(resolved);
21181
21212
  const parentReal = canonicalLockParentSync(parent);
21182
- const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
21183
- if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
21213
+ const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
21214
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
21184
21215
  throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
21185
21216
  }
21186
- return path2.join(parentReal, path2.basename(resolved));
21217
+ return path3.join(parentReal, path3.basename(resolved));
21187
21218
  }
21188
21219
  function defaultShouldReclaim(snapshot, staleMs, nowMs) {
21189
21220
  const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
@@ -21396,6 +21427,33 @@ var metadataLockSchema = object4({
21396
21427
  nonce: string5().min(1)
21397
21428
  }).strict();
21398
21429
  var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
21430
+ var cachedRuntimeVersion = null;
21431
+ var runtimeVersion = () => {
21432
+ if (cachedRuntimeVersion === null) {
21433
+ try {
21434
+ const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
21435
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
21436
+ } catch {
21437
+ cachedRuntimeVersion = "0.0.0";
21438
+ }
21439
+ }
21440
+ return cachedRuntimeVersion;
21441
+ };
21442
+ var stampNew = (record) => ({
21443
+ createdWith: record.runtime?.createdWith ?? null,
21444
+ updatedWith: runtimeVersion()
21445
+ });
21446
+ var isNewerVersion = (candidate, current) => {
21447
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
21448
+ const [next, now] = [parts(candidate), parts(current)];
21449
+ for (let index = 0;index < 3; index += 1) {
21450
+ const left = next[index] ?? 0;
21451
+ const right = now[index] ?? 0;
21452
+ if (left !== right)
21453
+ return left > right;
21454
+ }
21455
+ return false;
21456
+ };
21399
21457
  var jsonBytes = (value) => `${canonicalJson(value)}
21400
21458
  `;
21401
21459
  var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
@@ -21422,12 +21480,12 @@ var sameMetadataLock = (left, right) => {
21422
21480
  class TaskStore {
21423
21481
  root;
21424
21482
  constructor(root) {
21425
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path3.resolve(root);
21483
+ this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
21426
21484
  }
21427
21485
  readTask(taskId) {
21428
21486
  if (!validId(taskId))
21429
21487
  return failure("invalid_input", "task ID is invalid", { taskId });
21430
- const result = this.readRecord(path3.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
21488
+ const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
21431
21489
  if (!result.exists)
21432
21490
  return failure("not_found", "task not found", { taskId });
21433
21491
  if (result.result.ok && result.result.data.id !== taskId)
@@ -21461,7 +21519,7 @@ class TaskStore {
21461
21519
  });
21462
21520
  const tasks = [];
21463
21521
  for (const name of names.sort()) {
21464
- const item = this.readRecord(path3.join(this.tasksDir, name), taskRecordSchema);
21522
+ const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
21465
21523
  if (!item.exists)
21466
21524
  continue;
21467
21525
  if (!item.result.ok)
@@ -21495,7 +21553,7 @@ class TaskStore {
21495
21553
  if (!current.ok)
21496
21554
  return current;
21497
21555
  if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
21498
- return failure("revision_conflict", "workspace revision does not match", {
21556
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
21499
21557
  expectedWorkspaceRevision: value.expectedWorkspaceRevision,
21500
21558
  actualWorkspaceRevision: current.data?.revision ?? null
21501
21559
  });
@@ -21507,11 +21565,17 @@ class TaskStore {
21507
21565
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
21508
21566
  if (current.data && !previousWorkspaceBytes)
21509
21567
  return failure("storage_error", "workspace snapshot disappeared during creation");
21510
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
21568
+ const workspace = current.data ? {
21569
+ ...current.data,
21570
+ revision: newRevision(),
21571
+ root: this.root,
21572
+ runtime: stampNew(current.data)
21573
+ } : {
21511
21574
  schemaVersion: SCHEMA_VERSION,
21512
21575
  id: newId(),
21513
21576
  revision: newRevision(),
21514
21577
  root: this.root,
21578
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
21515
21579
  writer: null
21516
21580
  };
21517
21581
  const timestamp = value.now ?? now();
@@ -21533,6 +21597,7 @@ class TaskStore {
21533
21597
  status: "active",
21534
21598
  closure: null,
21535
21599
  progress: { summary: "", nextAction: null, blockers: [] },
21600
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
21536
21601
  assessments: [],
21537
21602
  policy: null,
21538
21603
  policyChanges: [],
@@ -21563,7 +21628,7 @@ class TaskStore {
21563
21628
  if (!current.ok)
21564
21629
  return current;
21565
21630
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
21566
- return failure("revision_conflict", "workspace revision does not match", {
21631
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
21567
21632
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
21568
21633
  actualWorkspaceRevision: current.data?.revision ?? null
21569
21634
  });
@@ -21571,11 +21636,17 @@ class TaskStore {
21571
21636
  if (current.data && !previousWorkspaceBytes)
21572
21637
  return failure("storage_error", "workspace snapshot disappeared during import");
21573
21638
  const timestamp = input.now ?? now();
21574
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
21639
+ const workspace = current.data ? {
21640
+ ...current.data,
21641
+ revision: newRevision(),
21642
+ root: this.root,
21643
+ runtime: stampNew(current.data)
21644
+ } : {
21575
21645
  schemaVersion: SCHEMA_VERSION,
21576
21646
  id: input.workspaceId ?? newId(),
21577
21647
  revision: newRevision(),
21578
21648
  root: this.root,
21649
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
21579
21650
  writer: null
21580
21651
  };
21581
21652
  const task = {
@@ -21583,7 +21654,11 @@ class TaskStore {
21583
21654
  workspaceId: workspace.id,
21584
21655
  revision: newRevision(),
21585
21656
  createdAt: timestamp,
21586
- updatedAt: timestamp
21657
+ updatedAt: timestamp,
21658
+ runtime: {
21659
+ createdWith: input.task.runtime?.createdWith ?? null,
21660
+ updatedWith: runtimeVersion()
21661
+ }
21587
21662
  };
21588
21663
  const validTask = taskRecordSchema.safeParse(task);
21589
21664
  if (!validTask.success)
@@ -21632,7 +21707,8 @@ class TaskStore {
21632
21707
  workspaceId: current.data.workspaceId,
21633
21708
  createdAt: current.data.createdAt,
21634
21709
  revision: context.revision,
21635
- updatedAt: context.now
21710
+ updatedAt: context.now,
21711
+ runtime: stampNew(current.data)
21636
21712
  };
21637
21713
  const valid = taskRecordSchema.safeParse(record);
21638
21714
  if (!valid.success)
@@ -21666,7 +21742,8 @@ class TaskStore {
21666
21742
  ...changed.data,
21667
21743
  id: current.data.id,
21668
21744
  root: this.root,
21669
- revision: context.revision
21745
+ revision: context.revision,
21746
+ runtime: stampNew(current.data)
21670
21747
  };
21671
21748
  const valid = workspaceRecordSchema.safeParse(record);
21672
21749
  if (!valid.success)
@@ -21709,7 +21786,8 @@ class TaskStore {
21709
21786
  ...changedWorkspace.data,
21710
21787
  id: workspace.data.id,
21711
21788
  root: this.root,
21712
- revision: workspaceContext.revision
21789
+ revision: workspaceContext.revision,
21790
+ runtime: stampNew(workspace.data)
21713
21791
  });
21714
21792
  if (!nextWorkspace.success)
21715
21793
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -21734,7 +21812,8 @@ class TaskStore {
21734
21812
  workspaceId: task.data.workspaceId,
21735
21813
  createdAt: task.data.createdAt,
21736
21814
  revision: taskContext.revision,
21737
- updatedAt: taskContext.now
21815
+ updatedAt: taskContext.now,
21816
+ runtime: stampNew(task.data)
21738
21817
  });
21739
21818
  if (!nextTask.success) {
21740
21819
  this.markUncertain(nextWorkspace.data);
@@ -21761,7 +21840,7 @@ class TaskStore {
21761
21840
  if (match)
21762
21841
  candidates.push({
21763
21842
  target: match[1],
21764
- path: path3.join(this.recoveryDir, name),
21843
+ path: path4.join(this.recoveryDir, name),
21765
21844
  digest: match[3]
21766
21845
  });
21767
21846
  }
@@ -21954,7 +22033,8 @@ class TaskStore {
21954
22033
  const value = {
21955
22034
  ...workspace,
21956
22035
  writer: { ...workspace.writer, state: "uncertain" },
21957
- revision: newRevision()
22036
+ revision: newRevision(),
22037
+ runtime: stampNew(workspace)
21958
22038
  };
21959
22039
  this.replaceSnapshot(this.workspacePath, value, workspace);
21960
22040
  }
@@ -22032,7 +22112,7 @@ class TaskStore {
22032
22112
  }
22033
22113
  fs4.renameSync(temporary, file);
22034
22114
  temporary = undefined;
22035
- this.fsyncDirectory(path3.dirname(file));
22115
+ this.fsyncDirectory(path4.dirname(file));
22036
22116
  return success(null, null, value);
22037
22117
  } catch (error) {
22038
22118
  return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
@@ -22046,9 +22126,9 @@ class TaskStore {
22046
22126
  }
22047
22127
  }
22048
22128
  saveRecovery(file, bytes) {
22049
- const target = path3.basename(file) === "workspace.json" ? "workspace" : "task";
22050
- const id = target === "task" ? path3.basename(file, ".json") : "workspace";
22051
- const destination = path3.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
22129
+ const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
22130
+ const id = target === "task" ? path4.basename(file, ".json") : "workspace";
22131
+ const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
22052
22132
  if (fs4.existsSync(destination)) {
22053
22133
  if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
22054
22134
  return;
@@ -22126,7 +22206,7 @@ class TaskStore {
22126
22206
  for (const name of fs4.readdirSync(this.tasksDir)) {
22127
22207
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
22128
22208
  return null;
22129
- const parsed = this.parseBytes(fs4.readFileSync(path3.join(this.tasksDir, name)), taskRecordSchema);
22209
+ const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
22130
22210
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
22131
22211
  return null;
22132
22212
  ids.add(parsed.data.workspaceId);
@@ -22168,7 +22248,12 @@ class TaskStore {
22168
22248
  if (isObject3(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
22169
22249
  return failure("unsupported_version", "unsupported snapshot schema version");
22170
22250
  const parsed = schema.safeParse(value);
22171
- return parsed.success ? success(null, null, parsed.data) : failure("recovery_required", "snapshot does not satisfy its schema");
22251
+ if (parsed.success)
22252
+ return success(null, null, parsed.data);
22253
+ const writerVersion = isObject3(value) && isObject3(value.runtime) ? value.runtime.updatedWith : null;
22254
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
22255
+ return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
22256
+ return failure("recovery_required", "snapshot does not satisfy its schema");
22172
22257
  }
22173
22258
  processStart(pid) {
22174
22259
  try {
@@ -22178,43 +22263,98 @@ class TaskStore {
22178
22263
  }
22179
22264
  }
22180
22265
  conflict(expected, actual) {
22181
- return failure("revision_conflict", "snapshot revision does not match", {
22266
+ return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
22182
22267
  expectedRevision: expected,
22183
22268
  actualRevision: actual
22184
22269
  });
22185
22270
  }
22186
22271
  taskPath(taskId) {
22187
- return path3.join(this.tasksDir, `${taskId}.json`);
22272
+ return path4.join(this.tasksDir, `${taskId}.json`);
22188
22273
  }
22189
22274
  get workitDir() {
22190
- return path3.join(this.root, ".workit");
22275
+ return path4.join(this.root, ".workit");
22191
22276
  }
22192
22277
  get tasksDir() {
22193
- return path3.join(this.workitDir, "tasks");
22278
+ return path4.join(this.workitDir, "tasks");
22194
22279
  }
22195
22280
  get recoveryDir() {
22196
- return path3.join(this.workitDir, "recovery");
22281
+ return path4.join(this.workitDir, "recovery");
22197
22282
  }
22198
22283
  get workspacePath() {
22199
- return path3.join(this.workitDir, "workspace.json");
22284
+ return path4.join(this.workitDir, "workspace.json");
22200
22285
  }
22201
22286
  get lockPath() {
22202
- return path3.join(this.workitDir, "metadata.lock");
22287
+ return path4.join(this.workitDir, "metadata.lock");
22203
22288
  }
22204
22289
  get gitignorePath() {
22205
- return path3.join(this.workitDir, ".gitignore");
22290
+ return path4.join(this.workitDir, ".gitignore");
22206
22291
  }
22207
22292
  }
22293
+ // packages/workit-core/src/core/workers.ts
22294
+ var same = (left, right) => {
22295
+ try {
22296
+ return canonicalJson(left) === canonicalJson(right);
22297
+ } catch {
22298
+ return false;
22299
+ }
22300
+ };
22301
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
22302
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
22303
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
22304
+ var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
22305
+ const code = char.charCodeAt(0);
22306
+ return code < 32 || code === 127;
22307
+ }) && !value.split(/[\\/]/).includes("..");
22308
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
22309
+ var workerIdOf = (value) => value.workerId ?? null;
22310
+ function assertProductWriteAllowed(input) {
22311
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
22312
+ return failure("invalid_input", "invalid product write path");
22313
+ if (!input.store)
22314
+ return failure("permission_denied", "write authorization requires core state");
22315
+ const task = input.store.readTask(input.task.id);
22316
+ if (!task.ok)
22317
+ return task;
22318
+ const workspace = input.store.readWorkspace();
22319
+ if (!workspace.ok)
22320
+ return workspace;
22321
+ if (!workspace.data)
22322
+ return failure("not_found", "workspace not found");
22323
+ const workspaceRecord = workspace.data;
22324
+ if (task.data.status !== "active")
22325
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
22326
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
22327
+ return failure("recovery_required", "task and workspace bindings are invalid");
22328
+ const writer = workspaceRecord.writer;
22329
+ if (!writer)
22330
+ return failure("permission_denied", "checkout has no writer owner");
22331
+ if (writer.state === "uncertain")
22332
+ return failure("recovery_required", "checkout writer requires recovery", {
22333
+ owner: writer.owner
22334
+ });
22335
+ const caller = callerValue(input.caller);
22336
+ const workerId = workerIdOf(input.caller);
22337
+ const owner = writer.owner;
22338
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
22339
+ return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
22340
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
22341
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
22342
+ return failure("permission_denied", "only implementers can own product writes");
22343
+ 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))))
22344
+ return failure("recovery_required", "worker session is not an active writer");
22345
+ return success(null, workspaceRecord.revision, owner);
22346
+ }
22347
+
22208
22348
  // packages/workit-core/src/core/task-evaluation.ts
22209
22349
  import { createHash as createHash4 } from "node:crypto";
22210
22350
  import * as fs6 from "node:fs";
22211
22351
  import { spawnSync } from "node:child_process";
22212
- import path5 from "node:path";
22352
+ import path6 from "node:path";
22213
22353
 
22214
22354
  // packages/workit-core/src/core/authority.ts
22215
22355
  import { createHash as createHash3 } from "node:crypto";
22216
22356
  import * as fs5 from "node:fs";
22217
- import path4 from "node:path";
22357
+ import path5 from "node:path";
22218
22358
  var verifiedAuthorities = new WeakMap;
22219
22359
  var trustedAuthority = (kind, provenance, expected, binding, caller) => {
22220
22360
  const token = {};
@@ -22341,20 +22481,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
22341
22481
  continue;
22342
22482
  if (!reference.digest)
22343
22483
  return failure("invalid_input", "document references require a byte digest");
22344
- const target = path4.resolve(root, reference.path);
22345
- if (target !== root && !target.startsWith(`${root}${path4.sep}`))
22346
- return failure("invalid_input", "document reference escapes checkout");
22484
+ const target = path5.resolve(root, reference.path);
22485
+ if (target !== root && !target.startsWith(`${root}${path5.sep}`))
22486
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
22487
+ fields: [
22488
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
22489
+ ]
22490
+ });
22347
22491
  try {
22348
- const relative = path4.relative(root, target);
22492
+ const relative = path5.relative(root, target);
22349
22493
  let current = root;
22350
- for (const segment of relative.split(path4.sep)) {
22494
+ for (const segment of relative.split(path5.sep)) {
22351
22495
  if (!segment)
22352
22496
  continue;
22353
- current = path4.join(current, segment);
22497
+ current = path5.join(current, segment);
22354
22498
  const stat = fs5.lstatSync(current);
22355
22499
  const real = fs5.realpathSync(current);
22356
- if (real !== root && !real.startsWith(`${root}${path4.sep}`))
22357
- return failure("invalid_input", "document reference escapes checkout");
22500
+ if (real !== root && !real.startsWith(`${root}${path5.sep}`))
22501
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
22502
+ fields: [
22503
+ {
22504
+ path: "contentRefs.path",
22505
+ reason: `${reference.path} resolves outside the checkout`
22506
+ }
22507
+ ]
22508
+ });
22358
22509
  if (stat.isSymbolicLink())
22359
22510
  return failure("invalid_input", "document reference uses a symlink");
22360
22511
  if (current === target && !stat.isFile())
@@ -22701,14 +22852,14 @@ var verifyDecisionContent = verifyContentRefs;
22701
22852
 
22702
22853
  // packages/workit-core/src/core/task-evaluation.ts
22703
22854
  var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
22704
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path5.sep}`);
22705
- var relative = (root, candidate) => path5.relative(root, candidate).split(path5.sep).join("/") || ".";
22855
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
22856
+ var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
22706
22857
  var canonicalPath = (value) => {
22707
22858
  if (typeof value !== "string" || !value)
22708
22859
  return null;
22709
22860
  if (value.includes("\\") || value.split("/").includes(".."))
22710
22861
  return null;
22711
- const normalized = path5.posix.normalize(value);
22862
+ const normalized = path6.posix.normalize(value);
22712
22863
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
22713
22864
  return null;
22714
22865
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -22756,14 +22907,14 @@ var gitPaths = (root) => {
22756
22907
  git: /not a git repository/i.test(stderr) ? false : true
22757
22908
  };
22758
22909
  }
22759
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")) : [];
22910
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
22760
22911
  const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
22761
22912
  if (stagedDeleted.status !== 0) {
22762
22913
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
22763
22914
  if (!/not a git repository/i.test(stderr))
22764
22915
  return { paths, uncertain: true, git: true };
22765
22916
  } else if (stagedDeleted.stdout) {
22766
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")));
22917
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
22767
22918
  }
22768
22919
  return { paths: [...new Set(paths)], uncertain: false, git: true };
22769
22920
  };
@@ -22778,7 +22929,7 @@ var walk = (root, directory, output) => {
22778
22929
  for (const name of names) {
22779
22930
  if (name === ".git" || name === ".workit")
22780
22931
  continue;
22781
- const target = path5.join(directory, name);
22932
+ const target = path6.join(directory, name);
22782
22933
  let stat;
22783
22934
  try {
22784
22935
  stat = fs6.lstatSync(target);
@@ -22796,17 +22947,17 @@ var walk = (root, directory, output) => {
22796
22947
  var scopeRoots = (root, scope) => {
22797
22948
  const roots = [];
22798
22949
  for (const item of scope.paths.length ? scope.paths : ["."]) {
22799
- const target = path5.resolve(root, item);
22950
+ const target = path6.resolve(root, item);
22800
22951
  if (!inside(root, target))
22801
- return failure("invalid_input", "candidate scope escapes checkout");
22952
+ 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` }] });
22802
22953
  let ancestor = target;
22803
22954
  while (!fs6.existsSync(ancestor) && ancestor !== root)
22804
- ancestor = path5.dirname(ancestor);
22955
+ ancestor = path6.dirname(ancestor);
22805
22956
  try {
22806
22957
  if (!inside(root, fs6.realpathSync(ancestor)))
22807
- return failure("invalid_input", "candidate scope escapes checkout");
22958
+ 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` }] });
22808
22959
  } catch {
22809
- return failure("invalid_input", "candidate scope cannot be inspected");
22960
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
22810
22961
  }
22811
22962
  roots.push(target);
22812
22963
  }
@@ -22821,7 +22972,7 @@ function captureCandidate(root, scope, environment = []) {
22821
22972
  }
22822
22973
  const normalizedScope = canonicalScope(scope);
22823
22974
  if (!normalizedScope)
22824
- return failure("invalid_input", "candidate scope is invalid");
22975
+ 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" }] });
22825
22976
  const roots = scopeRoots(checkout, normalizedScope);
22826
22977
  if (!roots.ok)
22827
22978
  return roots;
@@ -22853,7 +23004,7 @@ function captureCandidate(root, scope, environment = []) {
22853
23004
  for (const item of [...names].sort()) {
22854
23005
  if (!scopeMatches(item, normalizedScope))
22855
23006
  continue;
22856
- const target = path5.join(checkout, item);
23007
+ const target = path6.join(checkout, item);
22857
23008
  try {
22858
23009
  const stat = fs6.lstatSync(target);
22859
23010
  if (stat.isSymbolicLink()) {
@@ -23010,7 +23161,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
23010
23161
  return true;
23011
23162
  const reviewSession = sessionFromRef(entry.data.reviewContext);
23012
23163
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
23013
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
23164
+ const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
23014
23165
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
23015
23166
  });
23016
23167
  if (passed.length) {
@@ -23106,7 +23257,17 @@ function evaluateClosure(requestedOutcome, view) {
23106
23257
  const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
23107
23258
  if (requestedOutcome !== "stopped" && blocking.length)
23108
23259
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
23109
- requirementIds: blocking.map((item) => item.requirementId)
23260
+ requirementIds: blocking.map((item) => item.requirementId),
23261
+ requirements: blocking.map((item) => {
23262
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
23263
+ return {
23264
+ requirementId: item.requirementId,
23265
+ ruleId: requirement?.ruleId ?? "unknown",
23266
+ reason: item.reason,
23267
+ satisfaction: requirement?.satisfaction ?? "",
23268
+ dependentAction: requirement?.dependentAction ?? null
23269
+ };
23270
+ })
23110
23271
  });
23111
23272
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
23112
23273
  if (requestedOutcome === "verified" && accepted.length)
@@ -23142,7 +23303,7 @@ function reconcileResume(view, observations = []) {
23142
23303
  return candidate;
23143
23304
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
23144
23305
  const blockers = [...view.task.progress.blockers];
23145
- if (view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
23306
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
23146
23307
  blockers.push({
23147
23308
  reason: "worker state requires reconciliation",
23148
23309
  dependentAction: "resume",
@@ -23175,7 +23336,11 @@ var externalActionSchema = discriminatedUnion2("operation", [
23175
23336
  }).strict(),
23176
23337
  object4({
23177
23338
  operation: literal3("git.commit"),
23178
- payload: object4({ message: string5().min(1) }).strict()
23339
+ payload: object4({
23340
+ message: string5().min(1).optional(),
23341
+ plan_steps: array3(string5().min(1)).optional(),
23342
+ plan_branch: string5().min(1).optional()
23343
+ }).strict()
23179
23344
  }).strict(),
23180
23345
  object4({
23181
23346
  operation: literal3("git.push"),
@@ -23410,7 +23575,7 @@ function resolveRequirements(input) {
23410
23575
  ].join(", ")}`,
23411
23576
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
23412
23577
  before: "dependent_action",
23413
- dependentAction: "dependent-action",
23578
+ dependentAction: null,
23414
23579
  acceptanceAllowed: false
23415
23580
  }));
23416
23581
  if (signals.productChoiceOpen.value === true)
@@ -23421,7 +23586,7 @@ function resolveRequirements(input) {
23421
23586
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
23422
23587
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
23423
23588
  before: "dependent_action",
23424
- dependentAction: "dependent-action",
23589
+ dependentAction: null,
23425
23590
  acceptanceAllowed: false
23426
23591
  }));
23427
23592
  if (signals.behaviorChange.value === true)
@@ -23579,59 +23744,6 @@ function diffPolicy(previous, next, reason, now) {
23579
23744
  return parsed.data;
23580
23745
  }
23581
23746
 
23582
- // packages/workit-core/src/core/workers.ts
23583
- var same = (left, right) => {
23584
- try {
23585
- return canonicalJson(left) === canonicalJson(right);
23586
- } catch {
23587
- return false;
23588
- }
23589
- };
23590
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
23591
- var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
23592
- const code = char.charCodeAt(0);
23593
- return code < 32 || code === 127;
23594
- }) && !value.split(/[\\/]/).includes("..");
23595
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
23596
- var workerIdOf = (value) => value.workerId ?? null;
23597
- function assertProductWriteAllowed(input) {
23598
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
23599
- return failure("invalid_input", "invalid product write path");
23600
- if (!input.store)
23601
- return failure("permission_denied", "write authorization requires core state");
23602
- const task = input.store.readTask(input.task.id);
23603
- if (!task.ok)
23604
- return task;
23605
- const workspace = input.store.readWorkspace();
23606
- if (!workspace.ok)
23607
- return workspace;
23608
- if (!workspace.data)
23609
- return failure("not_found", "workspace not found");
23610
- const workspaceRecord = workspace.data;
23611
- if (task.data.status !== "active")
23612
- return failure("invalid_transition", "paused or closed tasks cannot own product writes");
23613
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
23614
- return failure("recovery_required", "task and workspace bindings are invalid");
23615
- const writer = workspaceRecord.writer;
23616
- if (!writer)
23617
- return failure("permission_denied", "checkout has no writer owner");
23618
- if (writer.state === "uncertain")
23619
- return failure("recovery_required", "checkout writer requires recovery", {
23620
- owner: writer.owner
23621
- });
23622
- const caller = callerValue(input.caller);
23623
- const workerId = workerIdOf(input.caller);
23624
- const owner = writer.owner;
23625
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
23626
- return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
23627
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
23628
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
23629
- return failure("permission_denied", "only implementers can own product writes");
23630
- 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))))
23631
- return failure("recovery_required", "worker session is not an active writer");
23632
- return success(null, workspaceRecord.revision, owner);
23633
- }
23634
-
23635
23747
  // packages/workit-core/src/core/task-engine.ts
23636
23748
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
23637
23749
  kind,
@@ -23750,10 +23862,10 @@ var applyWorkerLifecycle = (input) => {
23750
23862
  const notStarted = input.dispatch === true;
23751
23863
  if (notStarted && (input.state !== "stopped" || input.session !== null))
23752
23864
  return failure("invalid_input", "a never-dispatched worker stops with no session");
23753
- if (notStarted && (entry.data.state === "stopped" || entry.data.state === "unknown"))
23754
- return failure("invalid_transition", "worker is no longer awaiting dispatch");
23755
- if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
23865
+ if (notStarted && entry.data.session !== null)
23756
23866
  return failure("invalid_transition", "worker already has an observed session");
23867
+ if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
23868
+ return failure("invalid_transition", "worker launch was not claimed");
23757
23869
  if ((input.state === "running" || input.state === "cancelling" || input.state === "stopped" && !notStarted) && !input.session)
23758
23870
  return failure("invalid_input", "running worker observations require a session");
23759
23871
  if (entry.data.session && !sameValue(entry.data.session, input.session))
@@ -23762,15 +23874,20 @@ var applyWorkerLifecycle = (input) => {
23762
23874
  return failure("invalid_transition", "worker has not started");
23763
23875
  if (input.state === "running" && entry.data.state === "stopped")
23764
23876
  return failure("invalid_transition", "stopped worker cannot run again");
23877
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
23765
23878
  const nextEntry = {
23766
23879
  ...entry,
23767
23880
  recordedAt: input.now ?? entry.recordedAt,
23768
23881
  provenance: authority.provenance,
23769
- data: { ...entry.data, state: input.state, session: input.session }
23882
+ data: {
23883
+ ...entry.data,
23884
+ state: terminalCancel ? "cancelling" : input.state,
23885
+ session: input.session
23886
+ }
23770
23887
  };
23771
23888
  const shouldClear = input.state === "stopped" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
23772
23889
  const shouldUncertain = input.state === "unknown" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
23773
- if (entry.data.state === input.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
23890
+ if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
23774
23891
  return success(task.data.revision, workspace.data.revision, entry);
23775
23892
  const changed = input.store.mutateTaskAndWorkspace({
23776
23893
  taskId: task.data.id,
@@ -24140,7 +24257,7 @@ class WorkitCore {
24140
24257
  activeWorkerBlocker(task, workspace) {
24141
24258
  if (workspace.writer)
24142
24259
  return failure("recovery_required", "writer ownership must be released first");
24143
- if (task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
24260
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
24144
24261
  return failure("recovery_required", "worker state requires reconciliation");
24145
24262
  return null;
24146
24263
  }
@@ -24377,11 +24494,6 @@ class WorkitCore {
24377
24494
  if (helper && !helper.ok)
24378
24495
  return helper;
24379
24496
  const evidence = input.evidence;
24380
- if (helper?.ok) {
24381
- const assignment = helper.data.data.assignment;
24382
- 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))
24383
- return failure("permission_denied", "evidence is outside the worker assignment");
24384
- }
24385
24497
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
24386
24498
  return failure("invalid_input", "missing or skipped evidence requires a reason");
24387
24499
  if (evidence.kind === "review") {
@@ -24398,6 +24510,11 @@ class WorkitCore {
24398
24510
  evidence.beforeCandidateId ??= null;
24399
24511
  evidence.candidateId ??= null;
24400
24512
  }
24513
+ if (helper?.ok) {
24514
+ const assignment = helper.data.data.assignment;
24515
+ 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))
24516
+ return failure("permission_denied", "evidence is outside the worker assignment");
24517
+ }
24401
24518
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
24402
24519
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
24403
24520
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -24904,7 +25021,12 @@ class WorkitCore {
24904
25021
  return failure("invalid_transition", "worker is not awaiting dispatch");
24905
25022
  return success(mutation.revision, null, {
24906
25023
  ...current,
24907
- workers: current.workers.map((item) => item.id === input.workerId ? { ...item, recordedAt: mutation.now, provenance: attested.data } : item)
25024
+ workers: current.workers.map((item) => item.id === input.workerId ? {
25025
+ ...item,
25026
+ recordedAt: mutation.now,
25027
+ provenance: attested.data,
25028
+ data: { ...item.data, state: "dispatching" }
25029
+ } : item)
24908
25030
  });
24909
25031
  }
24910
25032
  });
@@ -25288,6 +25410,9 @@ class WorkitCore {
25288
25410
  id: task.id,
25289
25411
  revision: task.revision,
25290
25412
  workspaceRevision: workspace.data.revision,
25413
+ createdAt: task.createdAt,
25414
+ updatedAt: task.updatedAt,
25415
+ runtime: task.runtime ?? null,
25291
25416
  objective: task.intent.data.objective,
25292
25417
  status: task.status,
25293
25418
  closure: task.closure,
@@ -25373,7 +25498,7 @@ class WorkitCore {
25373
25498
  const base = reconcileResume(effectiveView);
25374
25499
  if (!base.ok)
25375
25500
  return base;
25376
- const needsReconciliation = view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
25501
+ const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
25377
25502
  const blockers = [...base.data.blockers];
25378
25503
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
25379
25504
  blockers.push({
@@ -25440,7 +25565,8 @@ class WorkitCore {
25440
25565
  ...current,
25441
25566
  status,
25442
25567
  candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
25443
- progress: status === "paused" ? { ...current.progress, summary: input.reason } : current.progress
25568
+ progress: current.progress,
25569
+ pauseReason: status === "paused" ? input.reason ?? null : null
25444
25570
  })
25445
25571
  });
25446
25572
  if (!changed.ok)
@@ -25476,7 +25602,7 @@ class WorkitCore {
25476
25602
  return workspace;
25477
25603
  if (!workspace.data)
25478
25604
  return failure("not_found", "workspace not found");
25479
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
25605
+ if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
25480
25606
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
25481
25607
  this.fillRevisions(input, task.data, workspace.data);
25482
25608
  const changed = this.store.mutateTaskAndWorkspace({
@@ -25587,15 +25713,15 @@ function resolveGitRevision(root, value) {
25587
25713
  }
25588
25714
  function resolveInside(root, candidate) {
25589
25715
  const base = realpathSync5(root);
25590
- const target = path6.resolve(base, candidate);
25591
- if (target !== base && !target.startsWith(base + path6.sep)) {
25716
+ const target = path7.resolve(base, candidate);
25717
+ if (target !== base && !target.startsWith(base + path7.sep)) {
25592
25718
  throw new Error("path must stay inside repository root");
25593
25719
  }
25594
25720
  let ancestor = target;
25595
- while (!existsSync3(ancestor))
25596
- ancestor = path6.dirname(ancestor);
25721
+ while (!existsSync4(ancestor))
25722
+ ancestor = path7.dirname(ancestor);
25597
25723
  const canonicalAncestor = realpathSync5(ancestor);
25598
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path6.sep)) {
25724
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
25599
25725
  throw new Error("path must stay inside repository root");
25600
25726
  }
25601
25727
  return target;
@@ -25780,14 +25906,14 @@ var gitContext = (workspaceRoot, paths = []) => {
25780
25906
  import {
25781
25907
  copyFileSync,
25782
25908
  cpSync,
25783
- existsSync as existsSync4,
25909
+ existsSync as existsSync5,
25784
25910
  mkdirSync as mkdirSync2,
25785
25911
  readFileSync as readFileSync4,
25786
25912
  readdirSync as readdirSync3,
25787
25913
  writeFileSync as writeFileSync2
25788
25914
  } from "node:fs";
25789
25915
  import os2 from "node:os";
25790
- import path7 from "node:path";
25916
+ import path8 from "node:path";
25791
25917
  var diagnosticLogger;
25792
25918
  var PRESETS = {
25793
25919
  gitflow: {
@@ -25808,7 +25934,7 @@ var mergePreset = (preset, input = {}, current = {
25808
25934
  protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
25809
25935
  };
25810
25936
  };
25811
- var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path7.join(process.env.XDG_CONFIG_HOME || path7.join(os2.homedir(), ".config"), "workit");
25937
+ var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path8.join(process.env.XDG_CONFIG_HOME || path8.join(os2.homedir(), ".config"), "workit");
25812
25938
  var migratedDir = null;
25813
25939
  var migrationFailed = false;
25814
25940
  var ensureConfigDir = (dir = resolveConfigDir()) => {
@@ -25818,12 +25944,12 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
25818
25944
  migratedDir = dir;
25819
25945
  return dir;
25820
25946
  }
25821
- const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os2.homedir(), ".config"), "workflow-toolkit");
25822
- if (!existsSync4(legacy)) {
25947
+ const legacy = path8.join(process.env.XDG_CONFIG_HOME || path8.join(os2.homedir(), ".config"), "workflow-toolkit");
25948
+ if (!existsSync5(legacy)) {
25823
25949
  migratedDir = dir;
25824
25950
  return dir;
25825
25951
  }
25826
- if (!migrationFailed && existsSync4(dir)) {
25952
+ if (!migrationFailed && existsSync5(dir)) {
25827
25953
  migratedDir = dir;
25828
25954
  return dir;
25829
25955
  }
@@ -25831,9 +25957,9 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
25831
25957
  mkdirSync2(dir, { recursive: true });
25832
25958
  diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
25833
25959
  for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
25834
- const src = path7.join(legacy, entry.name);
25835
- const dest = path7.join(dir, entry.name);
25836
- if (existsSync4(dest))
25960
+ const src = path8.join(legacy, entry.name);
25961
+ const dest = path8.join(dir, entry.name);
25962
+ if (existsSync5(dest))
25837
25963
  continue;
25838
25964
  try {
25839
25965
  if (entry.isDirectory())
@@ -25906,7 +26032,7 @@ var parseConfigResult = (raw, file) => {
25906
26032
  };
25907
26033
  };
25908
26034
  var readConfigTyped = (dir) => {
25909
- const file = path7.join(dir ?? configDir(), "config.json");
26035
+ const file = path8.join(dir ?? configDir(), "config.json");
25910
26036
  return parseConfigResult(readSafe(file), file);
25911
26037
  };
25912
26038
  var readConfig = () => {
@@ -25942,9 +26068,9 @@ var COMMIT_PRESETS = [
25942
26068
 
25943
26069
  // packages/workit-core/src/core/workspaces.ts
25944
26070
  import { readFileSync as readFileSync5, realpathSync as realpathSync6 } from "node:fs";
25945
- import path8 from "node:path";
26071
+ import path9 from "node:path";
25946
26072
  var readWorkspacesResult = (dir = configDir()) => {
25947
- const file = path8.join(dir, "workspaces.json");
26073
+ const file = path9.join(dir, "workspaces.json");
25948
26074
  let raw;
25949
26075
  try {
25950
26076
  raw = readFileSync5(file, "utf8");
@@ -26042,10 +26168,10 @@ var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
26042
26168
 
26043
26169
  // packages/workit-core/src/core/vcs-config.ts
26044
26170
  import fs7 from "node:fs";
26045
- import path9 from "node:path";
26171
+ import path10 from "node:path";
26046
26172
  import { spawnSync as spawnSync3 } from "node:child_process";
26047
26173
  var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
26048
- var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
26174
+ var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path10.join(configDir(), "vcs.json");
26049
26175
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
26050
26176
  var remoteProvider = (cwd) => {
26051
26177
  const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
@@ -26128,8 +26254,8 @@ function vcsConfig(mode, cwd) {
26128
26254
  }
26129
26255
  const prov = cfg[provider] ?? {};
26130
26256
  const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
26131
- const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path9.join(configDir(), `${provider}.token`));
26132
- const tokenPath = path9.resolve(tokenFile);
26257
+ const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path10.join(configDir(), `${provider}.token`));
26258
+ const tokenPath = path10.resolve(tokenFile);
26133
26259
  let tokenOk = false;
26134
26260
  if (fs7.existsSync(tokenPath)) {
26135
26261
  const token = fs7.readFileSync(tokenPath, "utf8").trim();
@@ -26138,7 +26264,7 @@ function vcsConfig(mode, cwd) {
26138
26264
  }
26139
26265
  const out = {
26140
26266
  ok: true,
26141
- configPath: path9.resolve(cfgPath),
26267
+ configPath: path10.resolve(cfgPath),
26142
26268
  provider,
26143
26269
  defaultTargetBranch: defaultTarget,
26144
26270
  pr: cfg.pr ?? {},
@@ -26231,18 +26357,18 @@ var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig()
26231
26357
 
26232
26358
  // packages/workit-core/src/core/pr-create.ts
26233
26359
  import fs8 from "node:fs";
26234
- import path10 from "node:path";
26360
+ import path11 from "node:path";
26235
26361
  function parseGhIssue(value) {
26236
26362
  const m = /issues\/(\d+)/.exec(value);
26237
26363
  return m ? m[1] : String(value).trim().replace(/^#/, "");
26238
26364
  }
26239
26365
  function whichOnPath(tool) {
26240
26366
  const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
26241
- for (const dir of (process.env.PATH ?? "").split(path10.delimiter)) {
26367
+ for (const dir of (process.env.PATH ?? "").split(path11.delimiter)) {
26242
26368
  if (!dir)
26243
26369
  continue;
26244
26370
  for (const name of names) {
26245
- const candidate = path10.join(dir, name);
26371
+ const candidate = path11.join(dir, name);
26246
26372
  try {
26247
26373
  fs8.accessSync(candidate, fs8.constants.X_OK);
26248
26374
  return candidate;
@@ -26255,8 +26381,8 @@ var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "gla
26255
26381
 
26256
26382
  // packages/workit-core/src/core/repo-context.ts
26257
26383
  import { spawnSync as spawnSync4 } from "node:child_process";
26258
- import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "node:fs";
26259
- import path11 from "node:path";
26384
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "node:fs";
26385
+ import path12 from "node:path";
26260
26386
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
26261
26387
  try {
26262
26388
  gitRevisionParts(value);
@@ -26390,7 +26516,7 @@ var findPrTemplate = (cwd) => {
26390
26516
  ]) {
26391
26517
  const parts = rel.split("/");
26392
26518
  const base = parts[parts.length - 1];
26393
- const dir = parts.length > 1 ? path11.join(cwd, ...parts.slice(0, -1)) : cwd;
26519
+ const dir = parts.length > 1 ? path12.join(cwd, ...parts.slice(0, -1)) : cwd;
26394
26520
  const actual = probeCaseInsensitive(dir, base);
26395
26521
  if (actual)
26396
26522
  return [...parts.slice(0, -1), actual].join("/");
@@ -26421,8 +26547,8 @@ var diffStatForRange = (cwd, range) => {
26421
26547
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
26422
26548
  };
26423
26549
  var packageScripts = (cwd, keys) => {
26424
- const pkgPath = path11.join(cwd, "package.json");
26425
- if (!existsSync5(pkgPath))
26550
+ const pkgPath = path12.join(cwd, "package.json");
26551
+ if (!existsSync6(pkgPath))
26426
26552
  return [];
26427
26553
  try {
26428
26554
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
@@ -26447,9 +26573,9 @@ var documentationFiles = (cwd) => {
26447
26573
  if (entry.isDirectory()) {
26448
26574
  if (excluded.has(entry.name))
26449
26575
  continue;
26450
- walk(path11.join(dir, entry.name), depth + 1);
26576
+ walk(path12.join(dir, entry.name), depth + 1);
26451
26577
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
26452
- matches.push("./" + path11.relative(cwd, path11.join(dir, entry.name)).replaceAll("\\", "/"));
26578
+ matches.push("./" + path12.relative(cwd, path12.join(dir, entry.name)).replaceAll("\\", "/"));
26453
26579
  }
26454
26580
  }
26455
26581
  };
@@ -26457,7 +26583,7 @@ var documentationFiles = (cwd) => {
26457
26583
  return matches.sort().slice(0, 200);
26458
26584
  };
26459
26585
  var readTrimmed = (file, maxLines) => {
26460
- if (!existsSync5(file))
26586
+ if (!existsSync6(file))
26461
26587
  return "";
26462
26588
  const lines = readFileSync6(file, "utf8").split(`
26463
26589
  `);
@@ -26465,7 +26591,7 @@ var readTrimmed = (file, maxLines) => {
26465
26591
  `);
26466
26592
  };
26467
26593
  function prReadyContext(root, range) {
26468
- const cwd = path11.resolve(repoRoot(root));
26594
+ const cwd = path12.resolve(repoRoot(root));
26469
26595
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26470
26596
  return invalidRange(cwd);
26471
26597
  let stdout = "";
@@ -26523,7 +26649,7 @@ ${status.stderr}` : "");
26523
26649
  stdout += `template_path: ${template}
26524
26650
 
26525
26651
  `;
26526
- stdout += readTrimmed(path11.join(cwd, template), 220);
26652
+ stdout += readTrimmed(path12.join(cwd, template), 220);
26527
26653
  } else {
26528
26654
  stdout += `template_path: none
26529
26655
 
@@ -26533,7 +26659,7 @@ ${status.stderr}` : "");
26533
26659
  stdout += `
26534
26660
  `;
26535
26661
  stdout += printSection("Recent Validation Signals");
26536
- if (existsSync5(path11.join(cwd, "package.json"))) {
26662
+ if (existsSync6(path12.join(cwd, "package.json"))) {
26537
26663
  stdout += `package.json detected. Common scripts:
26538
26664
  `;
26539
26665
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -26542,7 +26668,7 @@ ${status.stderr}` : "");
26542
26668
  `) + `
26543
26669
  `;
26544
26670
  }
26545
- if (existsSync5(path11.join(cwd, "Cargo.toml")) || existsSync5(path11.join(cwd, "src-tauri/Cargo.toml"))) {
26671
+ if (existsSync6(path12.join(cwd, "Cargo.toml")) || existsSync6(path12.join(cwd, "src-tauri/Cargo.toml"))) {
26546
26672
  stdout += `Rust project detected.
26547
26673
  `;
26548
26674
  }
@@ -26575,7 +26701,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
26575
26701
  - Apply with the native workit_changelog_apply tool only (not hand-edits under Unreleased).
26576
26702
  - If Unreleased already has duplicate category headings, normalize_only first.`;
26577
26703
  function changelogContext(root, range) {
26578
- const cwd = path11.resolve(repoRoot(root));
26704
+ const cwd = path12.resolve(repoRoot(root));
26579
26705
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26580
26706
  return invalidRange(cwd);
26581
26707
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -26593,8 +26719,8 @@ function changelogContext(root, range) {
26593
26719
  stdout += CHANGELOG_RULES + `
26594
26720
  `;
26595
26721
  stdout += printSection("Existing CHANGELOG.md");
26596
- const changelogPath = path11.join(cwd, "CHANGELOG.md");
26597
- if (existsSync5(changelogPath)) {
26722
+ const changelogPath = path12.join(cwd, "CHANGELOG.md");
26723
+ if (existsSync6(changelogPath)) {
26598
26724
  stdout += readTrimmed(changelogPath, 260) + `
26599
26725
  `;
26600
26726
  } else {
@@ -26613,7 +26739,7 @@ function changelogContext(root, range) {
26613
26739
  return { stdout, stderr: "", exitCode: 0, cwd };
26614
26740
  }
26615
26741
  function docsRefreshContext(root, range) {
26616
- const cwd = path11.resolve(repoRoot(root));
26742
+ const cwd = path12.resolve(repoRoot(root));
26617
26743
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26618
26744
  return invalidRange(cwd);
26619
26745
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -26635,8 +26761,8 @@ function docsRefreshContext(root, range) {
26635
26761
  `) + `
26636
26762
  `;
26637
26763
  stdout += printSection("README Preview");
26638
- const readme = path11.join(cwd, "README.md");
26639
- if (existsSync5(readme)) {
26764
+ const readme = path12.join(cwd, "README.md");
26765
+ if (existsSync6(readme)) {
26640
26766
  stdout += readTrimmed(readme, 220) + `
26641
26767
  `;
26642
26768
  } else {
@@ -26644,8 +26770,8 @@ function docsRefreshContext(root, range) {
26644
26770
  `;
26645
26771
  }
26646
26772
  stdout += printSection("Package Scripts");
26647
- const pkgPath = path11.join(cwd, "package.json");
26648
- if (existsSync5(pkgPath)) {
26773
+ const pkgPath = path12.join(cwd, "package.json");
26774
+ if (existsSync6(pkgPath)) {
26649
26775
  try {
26650
26776
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
26651
26777
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
@@ -26658,7 +26784,7 @@ function docsRefreshContext(root, range) {
26658
26784
  return { stdout, stderr: "", exitCode: 0, cwd };
26659
26785
  }
26660
26786
  function releaseNotesContext(root, rangeOrTag) {
26661
- const cwd = path11.resolve(repoRoot(root));
26787
+ const cwd = path12.resolve(repoRoot(root));
26662
26788
  if (!rangeOrTag) {
26663
26789
  return { stdout: "", stderr: `ERROR: release tag or range required
26664
26790
  `, exitCode: 1, cwd };
@@ -26696,7 +26822,7 @@ function releaseNotesContext(root, rangeOrTag) {
26696
26822
  `;
26697
26823
  stdout += printSection("Existing Release Files");
26698
26824
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
26699
- if (existsSync5(path11.join(cwd, rel)))
26825
+ if (existsSync6(path12.join(cwd, rel)))
26700
26826
  stdout += rel + `
26701
26827
  `;
26702
26828
  }
@@ -26887,25 +27013,6 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
26887
27013
  import fs10 from "node:fs";
26888
27014
  import path13 from "node:path";
26889
27015
 
26890
- // packages/workit-core/src/core/package-root.ts
26891
- import { existsSync as existsSync6 } from "node:fs";
26892
- import path12 from "node:path";
26893
- import { fileURLToPath } from "node:url";
26894
- var packageRoot = () => {
26895
- let dir = path12.dirname(fileURLToPath(import.meta.url));
26896
- while (true) {
26897
- const parent = path12.dirname(dir);
26898
- if (existsSync6(path12.join(dir, "package.json")) || parent === dir)
26899
- return dir;
26900
- dir = parent;
26901
- }
26902
- };
26903
- var assetRoot = () => {
26904
- const root = packageRoot();
26905
- const assets = path12.join(root, "assets");
26906
- return existsSync6(assets) ? assets : root;
26907
- };
26908
-
26909
27016
  // packages/workit-core/src/core/templates.ts
26910
27017
  var repoRoot2 = assetRoot();
26911
27018
 
@@ -27409,6 +27516,32 @@ var toolInputSchema = (family) => {
27409
27516
  const schema = boundedOperationJsonSchema(family);
27410
27517
  return { type: "object", ...schema };
27411
27518
  };
27519
+ var schemaBranches = (schema) => Array.isArray(schema.oneOf) ? schema.oneOf : Array.isArray(schema.anyOf) ? schema.anyOf : [schema];
27520
+ var readOnlyBranches = (family) => schemaBranches(boundedOperationJsonSchema(family)).filter((branch) => {
27521
+ const action = branch.properties?.action?.const;
27522
+ return typeof action === "string" && READ_ONLY_ACTIONS.has(action);
27523
+ });
27524
+ var advertisedToolSchemas = (attested) => {
27525
+ if (attested)
27526
+ return OPERATION_FAMILIES.map((family) => ({
27527
+ name: `workit_${family}`,
27528
+ description: operationDescription(family),
27529
+ inputSchema: toolInputSchema(family)
27530
+ }));
27531
+ return OPERATION_FAMILIES.flatMap((family) => {
27532
+ const branches = readOnlyBranches(family);
27533
+ if (!branches.length)
27534
+ return [];
27535
+ const schema = boundedOperationJsonSchema(family);
27536
+ return [
27537
+ {
27538
+ name: `workit_${family}`,
27539
+ description: `${operationDescription(family)} Unattested MCP callers can only run read-only actions; mutating operations require an attested host session.`,
27540
+ inputSchema: { type: "object", ...schema, oneOf: branches }
27541
+ }
27542
+ ];
27543
+ });
27544
+ };
27412
27545
  var sanitizeFailure = (result, workspaceRoot) => {
27413
27546
  if (result.ok)
27414
27547
  return result;
@@ -27463,13 +27596,16 @@ function createMcpServer(host, contextProvider) {
27463
27596
  ].filter((file) => existsSync7(file)));
27464
27597
  let staleWarned = false;
27465
27598
  const server = new Server({ name: "workit", version: VERSION }, { capabilities: { tools: {}, resources: {} } });
27466
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
27467
- tools: OPERATION_FAMILIES.map((family) => ({
27468
- name: `workit_${family}`,
27469
- description: operationDescription(family),
27470
- inputSchema: toolInputSchema(family)
27471
- }))
27472
- }));
27599
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
27600
+ let attested = false;
27601
+ try {
27602
+ const context = await contextProvider.current();
27603
+ attested = context.callerAttested === true;
27604
+ } catch {
27605
+ attested = false;
27606
+ }
27607
+ return { tools: advertisedToolSchemas(attested) };
27608
+ });
27473
27609
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
27474
27610
  resources: CONTEXT_KINDS.map((kind) => ({
27475
27611
  uri: `workit://context/${kind}`,
@@ -27584,7 +27720,7 @@ function createMcpServer(host, contextProvider) {
27584
27720
  const parsed = parseOperation(family, request.params.arguments);
27585
27721
  if (!parsed.ok)
27586
27722
  return resultForClient(parsed, workspaceRoot);
27587
- if (context.callerAttested === false && requiresCallerIdentity(parsed.data))
27723
+ if (context.callerAttested !== true && requiresCallerIdentity(parsed.data))
27588
27724
  return resultForClient({
27589
27725
  ok: false,
27590
27726
  schemaVersion: 1,