@brainervirus/workit-mcp 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +359 -222
  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,10 +20270,12 @@ var decisionSchema = object4({
20270
20270
  scope: scopeSchema,
20271
20271
  presented: text,
20272
20272
  approvedContent: text,
20273
- contentRefs: array3(refSchema)
20273
+ displayed: text.optional(),
20274
+ contentRefs: array3(refSchema),
20275
+ statedChoice: object4({ ref: text, text }).strict().optional()
20274
20276
  }).strict(),
20275
20277
  digest,
20276
- response: _enum2(["approved", "rejected"]),
20278
+ response: _enum2(["approved", "rejected", "stated"]),
20277
20279
  requirementIds: array3(digest),
20278
20280
  revoked: object4({ at: utc, reason: text }).strict().nullable(),
20279
20281
  consumption: object4({
@@ -20348,6 +20350,10 @@ var actionProgressListSchema = array3(actionProgressSchema).check((ctx) => {
20348
20350
  path: ["decisionId"]
20349
20351
  });
20350
20352
  });
20353
+ var runtimeSchema = object4({
20354
+ createdWith: text.nullable(),
20355
+ updatedWith: text
20356
+ }).strict();
20351
20357
  var taskRecordSchema = object4({
20352
20358
  schemaVersion: literal3(1),
20353
20359
  id,
@@ -20361,6 +20367,8 @@ var taskRecordSchema = object4({
20361
20367
  status: _enum2(["active", "paused", "closed"]),
20362
20368
  closure: closureSchema.nullable(),
20363
20369
  progress: progressSchema,
20370
+ pauseReason: text.nullable().optional(),
20371
+ runtime: runtimeSchema.optional(),
20364
20372
  assessments: array3(entrySchema(assessmentSchema)),
20365
20373
  policy: policySchema.nullable(),
20366
20374
  policyChanges: array3(policyChangeSchema),
@@ -20376,6 +20384,7 @@ var workspaceRecordSchema = object4({
20376
20384
  id,
20377
20385
  revision,
20378
20386
  root: nonEmpty,
20387
+ runtime: runtimeSchema.optional(),
20379
20388
  writer: object4({ state: _enum2(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
20380
20389
  }).strict();
20381
20390
  var capabilitySchema = object4({
@@ -20403,6 +20412,9 @@ var taskSummarySchema = object4({
20403
20412
  id,
20404
20413
  revision,
20405
20414
  workspaceRevision: revision,
20415
+ createdAt: utc,
20416
+ updatedAt: utc,
20417
+ runtime: runtimeSchema.nullable(),
20406
20418
  objective: text,
20407
20419
  status: _enum2(["active", "paused", "closed"]),
20408
20420
  closure: closureSchema.nullable(),
@@ -20473,7 +20485,7 @@ var taskOperations = {
20473
20485
  expectedWorkspaceRevision: revision.optional(),
20474
20486
  outcome: outcomeSchema,
20475
20487
  summary: text,
20476
- decisionIds: array3(id)
20488
+ decisionIds: array3(id).optional()
20477
20489
  })
20478
20490
  };
20479
20491
  var policyOperations = {
@@ -20565,14 +20577,15 @@ var writerOperations = {
20565
20577
  ...taskId,
20566
20578
  expectedRevision: revision.optional(),
20567
20579
  expectedWorkspaceRevision: revision.optional(),
20568
- workerId: nullableId.optional()
20580
+ workerId: nullableId.optional(),
20581
+ reason: text.optional()
20569
20582
  }),
20570
20583
  release: operation({
20571
20584
  action: literal3("release"),
20572
20585
  ...taskId,
20573
20586
  expectedRevision: revision.optional(),
20574
20587
  expectedWorkspaceRevision: revision.optional(),
20575
- reason: text
20588
+ reason: text.optional()
20576
20589
  })
20577
20590
  };
20578
20591
  var stateOperations = {
@@ -20779,7 +20792,26 @@ function requirementId(input) {
20779
20792
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
20780
20793
  import * as fs4 from "node:fs";
20781
20794
  import { hostname as hostname2 } from "node:os";
20782
- import path3 from "node:path";
20795
+ import path4 from "node:path";
20796
+
20797
+ // packages/workit-core/src/core/package-root.ts
20798
+ import { existsSync } from "node:fs";
20799
+ import path from "node:path";
20800
+ import { fileURLToPath } from "node:url";
20801
+ var packageRoot = () => {
20802
+ let dir = path.dirname(fileURLToPath(import.meta.url));
20803
+ while (true) {
20804
+ const parent = path.dirname(dir);
20805
+ if (existsSync(path.join(dir, "package.json")) || parent === dir)
20806
+ return dir;
20807
+ dir = parent;
20808
+ }
20809
+ };
20810
+ var assetRoot = () => {
20811
+ const root = packageRoot();
20812
+ const assets = path.join(root, "assets");
20813
+ return existsSync(assets) ? assets : root;
20814
+ };
20783
20815
 
20784
20816
  // node_modules/@openclaw/fs-safe/dist/errors.js
20785
20817
  var OPERATIONAL_CODES = new Set([
@@ -20830,7 +20862,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
20830
20862
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
20831
20863
  import { randomBytes } from "node:crypto";
20832
20864
  import fsSync from "node:fs";
20833
- import path from "node:path";
20865
+ import path2 from "node:path";
20834
20866
 
20835
20867
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
20836
20868
  import fs from "node:fs";
@@ -20920,13 +20952,13 @@ ${ownershipToken}
20920
20952
  return { raw, ownershipToken };
20921
20953
  }
20922
20954
  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)) {
20955
+ const resolved = path2.resolve(lockPath);
20956
+ const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
20957
+ const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
20958
+ if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
20927
20959
  throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
20928
20960
  }
20929
- return relative.split(path.sep).join(path.posix.sep);
20961
+ return relative.split(path2.sep).join(path2.posix.sep);
20930
20962
  }
20931
20963
  function parseSidecarLockPayload(raw, parser) {
20932
20964
  if (parser) {
@@ -21073,7 +21105,7 @@ function getFsSafeLockConfig() {
21073
21105
 
21074
21106
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
21075
21107
  import fs3 from "node:fs";
21076
- import path2 from "node:path";
21108
+ import path3 from "node:path";
21077
21109
 
21078
21110
  // node_modules/@openclaw/fs-safe/dist/timing.js
21079
21111
  function sleepSync(ms) {
@@ -21164,26 +21196,26 @@ function canonicalLockParentSync(parent) {
21164
21196
  return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
21165
21197
  }
21166
21198
  function normalizeTargetPath(targetPath) {
21167
- const resolved = path2.resolve(targetPath);
21168
- fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
21199
+ const resolved = path3.resolve(targetPath);
21200
+ fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
21169
21201
  try {
21170
- return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
21202
+ return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
21171
21203
  } catch {
21172
21204
  return resolved;
21173
21205
  }
21174
21206
  }
21175
21207
  function boundedLockPath(lockPath, lockRoot) {
21176
- const resolved = path2.resolve(lockPath);
21208
+ const resolved = path3.resolve(lockPath);
21177
21209
  if (!lockRoot)
21178
21210
  return resolved;
21179
21211
  relativeSidecarLockPath(lockRoot, resolved);
21180
- const parent = path2.dirname(resolved);
21212
+ const parent = path3.dirname(resolved);
21181
21213
  const parentReal = canonicalLockParentSync(parent);
21182
- const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
21183
- if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
21214
+ const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
21215
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
21184
21216
  throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
21185
21217
  }
21186
- return path2.join(parentReal, path2.basename(resolved));
21218
+ return path3.join(parentReal, path3.basename(resolved));
21187
21219
  }
21188
21220
  function defaultShouldReclaim(snapshot, staleMs, nowMs) {
21189
21221
  const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
@@ -21396,6 +21428,33 @@ var metadataLockSchema = object4({
21396
21428
  nonce: string5().min(1)
21397
21429
  }).strict();
21398
21430
  var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
21431
+ var cachedRuntimeVersion = null;
21432
+ var runtimeVersion = () => {
21433
+ if (cachedRuntimeVersion === null) {
21434
+ try {
21435
+ const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
21436
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
21437
+ } catch {
21438
+ cachedRuntimeVersion = "0.0.0";
21439
+ }
21440
+ }
21441
+ return cachedRuntimeVersion;
21442
+ };
21443
+ var stampNew = (record) => ({
21444
+ createdWith: record.runtime?.createdWith ?? null,
21445
+ updatedWith: runtimeVersion()
21446
+ });
21447
+ var isNewerVersion = (candidate, current) => {
21448
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
21449
+ const [next, now] = [parts(candidate), parts(current)];
21450
+ for (let index = 0;index < 3; index += 1) {
21451
+ const left = next[index] ?? 0;
21452
+ const right = now[index] ?? 0;
21453
+ if (left !== right)
21454
+ return left > right;
21455
+ }
21456
+ return false;
21457
+ };
21399
21458
  var jsonBytes = (value) => `${canonicalJson(value)}
21400
21459
  `;
21401
21460
  var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
@@ -21422,12 +21481,12 @@ var sameMetadataLock = (left, right) => {
21422
21481
  class TaskStore {
21423
21482
  root;
21424
21483
  constructor(root) {
21425
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path3.resolve(root);
21484
+ this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
21426
21485
  }
21427
21486
  readTask(taskId) {
21428
21487
  if (!validId(taskId))
21429
21488
  return failure("invalid_input", "task ID is invalid", { taskId });
21430
- const result = this.readRecord(path3.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
21489
+ const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
21431
21490
  if (!result.exists)
21432
21491
  return failure("not_found", "task not found", { taskId });
21433
21492
  if (result.result.ok && result.result.data.id !== taskId)
@@ -21461,7 +21520,7 @@ class TaskStore {
21461
21520
  });
21462
21521
  const tasks = [];
21463
21522
  for (const name of names.sort()) {
21464
- const item = this.readRecord(path3.join(this.tasksDir, name), taskRecordSchema);
21523
+ const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
21465
21524
  if (!item.exists)
21466
21525
  continue;
21467
21526
  if (!item.result.ok)
@@ -21495,7 +21554,7 @@ class TaskStore {
21495
21554
  if (!current.ok)
21496
21555
  return current;
21497
21556
  if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
21498
- return failure("revision_conflict", "workspace revision does not match", {
21557
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
21499
21558
  expectedWorkspaceRevision: value.expectedWorkspaceRevision,
21500
21559
  actualWorkspaceRevision: current.data?.revision ?? null
21501
21560
  });
@@ -21507,11 +21566,17 @@ class TaskStore {
21507
21566
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
21508
21567
  if (current.data && !previousWorkspaceBytes)
21509
21568
  return failure("storage_error", "workspace snapshot disappeared during creation");
21510
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
21569
+ const workspace = current.data ? {
21570
+ ...current.data,
21571
+ revision: newRevision(),
21572
+ root: this.root,
21573
+ runtime: stampNew(current.data)
21574
+ } : {
21511
21575
  schemaVersion: SCHEMA_VERSION,
21512
21576
  id: newId(),
21513
21577
  revision: newRevision(),
21514
21578
  root: this.root,
21579
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
21515
21580
  writer: null
21516
21581
  };
21517
21582
  const timestamp = value.now ?? now();
@@ -21533,6 +21598,7 @@ class TaskStore {
21533
21598
  status: "active",
21534
21599
  closure: null,
21535
21600
  progress: { summary: "", nextAction: null, blockers: [] },
21601
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
21536
21602
  assessments: [],
21537
21603
  policy: null,
21538
21604
  policyChanges: [],
@@ -21563,7 +21629,7 @@ class TaskStore {
21563
21629
  if (!current.ok)
21564
21630
  return current;
21565
21631
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
21566
- return failure("revision_conflict", "workspace revision does not match", {
21632
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
21567
21633
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
21568
21634
  actualWorkspaceRevision: current.data?.revision ?? null
21569
21635
  });
@@ -21571,11 +21637,17 @@ class TaskStore {
21571
21637
  if (current.data && !previousWorkspaceBytes)
21572
21638
  return failure("storage_error", "workspace snapshot disappeared during import");
21573
21639
  const timestamp = input.now ?? now();
21574
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
21640
+ const workspace = current.data ? {
21641
+ ...current.data,
21642
+ revision: newRevision(),
21643
+ root: this.root,
21644
+ runtime: stampNew(current.data)
21645
+ } : {
21575
21646
  schemaVersion: SCHEMA_VERSION,
21576
21647
  id: input.workspaceId ?? newId(),
21577
21648
  revision: newRevision(),
21578
21649
  root: this.root,
21650
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
21579
21651
  writer: null
21580
21652
  };
21581
21653
  const task = {
@@ -21583,7 +21655,11 @@ class TaskStore {
21583
21655
  workspaceId: workspace.id,
21584
21656
  revision: newRevision(),
21585
21657
  createdAt: timestamp,
21586
- updatedAt: timestamp
21658
+ updatedAt: timestamp,
21659
+ runtime: {
21660
+ createdWith: input.task.runtime?.createdWith ?? null,
21661
+ updatedWith: runtimeVersion()
21662
+ }
21587
21663
  };
21588
21664
  const validTask = taskRecordSchema.safeParse(task);
21589
21665
  if (!validTask.success)
@@ -21632,7 +21708,8 @@ class TaskStore {
21632
21708
  workspaceId: current.data.workspaceId,
21633
21709
  createdAt: current.data.createdAt,
21634
21710
  revision: context.revision,
21635
- updatedAt: context.now
21711
+ updatedAt: context.now,
21712
+ runtime: stampNew(current.data)
21636
21713
  };
21637
21714
  const valid = taskRecordSchema.safeParse(record);
21638
21715
  if (!valid.success)
@@ -21666,7 +21743,8 @@ class TaskStore {
21666
21743
  ...changed.data,
21667
21744
  id: current.data.id,
21668
21745
  root: this.root,
21669
- revision: context.revision
21746
+ revision: context.revision,
21747
+ runtime: stampNew(current.data)
21670
21748
  };
21671
21749
  const valid = workspaceRecordSchema.safeParse(record);
21672
21750
  if (!valid.success)
@@ -21709,7 +21787,8 @@ class TaskStore {
21709
21787
  ...changedWorkspace.data,
21710
21788
  id: workspace.data.id,
21711
21789
  root: this.root,
21712
- revision: workspaceContext.revision
21790
+ revision: workspaceContext.revision,
21791
+ runtime: stampNew(workspace.data)
21713
21792
  });
21714
21793
  if (!nextWorkspace.success)
21715
21794
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -21734,7 +21813,8 @@ class TaskStore {
21734
21813
  workspaceId: task.data.workspaceId,
21735
21814
  createdAt: task.data.createdAt,
21736
21815
  revision: taskContext.revision,
21737
- updatedAt: taskContext.now
21816
+ updatedAt: taskContext.now,
21817
+ runtime: stampNew(task.data)
21738
21818
  });
21739
21819
  if (!nextTask.success) {
21740
21820
  this.markUncertain(nextWorkspace.data);
@@ -21761,7 +21841,7 @@ class TaskStore {
21761
21841
  if (match)
21762
21842
  candidates.push({
21763
21843
  target: match[1],
21764
- path: path3.join(this.recoveryDir, name),
21844
+ path: path4.join(this.recoveryDir, name),
21765
21845
  digest: match[3]
21766
21846
  });
21767
21847
  }
@@ -21954,7 +22034,8 @@ class TaskStore {
21954
22034
  const value = {
21955
22035
  ...workspace,
21956
22036
  writer: { ...workspace.writer, state: "uncertain" },
21957
- revision: newRevision()
22037
+ revision: newRevision(),
22038
+ runtime: stampNew(workspace)
21958
22039
  };
21959
22040
  this.replaceSnapshot(this.workspacePath, value, workspace);
21960
22041
  }
@@ -22032,7 +22113,7 @@ class TaskStore {
22032
22113
  }
22033
22114
  fs4.renameSync(temporary, file);
22034
22115
  temporary = undefined;
22035
- this.fsyncDirectory(path3.dirname(file));
22116
+ this.fsyncDirectory(path4.dirname(file));
22036
22117
  return success(null, null, value);
22037
22118
  } catch (error) {
22038
22119
  return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
@@ -22046,9 +22127,9 @@ class TaskStore {
22046
22127
  }
22047
22128
  }
22048
22129
  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`);
22130
+ const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
22131
+ const id = target === "task" ? path4.basename(file, ".json") : "workspace";
22132
+ const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
22052
22133
  if (fs4.existsSync(destination)) {
22053
22134
  if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
22054
22135
  return;
@@ -22126,7 +22207,7 @@ class TaskStore {
22126
22207
  for (const name of fs4.readdirSync(this.tasksDir)) {
22127
22208
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
22128
22209
  return null;
22129
- const parsed = this.parseBytes(fs4.readFileSync(path3.join(this.tasksDir, name)), taskRecordSchema);
22210
+ const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
22130
22211
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
22131
22212
  return null;
22132
22213
  ids.add(parsed.data.workspaceId);
@@ -22168,7 +22249,12 @@ class TaskStore {
22168
22249
  if (isObject3(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
22169
22250
  return failure("unsupported_version", "unsupported snapshot schema version");
22170
22251
  const parsed = schema.safeParse(value);
22171
- return parsed.success ? success(null, null, parsed.data) : failure("recovery_required", "snapshot does not satisfy its schema");
22252
+ if (parsed.success)
22253
+ return success(null, null, parsed.data);
22254
+ const writerVersion = isObject3(value) && isObject3(value.runtime) ? value.runtime.updatedWith : null;
22255
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
22256
+ return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
22257
+ return failure("recovery_required", "snapshot does not satisfy its schema");
22172
22258
  }
22173
22259
  processStart(pid) {
22174
22260
  try {
@@ -22178,43 +22264,98 @@ class TaskStore {
22178
22264
  }
22179
22265
  }
22180
22266
  conflict(expected, actual) {
22181
- return failure("revision_conflict", "snapshot revision does not match", {
22267
+ return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
22182
22268
  expectedRevision: expected,
22183
22269
  actualRevision: actual
22184
22270
  });
22185
22271
  }
22186
22272
  taskPath(taskId) {
22187
- return path3.join(this.tasksDir, `${taskId}.json`);
22273
+ return path4.join(this.tasksDir, `${taskId}.json`);
22188
22274
  }
22189
22275
  get workitDir() {
22190
- return path3.join(this.root, ".workit");
22276
+ return path4.join(this.root, ".workit");
22191
22277
  }
22192
22278
  get tasksDir() {
22193
- return path3.join(this.workitDir, "tasks");
22279
+ return path4.join(this.workitDir, "tasks");
22194
22280
  }
22195
22281
  get recoveryDir() {
22196
- return path3.join(this.workitDir, "recovery");
22282
+ return path4.join(this.workitDir, "recovery");
22197
22283
  }
22198
22284
  get workspacePath() {
22199
- return path3.join(this.workitDir, "workspace.json");
22285
+ return path4.join(this.workitDir, "workspace.json");
22200
22286
  }
22201
22287
  get lockPath() {
22202
- return path3.join(this.workitDir, "metadata.lock");
22288
+ return path4.join(this.workitDir, "metadata.lock");
22203
22289
  }
22204
22290
  get gitignorePath() {
22205
- return path3.join(this.workitDir, ".gitignore");
22291
+ return path4.join(this.workitDir, ".gitignore");
22206
22292
  }
22207
22293
  }
22294
+ // packages/workit-core/src/core/workers.ts
22295
+ var same = (left, right) => {
22296
+ try {
22297
+ return canonicalJson(left) === canonicalJson(right);
22298
+ } catch {
22299
+ return false;
22300
+ }
22301
+ };
22302
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
22303
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
22304
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
22305
+ var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
22306
+ const code = char.charCodeAt(0);
22307
+ return code < 32 || code === 127;
22308
+ }) && !value.split(/[\\/]/).includes("..");
22309
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
22310
+ var workerIdOf = (value) => value.workerId ?? null;
22311
+ function assertProductWriteAllowed(input) {
22312
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
22313
+ return failure("invalid_input", "invalid product write path");
22314
+ if (!input.store)
22315
+ return failure("permission_denied", "write authorization requires core state");
22316
+ const task = input.store.readTask(input.task.id);
22317
+ if (!task.ok)
22318
+ return task;
22319
+ const workspace = input.store.readWorkspace();
22320
+ if (!workspace.ok)
22321
+ return workspace;
22322
+ if (!workspace.data)
22323
+ return failure("not_found", "workspace not found");
22324
+ const workspaceRecord = workspace.data;
22325
+ if (task.data.status !== "active")
22326
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
22327
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
22328
+ return failure("recovery_required", "task and workspace bindings are invalid");
22329
+ const writer = workspaceRecord.writer;
22330
+ if (!writer)
22331
+ return failure("permission_denied", "checkout has no writer owner");
22332
+ if (writer.state === "uncertain")
22333
+ return failure("recovery_required", "checkout writer requires recovery", {
22334
+ owner: writer.owner
22335
+ });
22336
+ const caller = callerValue(input.caller);
22337
+ const workerId = workerIdOf(input.caller);
22338
+ const owner = writer.owner;
22339
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
22340
+ return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
22341
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
22342
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
22343
+ return failure("permission_denied", "only implementers can own product writes");
22344
+ 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))))
22345
+ return failure("recovery_required", "worker session is not an active writer");
22346
+ return success(null, workspaceRecord.revision, owner);
22347
+ }
22348
+
22208
22349
  // packages/workit-core/src/core/task-evaluation.ts
22209
22350
  import { createHash as createHash4 } from "node:crypto";
22210
22351
  import * as fs6 from "node:fs";
22211
22352
  import { spawnSync } from "node:child_process";
22212
- import path5 from "node:path";
22353
+ import path6 from "node:path";
22213
22354
 
22214
22355
  // packages/workit-core/src/core/authority.ts
22215
22356
  import { createHash as createHash3 } from "node:crypto";
22216
22357
  import * as fs5 from "node:fs";
22217
- import path4 from "node:path";
22358
+ import path5 from "node:path";
22218
22359
  var verifiedAuthorities = new WeakMap;
22219
22360
  var trustedAuthority = (kind, provenance, expected, binding, caller) => {
22220
22361
  const token = {};
@@ -22341,20 +22482,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
22341
22482
  continue;
22342
22483
  if (!reference.digest)
22343
22484
  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");
22485
+ const target = path5.resolve(root, reference.path);
22486
+ if (target !== root && !target.startsWith(`${root}${path5.sep}`))
22487
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
22488
+ fields: [
22489
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
22490
+ ]
22491
+ });
22347
22492
  try {
22348
- const relative = path4.relative(root, target);
22493
+ const relative = path5.relative(root, target);
22349
22494
  let current = root;
22350
- for (const segment of relative.split(path4.sep)) {
22495
+ for (const segment of relative.split(path5.sep)) {
22351
22496
  if (!segment)
22352
22497
  continue;
22353
- current = path4.join(current, segment);
22498
+ current = path5.join(current, segment);
22354
22499
  const stat = fs5.lstatSync(current);
22355
22500
  const real = fs5.realpathSync(current);
22356
- if (real !== root && !real.startsWith(`${root}${path4.sep}`))
22357
- return failure("invalid_input", "document reference escapes checkout");
22501
+ if (real !== root && !real.startsWith(`${root}${path5.sep}`))
22502
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
22503
+ fields: [
22504
+ {
22505
+ path: "contentRefs.path",
22506
+ reason: `${reference.path} resolves outside the checkout`
22507
+ }
22508
+ ]
22509
+ });
22358
22510
  if (stat.isSymbolicLink())
22359
22511
  return failure("invalid_input", "document reference uses a symlink");
22360
22512
  if (current === target && !stat.isFile())
@@ -22701,14 +22853,14 @@ var verifyDecisionContent = verifyContentRefs;
22701
22853
 
22702
22854
  // packages/workit-core/src/core/task-evaluation.ts
22703
22855
  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("/") || ".";
22856
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
22857
+ var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
22706
22858
  var canonicalPath = (value) => {
22707
22859
  if (typeof value !== "string" || !value)
22708
22860
  return null;
22709
22861
  if (value.includes("\\") || value.split("/").includes(".."))
22710
22862
  return null;
22711
- const normalized = path5.posix.normalize(value);
22863
+ const normalized = path6.posix.normalize(value);
22712
22864
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
22713
22865
  return null;
22714
22866
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -22756,14 +22908,14 @@ var gitPaths = (root) => {
22756
22908
  git: /not a git repository/i.test(stderr) ? false : true
22757
22909
  };
22758
22910
  }
22759
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")) : [];
22911
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
22760
22912
  const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
22761
22913
  if (stagedDeleted.status !== 0) {
22762
22914
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
22763
22915
  if (!/not a git repository/i.test(stderr))
22764
22916
  return { paths, uncertain: true, git: true };
22765
22917
  } else if (stagedDeleted.stdout) {
22766
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")));
22918
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
22767
22919
  }
22768
22920
  return { paths: [...new Set(paths)], uncertain: false, git: true };
22769
22921
  };
@@ -22778,7 +22930,7 @@ var walk = (root, directory, output) => {
22778
22930
  for (const name of names) {
22779
22931
  if (name === ".git" || name === ".workit")
22780
22932
  continue;
22781
- const target = path5.join(directory, name);
22933
+ const target = path6.join(directory, name);
22782
22934
  let stat;
22783
22935
  try {
22784
22936
  stat = fs6.lstatSync(target);
@@ -22796,17 +22948,17 @@ var walk = (root, directory, output) => {
22796
22948
  var scopeRoots = (root, scope) => {
22797
22949
  const roots = [];
22798
22950
  for (const item of scope.paths.length ? scope.paths : ["."]) {
22799
- const target = path5.resolve(root, item);
22951
+ const target = path6.resolve(root, item);
22800
22952
  if (!inside(root, target))
22801
- return failure("invalid_input", "candidate scope escapes checkout");
22953
+ 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
22954
  let ancestor = target;
22803
22955
  while (!fs6.existsSync(ancestor) && ancestor !== root)
22804
- ancestor = path5.dirname(ancestor);
22956
+ ancestor = path6.dirname(ancestor);
22805
22957
  try {
22806
22958
  if (!inside(root, fs6.realpathSync(ancestor)))
22807
- return failure("invalid_input", "candidate scope escapes checkout");
22959
+ 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
22960
  } catch {
22809
- return failure("invalid_input", "candidate scope cannot be inspected");
22961
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
22810
22962
  }
22811
22963
  roots.push(target);
22812
22964
  }
@@ -22821,7 +22973,7 @@ function captureCandidate(root, scope, environment = []) {
22821
22973
  }
22822
22974
  const normalizedScope = canonicalScope(scope);
22823
22975
  if (!normalizedScope)
22824
- return failure("invalid_input", "candidate scope is invalid");
22976
+ 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
22977
  const roots = scopeRoots(checkout, normalizedScope);
22826
22978
  if (!roots.ok)
22827
22979
  return roots;
@@ -22853,7 +23005,7 @@ function captureCandidate(root, scope, environment = []) {
22853
23005
  for (const item of [...names].sort()) {
22854
23006
  if (!scopeMatches(item, normalizedScope))
22855
23007
  continue;
22856
- const target = path5.join(checkout, item);
23008
+ const target = path6.join(checkout, item);
22857
23009
  try {
22858
23010
  const stat = fs6.lstatSync(target);
22859
23011
  if (stat.isSymbolicLink()) {
@@ -22975,7 +23127,7 @@ function evaluateEvidence(task, candidate) {
22975
23127
  }
22976
23128
  var scopeCovers2 = scopeCovers;
22977
23129
  var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
22978
- var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && data.response === "approved" && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
23130
+ var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
22979
23131
  var evidenceMatchesRequirement = (kind, evidenceKind, dimension) => {
22980
23132
  if (kind !== "passed")
22981
23133
  return false;
@@ -23010,7 +23162,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
23010
23162
  return true;
23011
23163
  const reviewSession = sessionFromRef(entry.data.reviewContext);
23012
23164
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
23013
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
23165
+ const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
23014
23166
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
23015
23167
  });
23016
23168
  if (passed.length) {
@@ -23051,7 +23203,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
23051
23203
  status: "satisfied",
23052
23204
  evidenceIds: [],
23053
23205
  decisionIds: decisions.map((decision) => decision.id),
23054
- reason: "an applicable approved decision satisfies the requirement"
23206
+ reason: "an applicable approved or stated decision satisfies the requirement"
23055
23207
  };
23056
23208
  const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
23057
23209
  if (limitations.length)
@@ -23106,7 +23258,17 @@ function evaluateClosure(requestedOutcome, view) {
23106
23258
  const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
23107
23259
  if (requestedOutcome !== "stopped" && blocking.length)
23108
23260
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
23109
- requirementIds: blocking.map((item) => item.requirementId)
23261
+ requirementIds: blocking.map((item) => item.requirementId),
23262
+ requirements: blocking.map((item) => {
23263
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
23264
+ return {
23265
+ requirementId: item.requirementId,
23266
+ ruleId: requirement?.ruleId ?? "unknown",
23267
+ reason: item.reason,
23268
+ satisfaction: requirement?.satisfaction ?? "",
23269
+ dependentAction: requirement?.dependentAction ?? null
23270
+ };
23271
+ })
23110
23272
  });
23111
23273
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
23112
23274
  if (requestedOutcome === "verified" && accepted.length)
@@ -23142,7 +23304,7 @@ function reconcileResume(view, observations = []) {
23142
23304
  return candidate;
23143
23305
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
23144
23306
  const blockers = [...view.task.progress.blockers];
23145
- if (view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
23307
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
23146
23308
  blockers.push({
23147
23309
  reason: "worker state requires reconciliation",
23148
23310
  dependentAction: "resume",
@@ -23175,7 +23337,11 @@ var externalActionSchema = discriminatedUnion2("operation", [
23175
23337
  }).strict(),
23176
23338
  object4({
23177
23339
  operation: literal3("git.commit"),
23178
- payload: object4({ message: string5().min(1) }).strict()
23340
+ payload: object4({
23341
+ message: string5().min(1).optional(),
23342
+ plan_steps: array3(string5().min(1)).optional(),
23343
+ plan_branch: string5().min(1).optional()
23344
+ }).strict()
23179
23345
  }).strict(),
23180
23346
  object4({
23181
23347
  operation: literal3("git.push"),
@@ -23410,7 +23576,7 @@ function resolveRequirements(input) {
23410
23576
  ].join(", ")}`,
23411
23577
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
23412
23578
  before: "dependent_action",
23413
- dependentAction: "dependent-action",
23579
+ dependentAction: null,
23414
23580
  acceptanceAllowed: false
23415
23581
  }));
23416
23582
  if (signals.productChoiceOpen.value === true)
@@ -23421,7 +23587,7 @@ function resolveRequirements(input) {
23421
23587
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
23422
23588
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
23423
23589
  before: "dependent_action",
23424
- dependentAction: "dependent-action",
23590
+ dependentAction: null,
23425
23591
  acceptanceAllowed: false
23426
23592
  }));
23427
23593
  if (signals.behaviorChange.value === true)
@@ -23579,59 +23745,6 @@ function diffPolicy(previous, next, reason, now) {
23579
23745
  return parsed.data;
23580
23746
  }
23581
23747
 
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
23748
  // packages/workit-core/src/core/task-engine.ts
23636
23749
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
23637
23750
  kind,
@@ -23762,15 +23875,20 @@ var applyWorkerLifecycle = (input) => {
23762
23875
  return failure("invalid_transition", "worker has not started");
23763
23876
  if (input.state === "running" && entry.data.state === "stopped")
23764
23877
  return failure("invalid_transition", "stopped worker cannot run again");
23878
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
23765
23879
  const nextEntry = {
23766
23880
  ...entry,
23767
23881
  recordedAt: input.now ?? entry.recordedAt,
23768
23882
  provenance: authority.provenance,
23769
- data: { ...entry.data, state: input.state, session: input.session }
23883
+ data: {
23884
+ ...entry.data,
23885
+ state: terminalCancel ? "cancelling" : input.state,
23886
+ session: input.session
23887
+ }
23770
23888
  };
23771
23889
  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
23890
  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)
23891
+ if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
23774
23892
  return success(task.data.revision, workspace.data.revision, entry);
23775
23893
  const changed = input.store.mutateTaskAndWorkspace({
23776
23894
  taskId: task.data.id,
@@ -24140,7 +24258,7 @@ class WorkitCore {
24140
24258
  activeWorkerBlocker(task, workspace) {
24141
24259
  if (workspace.writer)
24142
24260
  return failure("recovery_required", "writer ownership must be released first");
24143
- if (task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
24261
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
24144
24262
  return failure("recovery_required", "worker state requires reconciliation");
24145
24263
  return null;
24146
24264
  }
@@ -24377,11 +24495,6 @@ class WorkitCore {
24377
24495
  if (helper && !helper.ok)
24378
24496
  return helper;
24379
24497
  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
24498
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
24386
24499
  return failure("invalid_input", "missing or skipped evidence requires a reason");
24387
24500
  if (evidence.kind === "review") {
@@ -24398,6 +24511,11 @@ class WorkitCore {
24398
24511
  evidence.beforeCandidateId ??= null;
24399
24512
  evidence.candidateId ??= null;
24400
24513
  }
24514
+ if (helper?.ok) {
24515
+ const assignment = helper.data.data.assignment;
24516
+ 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))
24517
+ return failure("permission_denied", "evidence is outside the worker assignment");
24518
+ }
24401
24519
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
24402
24520
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
24403
24521
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -24451,7 +24569,12 @@ class WorkitCore {
24451
24569
  const input = parsed.data;
24452
24570
  if (nativeRequired && input.action !== "record")
24453
24571
  return failure("permission_denied", "native observation is only valid for recording decisions");
24454
- if (nativeRequired && nativeObservation === undefined)
24572
+ const stated = input.action === "record" && input.response === "stated";
24573
+ if (stated && input.purpose === "action")
24574
+ return failure("permission_denied", "stated choices cannot authorize mutating actions");
24575
+ if (stated && !input.binding?.statedChoice)
24576
+ return failure("invalid_input", "stated choices require binding.statedChoice");
24577
+ if (nativeRequired && nativeObservation === undefined && !stated)
24455
24578
  return failure("permission_denied", "native decision observation is required");
24456
24579
  const task = this.store.readTask(input.taskId);
24457
24580
  if (!task.ok)
@@ -24484,7 +24607,7 @@ class WorkitCore {
24484
24607
  consumption: null
24485
24608
  };
24486
24609
  const data = { ...base, digest: decisionDigest(base) };
24487
- const native = nativeRequired ? verifyNativeDecision(this.context.nativeAuthority, {
24610
+ const native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
24488
24611
  observation: nativeObservation,
24489
24612
  expected: {
24490
24613
  taskId: task.data.id,
@@ -25293,6 +25416,9 @@ class WorkitCore {
25293
25416
  id: task.id,
25294
25417
  revision: task.revision,
25295
25418
  workspaceRevision: workspace.data.revision,
25419
+ createdAt: task.createdAt,
25420
+ updatedAt: task.updatedAt,
25421
+ runtime: task.runtime ?? null,
25296
25422
  objective: task.intent.data.objective,
25297
25423
  status: task.status,
25298
25424
  closure: task.closure,
@@ -25378,7 +25504,7 @@ class WorkitCore {
25378
25504
  const base = reconcileResume(effectiveView);
25379
25505
  if (!base.ok)
25380
25506
  return base;
25381
- const needsReconciliation = view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
25507
+ const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
25382
25508
  const blockers = [...base.data.blockers];
25383
25509
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
25384
25510
  blockers.push({
@@ -25445,7 +25571,8 @@ class WorkitCore {
25445
25571
  ...current,
25446
25572
  status,
25447
25573
  candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
25448
- progress: status === "paused" ? { ...current.progress, summary: input.reason } : current.progress
25574
+ progress: current.progress,
25575
+ pauseReason: status === "paused" ? input.reason ?? null : null
25449
25576
  })
25450
25577
  });
25451
25578
  if (!changed.ok)
@@ -25481,7 +25608,7 @@ class WorkitCore {
25481
25608
  return workspace;
25482
25609
  if (!workspace.data)
25483
25610
  return failure("not_found", "workspace not found");
25484
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
25611
+ if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
25485
25612
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
25486
25613
  this.fillRevisions(input, task.data, workspace.data);
25487
25614
  const changed = this.store.mutateTaskAndWorkspace({
@@ -25592,15 +25719,15 @@ function resolveGitRevision(root, value) {
25592
25719
  }
25593
25720
  function resolveInside(root, candidate) {
25594
25721
  const base = realpathSync5(root);
25595
- const target = path6.resolve(base, candidate);
25596
- if (target !== base && !target.startsWith(base + path6.sep)) {
25722
+ const target = path7.resolve(base, candidate);
25723
+ if (target !== base && !target.startsWith(base + path7.sep)) {
25597
25724
  throw new Error("path must stay inside repository root");
25598
25725
  }
25599
25726
  let ancestor = target;
25600
- while (!existsSync3(ancestor))
25601
- ancestor = path6.dirname(ancestor);
25727
+ while (!existsSync4(ancestor))
25728
+ ancestor = path7.dirname(ancestor);
25602
25729
  const canonicalAncestor = realpathSync5(ancestor);
25603
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path6.sep)) {
25730
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
25604
25731
  throw new Error("path must stay inside repository root");
25605
25732
  }
25606
25733
  return target;
@@ -25785,14 +25912,14 @@ var gitContext = (workspaceRoot, paths = []) => {
25785
25912
  import {
25786
25913
  copyFileSync,
25787
25914
  cpSync,
25788
- existsSync as existsSync4,
25915
+ existsSync as existsSync5,
25789
25916
  mkdirSync as mkdirSync2,
25790
25917
  readFileSync as readFileSync4,
25791
25918
  readdirSync as readdirSync3,
25792
25919
  writeFileSync as writeFileSync2
25793
25920
  } from "node:fs";
25794
25921
  import os2 from "node:os";
25795
- import path7 from "node:path";
25922
+ import path8 from "node:path";
25796
25923
  var diagnosticLogger;
25797
25924
  var PRESETS = {
25798
25925
  gitflow: {
@@ -25813,7 +25940,7 @@ var mergePreset = (preset, input = {}, current = {
25813
25940
  protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
25814
25941
  };
25815
25942
  };
25816
- 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");
25943
+ 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");
25817
25944
  var migratedDir = null;
25818
25945
  var migrationFailed = false;
25819
25946
  var ensureConfigDir = (dir = resolveConfigDir()) => {
@@ -25823,12 +25950,12 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
25823
25950
  migratedDir = dir;
25824
25951
  return dir;
25825
25952
  }
25826
- const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os2.homedir(), ".config"), "workflow-toolkit");
25827
- if (!existsSync4(legacy)) {
25953
+ const legacy = path8.join(process.env.XDG_CONFIG_HOME || path8.join(os2.homedir(), ".config"), "workflow-toolkit");
25954
+ if (!existsSync5(legacy)) {
25828
25955
  migratedDir = dir;
25829
25956
  return dir;
25830
25957
  }
25831
- if (!migrationFailed && existsSync4(dir)) {
25958
+ if (!migrationFailed && existsSync5(dir)) {
25832
25959
  migratedDir = dir;
25833
25960
  return dir;
25834
25961
  }
@@ -25836,9 +25963,9 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
25836
25963
  mkdirSync2(dir, { recursive: true });
25837
25964
  diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
25838
25965
  for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
25839
- const src = path7.join(legacy, entry.name);
25840
- const dest = path7.join(dir, entry.name);
25841
- if (existsSync4(dest))
25966
+ const src = path8.join(legacy, entry.name);
25967
+ const dest = path8.join(dir, entry.name);
25968
+ if (existsSync5(dest))
25842
25969
  continue;
25843
25970
  try {
25844
25971
  if (entry.isDirectory())
@@ -25911,7 +26038,7 @@ var parseConfigResult = (raw, file) => {
25911
26038
  };
25912
26039
  };
25913
26040
  var readConfigTyped = (dir) => {
25914
- const file = path7.join(dir ?? configDir(), "config.json");
26041
+ const file = path8.join(dir ?? configDir(), "config.json");
25915
26042
  return parseConfigResult(readSafe(file), file);
25916
26043
  };
25917
26044
  var readConfig = () => {
@@ -25947,9 +26074,9 @@ var COMMIT_PRESETS = [
25947
26074
 
25948
26075
  // packages/workit-core/src/core/workspaces.ts
25949
26076
  import { readFileSync as readFileSync5, realpathSync as realpathSync6 } from "node:fs";
25950
- import path8 from "node:path";
26077
+ import path9 from "node:path";
25951
26078
  var readWorkspacesResult = (dir = configDir()) => {
25952
- const file = path8.join(dir, "workspaces.json");
26079
+ const file = path9.join(dir, "workspaces.json");
25953
26080
  let raw;
25954
26081
  try {
25955
26082
  raw = readFileSync5(file, "utf8");
@@ -26047,10 +26174,10 @@ var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
26047
26174
 
26048
26175
  // packages/workit-core/src/core/vcs-config.ts
26049
26176
  import fs7 from "node:fs";
26050
- import path9 from "node:path";
26177
+ import path10 from "node:path";
26051
26178
  import { spawnSync as spawnSync3 } from "node:child_process";
26052
26179
  var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
26053
- var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
26180
+ var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path10.join(configDir(), "vcs.json");
26054
26181
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
26055
26182
  var remoteProvider = (cwd) => {
26056
26183
  const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
@@ -26133,8 +26260,8 @@ function vcsConfig(mode, cwd) {
26133
26260
  }
26134
26261
  const prov = cfg[provider] ?? {};
26135
26262
  const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
26136
- const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path9.join(configDir(), `${provider}.token`));
26137
- const tokenPath = path9.resolve(tokenFile);
26263
+ const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path10.join(configDir(), `${provider}.token`));
26264
+ const tokenPath = path10.resolve(tokenFile);
26138
26265
  let tokenOk = false;
26139
26266
  if (fs7.existsSync(tokenPath)) {
26140
26267
  const token = fs7.readFileSync(tokenPath, "utf8").trim();
@@ -26143,7 +26270,7 @@ function vcsConfig(mode, cwd) {
26143
26270
  }
26144
26271
  const out = {
26145
26272
  ok: true,
26146
- configPath: path9.resolve(cfgPath),
26273
+ configPath: path10.resolve(cfgPath),
26147
26274
  provider,
26148
26275
  defaultTargetBranch: defaultTarget,
26149
26276
  pr: cfg.pr ?? {},
@@ -26236,18 +26363,18 @@ var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig()
26236
26363
 
26237
26364
  // packages/workit-core/src/core/pr-create.ts
26238
26365
  import fs8 from "node:fs";
26239
- import path10 from "node:path";
26366
+ import path11 from "node:path";
26240
26367
  function parseGhIssue(value) {
26241
26368
  const m = /issues\/(\d+)/.exec(value);
26242
26369
  return m ? m[1] : String(value).trim().replace(/^#/, "");
26243
26370
  }
26244
26371
  function whichOnPath(tool) {
26245
26372
  const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
26246
- for (const dir of (process.env.PATH ?? "").split(path10.delimiter)) {
26373
+ for (const dir of (process.env.PATH ?? "").split(path11.delimiter)) {
26247
26374
  if (!dir)
26248
26375
  continue;
26249
26376
  for (const name of names) {
26250
- const candidate = path10.join(dir, name);
26377
+ const candidate = path11.join(dir, name);
26251
26378
  try {
26252
26379
  fs8.accessSync(candidate, fs8.constants.X_OK);
26253
26380
  return candidate;
@@ -26260,8 +26387,8 @@ var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "gla
26260
26387
 
26261
26388
  // packages/workit-core/src/core/repo-context.ts
26262
26389
  import { spawnSync as spawnSync4 } from "node:child_process";
26263
- import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "node:fs";
26264
- import path11 from "node:path";
26390
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "node:fs";
26391
+ import path12 from "node:path";
26265
26392
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
26266
26393
  try {
26267
26394
  gitRevisionParts(value);
@@ -26395,7 +26522,7 @@ var findPrTemplate = (cwd) => {
26395
26522
  ]) {
26396
26523
  const parts = rel.split("/");
26397
26524
  const base = parts[parts.length - 1];
26398
- const dir = parts.length > 1 ? path11.join(cwd, ...parts.slice(0, -1)) : cwd;
26525
+ const dir = parts.length > 1 ? path12.join(cwd, ...parts.slice(0, -1)) : cwd;
26399
26526
  const actual = probeCaseInsensitive(dir, base);
26400
26527
  if (actual)
26401
26528
  return [...parts.slice(0, -1), actual].join("/");
@@ -26426,8 +26553,8 @@ var diffStatForRange = (cwd, range) => {
26426
26553
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
26427
26554
  };
26428
26555
  var packageScripts = (cwd, keys) => {
26429
- const pkgPath = path11.join(cwd, "package.json");
26430
- if (!existsSync5(pkgPath))
26556
+ const pkgPath = path12.join(cwd, "package.json");
26557
+ if (!existsSync6(pkgPath))
26431
26558
  return [];
26432
26559
  try {
26433
26560
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
@@ -26452,9 +26579,9 @@ var documentationFiles = (cwd) => {
26452
26579
  if (entry.isDirectory()) {
26453
26580
  if (excluded.has(entry.name))
26454
26581
  continue;
26455
- walk(path11.join(dir, entry.name), depth + 1);
26582
+ walk(path12.join(dir, entry.name), depth + 1);
26456
26583
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
26457
- matches.push("./" + path11.relative(cwd, path11.join(dir, entry.name)).replaceAll("\\", "/"));
26584
+ matches.push("./" + path12.relative(cwd, path12.join(dir, entry.name)).replaceAll("\\", "/"));
26458
26585
  }
26459
26586
  }
26460
26587
  };
@@ -26462,7 +26589,7 @@ var documentationFiles = (cwd) => {
26462
26589
  return matches.sort().slice(0, 200);
26463
26590
  };
26464
26591
  var readTrimmed = (file, maxLines) => {
26465
- if (!existsSync5(file))
26592
+ if (!existsSync6(file))
26466
26593
  return "";
26467
26594
  const lines = readFileSync6(file, "utf8").split(`
26468
26595
  `);
@@ -26470,7 +26597,7 @@ var readTrimmed = (file, maxLines) => {
26470
26597
  `);
26471
26598
  };
26472
26599
  function prReadyContext(root, range) {
26473
- const cwd = path11.resolve(repoRoot(root));
26600
+ const cwd = path12.resolve(repoRoot(root));
26474
26601
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26475
26602
  return invalidRange(cwd);
26476
26603
  let stdout = "";
@@ -26528,7 +26655,7 @@ ${status.stderr}` : "");
26528
26655
  stdout += `template_path: ${template}
26529
26656
 
26530
26657
  `;
26531
- stdout += readTrimmed(path11.join(cwd, template), 220);
26658
+ stdout += readTrimmed(path12.join(cwd, template), 220);
26532
26659
  } else {
26533
26660
  stdout += `template_path: none
26534
26661
 
@@ -26538,7 +26665,7 @@ ${status.stderr}` : "");
26538
26665
  stdout += `
26539
26666
  `;
26540
26667
  stdout += printSection("Recent Validation Signals");
26541
- if (existsSync5(path11.join(cwd, "package.json"))) {
26668
+ if (existsSync6(path12.join(cwd, "package.json"))) {
26542
26669
  stdout += `package.json detected. Common scripts:
26543
26670
  `;
26544
26671
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -26547,7 +26674,7 @@ ${status.stderr}` : "");
26547
26674
  `) + `
26548
26675
  `;
26549
26676
  }
26550
- if (existsSync5(path11.join(cwd, "Cargo.toml")) || existsSync5(path11.join(cwd, "src-tauri/Cargo.toml"))) {
26677
+ if (existsSync6(path12.join(cwd, "Cargo.toml")) || existsSync6(path12.join(cwd, "src-tauri/Cargo.toml"))) {
26551
26678
  stdout += `Rust project detected.
26552
26679
  `;
26553
26680
  }
@@ -26580,7 +26707,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
26580
26707
  - Apply with the native workit_changelog_apply tool only (not hand-edits under Unreleased).
26581
26708
  - If Unreleased already has duplicate category headings, normalize_only first.`;
26582
26709
  function changelogContext(root, range) {
26583
- const cwd = path11.resolve(repoRoot(root));
26710
+ const cwd = path12.resolve(repoRoot(root));
26584
26711
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26585
26712
  return invalidRange(cwd);
26586
26713
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -26598,8 +26725,8 @@ function changelogContext(root, range) {
26598
26725
  stdout += CHANGELOG_RULES + `
26599
26726
  `;
26600
26727
  stdout += printSection("Existing CHANGELOG.md");
26601
- const changelogPath = path11.join(cwd, "CHANGELOG.md");
26602
- if (existsSync5(changelogPath)) {
26728
+ const changelogPath = path12.join(cwd, "CHANGELOG.md");
26729
+ if (existsSync6(changelogPath)) {
26603
26730
  stdout += readTrimmed(changelogPath, 260) + `
26604
26731
  `;
26605
26732
  } else {
@@ -26618,7 +26745,7 @@ function changelogContext(root, range) {
26618
26745
  return { stdout, stderr: "", exitCode: 0, cwd };
26619
26746
  }
26620
26747
  function docsRefreshContext(root, range) {
26621
- const cwd = path11.resolve(repoRoot(root));
26748
+ const cwd = path12.resolve(repoRoot(root));
26622
26749
  if (range !== undefined && !isResolvableContextRange(cwd, range))
26623
26750
  return invalidRange(cwd);
26624
26751
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -26640,8 +26767,8 @@ function docsRefreshContext(root, range) {
26640
26767
  `) + `
26641
26768
  `;
26642
26769
  stdout += printSection("README Preview");
26643
- const readme = path11.join(cwd, "README.md");
26644
- if (existsSync5(readme)) {
26770
+ const readme = path12.join(cwd, "README.md");
26771
+ if (existsSync6(readme)) {
26645
26772
  stdout += readTrimmed(readme, 220) + `
26646
26773
  `;
26647
26774
  } else {
@@ -26649,8 +26776,8 @@ function docsRefreshContext(root, range) {
26649
26776
  `;
26650
26777
  }
26651
26778
  stdout += printSection("Package Scripts");
26652
- const pkgPath = path11.join(cwd, "package.json");
26653
- if (existsSync5(pkgPath)) {
26779
+ const pkgPath = path12.join(cwd, "package.json");
26780
+ if (existsSync6(pkgPath)) {
26654
26781
  try {
26655
26782
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
26656
26783
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
@@ -26663,7 +26790,7 @@ function docsRefreshContext(root, range) {
26663
26790
  return { stdout, stderr: "", exitCode: 0, cwd };
26664
26791
  }
26665
26792
  function releaseNotesContext(root, rangeOrTag) {
26666
- const cwd = path11.resolve(repoRoot(root));
26793
+ const cwd = path12.resolve(repoRoot(root));
26667
26794
  if (!rangeOrTag) {
26668
26795
  return { stdout: "", stderr: `ERROR: release tag or range required
26669
26796
  `, exitCode: 1, cwd };
@@ -26701,7 +26828,7 @@ function releaseNotesContext(root, rangeOrTag) {
26701
26828
  `;
26702
26829
  stdout += printSection("Existing Release Files");
26703
26830
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
26704
- if (existsSync5(path11.join(cwd, rel)))
26831
+ if (existsSync6(path12.join(cwd, rel)))
26705
26832
  stdout += rel + `
26706
26833
  `;
26707
26834
  }
@@ -26892,25 +27019,6 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
26892
27019
  import fs10 from "node:fs";
26893
27020
  import path13 from "node:path";
26894
27021
 
26895
- // packages/workit-core/src/core/package-root.ts
26896
- import { existsSync as existsSync6 } from "node:fs";
26897
- import path12 from "node:path";
26898
- import { fileURLToPath } from "node:url";
26899
- var packageRoot = () => {
26900
- let dir = path12.dirname(fileURLToPath(import.meta.url));
26901
- while (true) {
26902
- const parent = path12.dirname(dir);
26903
- if (existsSync6(path12.join(dir, "package.json")) || parent === dir)
26904
- return dir;
26905
- dir = parent;
26906
- }
26907
- };
26908
- var assetRoot = () => {
26909
- const root = packageRoot();
26910
- const assets = path12.join(root, "assets");
26911
- return existsSync6(assets) ? assets : root;
26912
- };
26913
-
26914
27022
  // packages/workit-core/src/core/templates.ts
26915
27023
  var repoRoot2 = assetRoot();
26916
27024
 
@@ -27414,6 +27522,32 @@ var toolInputSchema = (family) => {
27414
27522
  const schema = boundedOperationJsonSchema(family);
27415
27523
  return { type: "object", ...schema };
27416
27524
  };
27525
+ var schemaBranches = (schema) => Array.isArray(schema.oneOf) ? schema.oneOf : Array.isArray(schema.anyOf) ? schema.anyOf : [schema];
27526
+ var readOnlyBranches = (family) => schemaBranches(boundedOperationJsonSchema(family)).filter((branch) => {
27527
+ const action = branch.properties?.action?.const;
27528
+ return typeof action === "string" && READ_ONLY_ACTIONS.has(action);
27529
+ });
27530
+ var advertisedToolSchemas = (attested) => {
27531
+ if (attested)
27532
+ return OPERATION_FAMILIES.map((family) => ({
27533
+ name: `workit_${family}`,
27534
+ description: operationDescription(family),
27535
+ inputSchema: toolInputSchema(family)
27536
+ }));
27537
+ return OPERATION_FAMILIES.flatMap((family) => {
27538
+ const branches = readOnlyBranches(family);
27539
+ if (!branches.length)
27540
+ return [];
27541
+ const schema = boundedOperationJsonSchema(family);
27542
+ return [
27543
+ {
27544
+ name: `workit_${family}`,
27545
+ description: `${operationDescription(family)} Unattested MCP callers can only run read-only actions; mutating operations require an attested host session.`,
27546
+ inputSchema: { type: "object", ...schema, oneOf: branches }
27547
+ }
27548
+ ];
27549
+ });
27550
+ };
27417
27551
  var sanitizeFailure = (result, workspaceRoot) => {
27418
27552
  if (result.ok)
27419
27553
  return result;
@@ -27468,13 +27602,16 @@ function createMcpServer(host, contextProvider) {
27468
27602
  ].filter((file) => existsSync7(file)));
27469
27603
  let staleWarned = false;
27470
27604
  const server = new Server({ name: "workit", version: VERSION }, { capabilities: { tools: {}, resources: {} } });
27471
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
27472
- tools: OPERATION_FAMILIES.map((family) => ({
27473
- name: `workit_${family}`,
27474
- description: operationDescription(family),
27475
- inputSchema: toolInputSchema(family)
27476
- }))
27477
- }));
27605
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
27606
+ let attested = false;
27607
+ try {
27608
+ const context = await contextProvider.current();
27609
+ attested = context.callerAttested === true;
27610
+ } catch {
27611
+ attested = false;
27612
+ }
27613
+ return { tools: advertisedToolSchemas(attested) };
27614
+ });
27478
27615
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
27479
27616
  resources: CONTEXT_KINDS.map((kind) => ({
27480
27617
  uri: `workit://context/${kind}`,
@@ -27589,7 +27726,7 @@ function createMcpServer(host, contextProvider) {
27589
27726
  const parsed = parseOperation(family, request.params.arguments);
27590
27727
  if (!parsed.ok)
27591
27728
  return resultForClient(parsed, workspaceRoot);
27592
- if (context.callerAttested === false && requiresCallerIdentity(parsed.data))
27729
+ if (context.callerAttested !== true && requiresCallerIdentity(parsed.data))
27593
27730
  return resultForClient({
27594
27731
  ok: false,
27595
27732
  schemaVersion: 1,