@brainervirus/workit-opencode 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.
package/dist/plugin.js CHANGED
@@ -14,14 +14,14 @@ var __export = (target, all) => {
14
14
  };
15
15
 
16
16
  // packages/workit-opencode/src/plugin.ts
17
- import { existsSync as existsSync12 } from "node:fs";
17
+ import { existsSync as existsSync13 } from "node:fs";
18
18
  import path25 from "node:path";
19
- import { fileURLToPath as fileURLToPath2 } from "node:url";
19
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
20
20
 
21
21
  // packages/workit-core/src/core.ts
22
22
  import { spawnSync as spawnSync2 } from "node:child_process";
23
- import { existsSync as existsSync3, realpathSync as realpathSync5 } from "node:fs";
24
- import path6 from "node:path";
23
+ import { existsSync as existsSync4, realpathSync as realpathSync5 } from "node:fs";
24
+ import path7 from "node:path";
25
25
 
26
26
  // packages/workit-core/src/core/task-contract.ts
27
27
  import { createHash, randomUUID } from "node:crypto";
@@ -7590,6 +7590,7 @@ var decisionSchema = object({
7590
7590
  scope: scopeSchema,
7591
7591
  presented: text,
7592
7592
  approvedContent: text,
7593
+ displayed: text.optional(),
7593
7594
  contentRefs: array(refSchema)
7594
7595
  }).strict(),
7595
7596
  digest,
@@ -7602,6 +7603,27 @@ var decisionSchema = object({
7602
7603
  actionRef: refSchema
7603
7604
  }).strict().nullable()
7604
7605
  }).strict();
7606
+ var BINDING_QUESTION_BUDGET = 300;
7607
+ var bindingQuestionHeader = /^Workit decision: (design|action|limitation|preference)$/;
7608
+ var workitBindingQuestionIssue = (questions) => {
7609
+ if (!Array.isArray(questions) || questions.length !== 1)
7610
+ return null;
7611
+ const question = questions[0];
7612
+ if (!question || typeof question !== "object")
7613
+ return null;
7614
+ if (typeof question.header !== "string" || !bindingQuestionHeader.test(question.header.trim()))
7615
+ return null;
7616
+ const over = (label, value) => `invalid_input: ${label} is ${value.length} characters; Workit binding questions must stay within ${BINDING_QUESTION_BUDGET}. present the item in the conversation first, then ask a short scoped question.`;
7617
+ if (typeof question.question === "string" && question.question.length > BINDING_QUESTION_BUDGET)
7618
+ return over("the question text", question.question);
7619
+ if (Array.isArray(question.options))
7620
+ for (const option of question.options) {
7621
+ const description = option.description;
7622
+ if (typeof description === "string" && description.length > BINDING_QUESTION_BUDGET)
7623
+ return over("an option description", description);
7624
+ }
7625
+ return null;
7626
+ };
7605
7627
  var findingSchema = object({
7606
7628
  claim: text,
7607
7629
  consequence: text,
@@ -7628,7 +7650,7 @@ var workerReportSchema = object({
7628
7650
  }).strict();
7629
7651
  var workerSchema = object({
7630
7652
  assignment: assignmentSchema,
7631
- state: _enum(["assigned", "running", "cancelling", "stopped", "unknown"]),
7653
+ state: _enum(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
7632
7654
  session: refSchema.nullable(),
7633
7655
  report: workerReportSchema.nullable()
7634
7656
  }).strict();
@@ -7668,6 +7690,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
7668
7690
  path: ["decisionId"]
7669
7691
  });
7670
7692
  });
7693
+ var runtimeSchema = object({
7694
+ createdWith: text.nullable(),
7695
+ updatedWith: text
7696
+ }).strict();
7671
7697
  var taskRecordSchema = object({
7672
7698
  schemaVersion: literal(1),
7673
7699
  id,
@@ -7681,6 +7707,8 @@ var taskRecordSchema = object({
7681
7707
  status: _enum(["active", "paused", "closed"]),
7682
7708
  closure: closureSchema.nullable(),
7683
7709
  progress: progressSchema,
7710
+ pauseReason: text.nullable().optional(),
7711
+ runtime: runtimeSchema.optional(),
7684
7712
  assessments: array(entrySchema(assessmentSchema)),
7685
7713
  policy: policySchema.nullable(),
7686
7714
  policyChanges: array(policyChangeSchema),
@@ -7696,6 +7724,7 @@ var workspaceRecordSchema = object({
7696
7724
  id,
7697
7725
  revision,
7698
7726
  root: nonEmpty,
7727
+ runtime: runtimeSchema.optional(),
7699
7728
  writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
7700
7729
  }).strict();
7701
7730
  var capabilitySchema = object({
@@ -7723,6 +7752,9 @@ var taskSummarySchema = object({
7723
7752
  id,
7724
7753
  revision,
7725
7754
  workspaceRevision: revision,
7755
+ createdAt: utc,
7756
+ updatedAt: utc,
7757
+ runtime: runtimeSchema.nullable(),
7726
7758
  objective: text,
7727
7759
  status: _enum(["active", "paused", "closed"]),
7728
7760
  closure: closureSchema.nullable(),
@@ -7793,7 +7825,7 @@ var taskOperations = {
7793
7825
  expectedWorkspaceRevision: revision.optional(),
7794
7826
  outcome: outcomeSchema,
7795
7827
  summary: text,
7796
- decisionIds: array(id)
7828
+ decisionIds: array(id).optional()
7797
7829
  })
7798
7830
  };
7799
7831
  var policyOperations = {
@@ -7885,14 +7917,15 @@ var writerOperations = {
7885
7917
  ...taskId,
7886
7918
  expectedRevision: revision.optional(),
7887
7919
  expectedWorkspaceRevision: revision.optional(),
7888
- workerId: nullableId.optional()
7920
+ workerId: nullableId.optional(),
7921
+ reason: text.optional()
7889
7922
  }),
7890
7923
  release: operation({
7891
7924
  action: literal("release"),
7892
7925
  ...taskId,
7893
7926
  expectedRevision: revision.optional(),
7894
7927
  expectedWorkspaceRevision: revision.optional(),
7895
- reason: text
7928
+ reason: text.optional()
7896
7929
  })
7897
7930
  };
7898
7931
  var stateOperations = {
@@ -8045,7 +8078,26 @@ function requirementId(input) {
8045
8078
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
8046
8079
  import * as fs4 from "node:fs";
8047
8080
  import { hostname } from "node:os";
8048
- import path3 from "node:path";
8081
+ import path4 from "node:path";
8082
+
8083
+ // packages/workit-core/src/core/package-root.ts
8084
+ import { existsSync } from "node:fs";
8085
+ import path from "node:path";
8086
+ import { fileURLToPath } from "node:url";
8087
+ var packageRoot = () => {
8088
+ let dir = path.dirname(fileURLToPath(import.meta.url));
8089
+ while (true) {
8090
+ const parent = path.dirname(dir);
8091
+ if (existsSync(path.join(dir, "package.json")) || parent === dir)
8092
+ return dir;
8093
+ dir = parent;
8094
+ }
8095
+ };
8096
+ var assetRoot = () => {
8097
+ const root = packageRoot();
8098
+ const assets = path.join(root, "assets");
8099
+ return existsSync(assets) ? assets : root;
8100
+ };
8049
8101
 
8050
8102
  // node_modules/@openclaw/fs-safe/dist/errors.js
8051
8103
  var OPERATIONAL_CODES = new Set([
@@ -8096,7 +8148,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
8096
8148
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
8097
8149
  import { randomBytes } from "node:crypto";
8098
8150
  import fsSync from "node:fs";
8099
- import path from "node:path";
8151
+ import path2 from "node:path";
8100
8152
 
8101
8153
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
8102
8154
  import fs from "node:fs";
@@ -8186,13 +8238,13 @@ ${ownershipToken}
8186
8238
  return { raw, ownershipToken };
8187
8239
  }
8188
8240
  function relativeSidecarLockPath(lockRoot, lockPath) {
8189
- const resolved = path.resolve(lockPath);
8190
- const lexicalRelative = path.relative(lockRoot.rootDir, resolved);
8191
- const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path.sep}`) && !path.isAbsolute(lexicalRelative) ? lexicalRelative : path.relative(lockRoot.rootReal, resolved);
8192
- if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
8241
+ const resolved = path2.resolve(lockPath);
8242
+ const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
8243
+ const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
8244
+ if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
8193
8245
  throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
8194
8246
  }
8195
- return relative.split(path.sep).join(path.posix.sep);
8247
+ return relative.split(path2.sep).join(path2.posix.sep);
8196
8248
  }
8197
8249
  function parseSidecarLockPayload(raw, parser) {
8198
8250
  if (parser) {
@@ -8339,7 +8391,7 @@ function getFsSafeLockConfig() {
8339
8391
 
8340
8392
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
8341
8393
  import fs3 from "node:fs";
8342
- import path2 from "node:path";
8394
+ import path3 from "node:path";
8343
8395
 
8344
8396
  // node_modules/@openclaw/fs-safe/dist/timing.js
8345
8397
  function sleepSync(ms) {
@@ -8430,26 +8482,26 @@ function canonicalLockParentSync(parent) {
8430
8482
  return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
8431
8483
  }
8432
8484
  function normalizeTargetPath(targetPath) {
8433
- const resolved = path2.resolve(targetPath);
8434
- fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
8485
+ const resolved = path3.resolve(targetPath);
8486
+ fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
8435
8487
  try {
8436
- return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
8488
+ return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
8437
8489
  } catch {
8438
8490
  return resolved;
8439
8491
  }
8440
8492
  }
8441
8493
  function boundedLockPath(lockPath, lockRoot) {
8442
- const resolved = path2.resolve(lockPath);
8494
+ const resolved = path3.resolve(lockPath);
8443
8495
  if (!lockRoot)
8444
8496
  return resolved;
8445
8497
  relativeSidecarLockPath(lockRoot, resolved);
8446
- const parent = path2.dirname(resolved);
8498
+ const parent = path3.dirname(resolved);
8447
8499
  const parentReal = canonicalLockParentSync(parent);
8448
- const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
8449
- if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
8500
+ const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
8501
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
8450
8502
  throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
8451
8503
  }
8452
- return path2.join(parentReal, path2.basename(resolved));
8504
+ return path3.join(parentReal, path3.basename(resolved));
8453
8505
  }
8454
8506
  function defaultShouldReclaim(snapshot, staleMs, nowMs) {
8455
8507
  const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
@@ -8662,6 +8714,33 @@ var metadataLockSchema = object({
8662
8714
  nonce: string2().min(1)
8663
8715
  }).strict();
8664
8716
  var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
8717
+ var cachedRuntimeVersion = null;
8718
+ var runtimeVersion = () => {
8719
+ if (cachedRuntimeVersion === null) {
8720
+ try {
8721
+ const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
8722
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
8723
+ } catch {
8724
+ cachedRuntimeVersion = "0.0.0";
8725
+ }
8726
+ }
8727
+ return cachedRuntimeVersion;
8728
+ };
8729
+ var stampNew = (record) => ({
8730
+ createdWith: record.runtime?.createdWith ?? null,
8731
+ updatedWith: runtimeVersion()
8732
+ });
8733
+ var isNewerVersion = (candidate, current) => {
8734
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
8735
+ const [next, now] = [parts(candidate), parts(current)];
8736
+ for (let index = 0;index < 3; index += 1) {
8737
+ const left = next[index] ?? 0;
8738
+ const right = now[index] ?? 0;
8739
+ if (left !== right)
8740
+ return left > right;
8741
+ }
8742
+ return false;
8743
+ };
8665
8744
  var jsonBytes = (value) => `${canonicalJson(value)}
8666
8745
  `;
8667
8746
  var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
@@ -8688,12 +8767,12 @@ var sameMetadataLock = (left, right) => {
8688
8767
  class TaskStore {
8689
8768
  root;
8690
8769
  constructor(root) {
8691
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path3.resolve(root);
8770
+ this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
8692
8771
  }
8693
8772
  readTask(taskId) {
8694
8773
  if (!validId(taskId))
8695
8774
  return failure("invalid_input", "task ID is invalid", { taskId });
8696
- const result = this.readRecord(path3.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
8775
+ const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
8697
8776
  if (!result.exists)
8698
8777
  return failure("not_found", "task not found", { taskId });
8699
8778
  if (result.result.ok && result.result.data.id !== taskId)
@@ -8727,7 +8806,7 @@ class TaskStore {
8727
8806
  });
8728
8807
  const tasks = [];
8729
8808
  for (const name of names.sort()) {
8730
- const item = this.readRecord(path3.join(this.tasksDir, name), taskRecordSchema);
8809
+ const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
8731
8810
  if (!item.exists)
8732
8811
  continue;
8733
8812
  if (!item.result.ok)
@@ -8761,7 +8840,7 @@ class TaskStore {
8761
8840
  if (!current.ok)
8762
8841
  return current;
8763
8842
  if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
8764
- return failure("revision_conflict", "workspace revision does not match", {
8843
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
8765
8844
  expectedWorkspaceRevision: value.expectedWorkspaceRevision,
8766
8845
  actualWorkspaceRevision: current.data?.revision ?? null
8767
8846
  });
@@ -8773,11 +8852,17 @@ class TaskStore {
8773
8852
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
8774
8853
  if (current.data && !previousWorkspaceBytes)
8775
8854
  return failure("storage_error", "workspace snapshot disappeared during creation");
8776
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
8855
+ const workspace = current.data ? {
8856
+ ...current.data,
8857
+ revision: newRevision(),
8858
+ root: this.root,
8859
+ runtime: stampNew(current.data)
8860
+ } : {
8777
8861
  schemaVersion: SCHEMA_VERSION,
8778
8862
  id: newId(),
8779
8863
  revision: newRevision(),
8780
8864
  root: this.root,
8865
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8781
8866
  writer: null
8782
8867
  };
8783
8868
  const timestamp = value.now ?? now();
@@ -8799,6 +8884,7 @@ class TaskStore {
8799
8884
  status: "active",
8800
8885
  closure: null,
8801
8886
  progress: { summary: "", nextAction: null, blockers: [] },
8887
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8802
8888
  assessments: [],
8803
8889
  policy: null,
8804
8890
  policyChanges: [],
@@ -8829,7 +8915,7 @@ class TaskStore {
8829
8915
  if (!current.ok)
8830
8916
  return current;
8831
8917
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
8832
- return failure("revision_conflict", "workspace revision does not match", {
8918
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
8833
8919
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
8834
8920
  actualWorkspaceRevision: current.data?.revision ?? null
8835
8921
  });
@@ -8837,11 +8923,17 @@ class TaskStore {
8837
8923
  if (current.data && !previousWorkspaceBytes)
8838
8924
  return failure("storage_error", "workspace snapshot disappeared during import");
8839
8925
  const timestamp = input.now ?? now();
8840
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
8926
+ const workspace = current.data ? {
8927
+ ...current.data,
8928
+ revision: newRevision(),
8929
+ root: this.root,
8930
+ runtime: stampNew(current.data)
8931
+ } : {
8841
8932
  schemaVersion: SCHEMA_VERSION,
8842
8933
  id: input.workspaceId ?? newId(),
8843
8934
  revision: newRevision(),
8844
8935
  root: this.root,
8936
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8845
8937
  writer: null
8846
8938
  };
8847
8939
  const task = {
@@ -8849,7 +8941,11 @@ class TaskStore {
8849
8941
  workspaceId: workspace.id,
8850
8942
  revision: newRevision(),
8851
8943
  createdAt: timestamp,
8852
- updatedAt: timestamp
8944
+ updatedAt: timestamp,
8945
+ runtime: {
8946
+ createdWith: input.task.runtime?.createdWith ?? null,
8947
+ updatedWith: runtimeVersion()
8948
+ }
8853
8949
  };
8854
8950
  const validTask = taskRecordSchema.safeParse(task);
8855
8951
  if (!validTask.success)
@@ -8898,7 +8994,8 @@ class TaskStore {
8898
8994
  workspaceId: current.data.workspaceId,
8899
8995
  createdAt: current.data.createdAt,
8900
8996
  revision: context.revision,
8901
- updatedAt: context.now
8997
+ updatedAt: context.now,
8998
+ runtime: stampNew(current.data)
8902
8999
  };
8903
9000
  const valid = taskRecordSchema.safeParse(record);
8904
9001
  if (!valid.success)
@@ -8932,7 +9029,8 @@ class TaskStore {
8932
9029
  ...changed.data,
8933
9030
  id: current.data.id,
8934
9031
  root: this.root,
8935
- revision: context.revision
9032
+ revision: context.revision,
9033
+ runtime: stampNew(current.data)
8936
9034
  };
8937
9035
  const valid = workspaceRecordSchema.safeParse(record);
8938
9036
  if (!valid.success)
@@ -8975,7 +9073,8 @@ class TaskStore {
8975
9073
  ...changedWorkspace.data,
8976
9074
  id: workspace.data.id,
8977
9075
  root: this.root,
8978
- revision: workspaceContext.revision
9076
+ revision: workspaceContext.revision,
9077
+ runtime: stampNew(workspace.data)
8979
9078
  });
8980
9079
  if (!nextWorkspace.success)
8981
9080
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -9000,7 +9099,8 @@ class TaskStore {
9000
9099
  workspaceId: task.data.workspaceId,
9001
9100
  createdAt: task.data.createdAt,
9002
9101
  revision: taskContext.revision,
9003
- updatedAt: taskContext.now
9102
+ updatedAt: taskContext.now,
9103
+ runtime: stampNew(task.data)
9004
9104
  });
9005
9105
  if (!nextTask.success) {
9006
9106
  this.markUncertain(nextWorkspace.data);
@@ -9027,7 +9127,7 @@ class TaskStore {
9027
9127
  if (match)
9028
9128
  candidates.push({
9029
9129
  target: match[1],
9030
- path: path3.join(this.recoveryDir, name),
9130
+ path: path4.join(this.recoveryDir, name),
9031
9131
  digest: match[3]
9032
9132
  });
9033
9133
  }
@@ -9220,7 +9320,8 @@ class TaskStore {
9220
9320
  const value = {
9221
9321
  ...workspace,
9222
9322
  writer: { ...workspace.writer, state: "uncertain" },
9223
- revision: newRevision()
9323
+ revision: newRevision(),
9324
+ runtime: stampNew(workspace)
9224
9325
  };
9225
9326
  this.replaceSnapshot(this.workspacePath, value, workspace);
9226
9327
  }
@@ -9298,7 +9399,7 @@ class TaskStore {
9298
9399
  }
9299
9400
  fs4.renameSync(temporary, file);
9300
9401
  temporary = undefined;
9301
- this.fsyncDirectory(path3.dirname(file));
9402
+ this.fsyncDirectory(path4.dirname(file));
9302
9403
  return success(null, null, value);
9303
9404
  } catch (error) {
9304
9405
  return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
@@ -9312,9 +9413,9 @@ class TaskStore {
9312
9413
  }
9313
9414
  }
9314
9415
  saveRecovery(file, bytes) {
9315
- const target = path3.basename(file) === "workspace.json" ? "workspace" : "task";
9316
- const id = target === "task" ? path3.basename(file, ".json") : "workspace";
9317
- const destination = path3.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
9416
+ const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
9417
+ const id = target === "task" ? path4.basename(file, ".json") : "workspace";
9418
+ const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
9318
9419
  if (fs4.existsSync(destination)) {
9319
9420
  if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
9320
9421
  return;
@@ -9392,7 +9493,7 @@ class TaskStore {
9392
9493
  for (const name of fs4.readdirSync(this.tasksDir)) {
9393
9494
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
9394
9495
  return null;
9395
- const parsed = this.parseBytes(fs4.readFileSync(path3.join(this.tasksDir, name)), taskRecordSchema);
9496
+ const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
9396
9497
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
9397
9498
  return null;
9398
9499
  ids.add(parsed.data.workspaceId);
@@ -9434,7 +9535,12 @@ class TaskStore {
9434
9535
  if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
9435
9536
  return failure("unsupported_version", "unsupported snapshot schema version");
9436
9537
  const parsed = schema.safeParse(value);
9437
- return parsed.success ? success(null, null, parsed.data) : failure("recovery_required", "snapshot does not satisfy its schema");
9538
+ if (parsed.success)
9539
+ return success(null, null, parsed.data);
9540
+ const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
9541
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
9542
+ return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
9543
+ return failure("recovery_required", "snapshot does not satisfy its schema");
9438
9544
  }
9439
9545
  processStart(pid) {
9440
9546
  try {
@@ -9444,43 +9550,98 @@ class TaskStore {
9444
9550
  }
9445
9551
  }
9446
9552
  conflict(expected, actual) {
9447
- return failure("revision_conflict", "snapshot revision does not match", {
9553
+ return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
9448
9554
  expectedRevision: expected,
9449
9555
  actualRevision: actual
9450
9556
  });
9451
9557
  }
9452
9558
  taskPath(taskId) {
9453
- return path3.join(this.tasksDir, `${taskId}.json`);
9559
+ return path4.join(this.tasksDir, `${taskId}.json`);
9454
9560
  }
9455
9561
  get workitDir() {
9456
- return path3.join(this.root, ".workit");
9562
+ return path4.join(this.root, ".workit");
9457
9563
  }
9458
9564
  get tasksDir() {
9459
- return path3.join(this.workitDir, "tasks");
9565
+ return path4.join(this.workitDir, "tasks");
9460
9566
  }
9461
9567
  get recoveryDir() {
9462
- return path3.join(this.workitDir, "recovery");
9568
+ return path4.join(this.workitDir, "recovery");
9463
9569
  }
9464
9570
  get workspacePath() {
9465
- return path3.join(this.workitDir, "workspace.json");
9571
+ return path4.join(this.workitDir, "workspace.json");
9466
9572
  }
9467
9573
  get lockPath() {
9468
- return path3.join(this.workitDir, "metadata.lock");
9574
+ return path4.join(this.workitDir, "metadata.lock");
9469
9575
  }
9470
9576
  get gitignorePath() {
9471
- return path3.join(this.workitDir, ".gitignore");
9577
+ return path4.join(this.workitDir, ".gitignore");
9472
9578
  }
9473
9579
  }
9580
+ // packages/workit-core/src/core/workers.ts
9581
+ var same = (left, right) => {
9582
+ try {
9583
+ return canonicalJson(left) === canonicalJson(right);
9584
+ } catch {
9585
+ return false;
9586
+ }
9587
+ };
9588
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
9589
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
9590
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
9591
+ var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
9592
+ const code = char.charCodeAt(0);
9593
+ return code < 32 || code === 127;
9594
+ }) && !value.split(/[\\/]/).includes("..");
9595
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
9596
+ var workerIdOf = (value) => value.workerId ?? null;
9597
+ function assertProductWriteAllowed(input) {
9598
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
9599
+ return failure("invalid_input", "invalid product write path");
9600
+ if (!input.store)
9601
+ return failure("permission_denied", "write authorization requires core state");
9602
+ const task = input.store.readTask(input.task.id);
9603
+ if (!task.ok)
9604
+ return task;
9605
+ const workspace = input.store.readWorkspace();
9606
+ if (!workspace.ok)
9607
+ return workspace;
9608
+ if (!workspace.data)
9609
+ return failure("not_found", "workspace not found");
9610
+ const workspaceRecord = workspace.data;
9611
+ if (task.data.status !== "active")
9612
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
9613
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
9614
+ return failure("recovery_required", "task and workspace bindings are invalid");
9615
+ const writer = workspaceRecord.writer;
9616
+ if (!writer)
9617
+ return failure("permission_denied", "checkout has no writer owner");
9618
+ if (writer.state === "uncertain")
9619
+ return failure("recovery_required", "checkout writer requires recovery", {
9620
+ owner: writer.owner
9621
+ });
9622
+ const caller = callerValue(input.caller);
9623
+ const workerId = workerIdOf(input.caller);
9624
+ const owner = writer.owner;
9625
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
9626
+ return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
9627
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
9628
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
9629
+ return failure("permission_denied", "only implementers can own product writes");
9630
+ 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))))
9631
+ return failure("recovery_required", "worker session is not an active writer");
9632
+ return success(null, workspaceRecord.revision, owner);
9633
+ }
9634
+
9474
9635
  // packages/workit-core/src/core/task-evaluation.ts
9475
9636
  import { createHash as createHash4 } from "node:crypto";
9476
9637
  import * as fs6 from "node:fs";
9477
9638
  import { spawnSync } from "node:child_process";
9478
- import path5 from "node:path";
9639
+ import path6 from "node:path";
9479
9640
 
9480
9641
  // packages/workit-core/src/core/authority.ts
9481
9642
  import { createHash as createHash3 } from "node:crypto";
9482
9643
  import * as fs5 from "node:fs";
9483
- import path4 from "node:path";
9644
+ import path5 from "node:path";
9484
9645
  var verifiedAuthorities = new WeakMap;
9485
9646
  var trustedAuthority = (kind, provenance, expected, binding, caller) => {
9486
9647
  const token = {};
@@ -9607,20 +9768,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
9607
9768
  continue;
9608
9769
  if (!reference.digest)
9609
9770
  return failure("invalid_input", "document references require a byte digest");
9610
- const target = path4.resolve(root, reference.path);
9611
- if (target !== root && !target.startsWith(`${root}${path4.sep}`))
9612
- return failure("invalid_input", "document reference escapes checkout");
9771
+ const target = path5.resolve(root, reference.path);
9772
+ if (target !== root && !target.startsWith(`${root}${path5.sep}`))
9773
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
9774
+ fields: [
9775
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
9776
+ ]
9777
+ });
9613
9778
  try {
9614
- const relative = path4.relative(root, target);
9779
+ const relative = path5.relative(root, target);
9615
9780
  let current = root;
9616
- for (const segment of relative.split(path4.sep)) {
9781
+ for (const segment of relative.split(path5.sep)) {
9617
9782
  if (!segment)
9618
9783
  continue;
9619
- current = path4.join(current, segment);
9784
+ current = path5.join(current, segment);
9620
9785
  const stat = fs5.lstatSync(current);
9621
9786
  const real = fs5.realpathSync(current);
9622
- if (real !== root && !real.startsWith(`${root}${path4.sep}`))
9623
- return failure("invalid_input", "document reference escapes checkout");
9787
+ if (real !== root && !real.startsWith(`${root}${path5.sep}`))
9788
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
9789
+ fields: [
9790
+ {
9791
+ path: "contentRefs.path",
9792
+ reason: `${reference.path} resolves outside the checkout`
9793
+ }
9794
+ ]
9795
+ });
9624
9796
  if (stat.isSymbolicLink())
9625
9797
  return failure("invalid_input", "document reference uses a symlink");
9626
9798
  if (current === target && !stat.isFile())
@@ -9967,14 +10139,14 @@ var verifyDecisionContent = verifyContentRefs;
9967
10139
 
9968
10140
  // packages/workit-core/src/core/task-evaluation.ts
9969
10141
  var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
9970
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path5.sep}`);
9971
- var relative = (root, candidate) => path5.relative(root, candidate).split(path5.sep).join("/") || ".";
10142
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
10143
+ var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
9972
10144
  var canonicalPath = (value) => {
9973
10145
  if (typeof value !== "string" || !value)
9974
10146
  return null;
9975
10147
  if (value.includes("\\") || value.split("/").includes(".."))
9976
10148
  return null;
9977
- const normalized = path5.posix.normalize(value);
10149
+ const normalized = path6.posix.normalize(value);
9978
10150
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
9979
10151
  return null;
9980
10152
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -10022,14 +10194,14 @@ var gitPaths = (root) => {
10022
10194
  git: /not a git repository/i.test(stderr) ? false : true
10023
10195
  };
10024
10196
  }
10025
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")) : [];
10197
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
10026
10198
  const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
10027
10199
  if (stagedDeleted.status !== 0) {
10028
10200
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
10029
10201
  if (!/not a git repository/i.test(stderr))
10030
10202
  return { paths, uncertain: true, git: true };
10031
10203
  } else if (stagedDeleted.stdout) {
10032
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")));
10204
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
10033
10205
  }
10034
10206
  return { paths: [...new Set(paths)], uncertain: false, git: true };
10035
10207
  };
@@ -10044,7 +10216,7 @@ var walk = (root, directory, output) => {
10044
10216
  for (const name of names) {
10045
10217
  if (name === ".git" || name === ".workit")
10046
10218
  continue;
10047
- const target = path5.join(directory, name);
10219
+ const target = path6.join(directory, name);
10048
10220
  let stat;
10049
10221
  try {
10050
10222
  stat = fs6.lstatSync(target);
@@ -10062,17 +10234,17 @@ var walk = (root, directory, output) => {
10062
10234
  var scopeRoots = (root, scope) => {
10063
10235
  const roots = [];
10064
10236
  for (const item of scope.paths.length ? scope.paths : ["."]) {
10065
- const target = path5.resolve(root, item);
10237
+ const target = path6.resolve(root, item);
10066
10238
  if (!inside(root, target))
10067
- return failure("invalid_input", "candidate scope escapes checkout");
10239
+ 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` }] });
10068
10240
  let ancestor = target;
10069
10241
  while (!fs6.existsSync(ancestor) && ancestor !== root)
10070
- ancestor = path5.dirname(ancestor);
10242
+ ancestor = path6.dirname(ancestor);
10071
10243
  try {
10072
10244
  if (!inside(root, fs6.realpathSync(ancestor)))
10073
- return failure("invalid_input", "candidate scope escapes checkout");
10245
+ 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` }] });
10074
10246
  } catch {
10075
- return failure("invalid_input", "candidate scope cannot be inspected");
10247
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
10076
10248
  }
10077
10249
  roots.push(target);
10078
10250
  }
@@ -10087,7 +10259,7 @@ function captureCandidate(root, scope, environment = []) {
10087
10259
  }
10088
10260
  const normalizedScope = canonicalScope(scope);
10089
10261
  if (!normalizedScope)
10090
- return failure("invalid_input", "candidate scope is invalid");
10262
+ 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" }] });
10091
10263
  const roots = scopeRoots(checkout, normalizedScope);
10092
10264
  if (!roots.ok)
10093
10265
  return roots;
@@ -10119,7 +10291,7 @@ function captureCandidate(root, scope, environment = []) {
10119
10291
  for (const item of [...names].sort()) {
10120
10292
  if (!scopeMatches(item, normalizedScope))
10121
10293
  continue;
10122
- const target = path5.join(checkout, item);
10294
+ const target = path6.join(checkout, item);
10123
10295
  try {
10124
10296
  const stat = fs6.lstatSync(target);
10125
10297
  if (stat.isSymbolicLink()) {
@@ -10276,7 +10448,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10276
10448
  return true;
10277
10449
  const reviewSession = sessionFromRef(entry.data.reviewContext);
10278
10450
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
10279
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
10451
+ const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
10280
10452
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
10281
10453
  });
10282
10454
  if (passed.length) {
@@ -10372,7 +10544,17 @@ function evaluateClosure(requestedOutcome, view) {
10372
10544
  const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
10373
10545
  if (requestedOutcome !== "stopped" && blocking.length)
10374
10546
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
10375
- requirementIds: blocking.map((item) => item.requirementId)
10547
+ requirementIds: blocking.map((item) => item.requirementId),
10548
+ requirements: blocking.map((item) => {
10549
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
10550
+ return {
10551
+ requirementId: item.requirementId,
10552
+ ruleId: requirement?.ruleId ?? "unknown",
10553
+ reason: item.reason,
10554
+ satisfaction: requirement?.satisfaction ?? "",
10555
+ dependentAction: requirement?.dependentAction ?? null
10556
+ };
10557
+ })
10376
10558
  });
10377
10559
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
10378
10560
  if (requestedOutcome === "verified" && accepted.length)
@@ -10399,6 +10581,106 @@ function evaluateClosure(requestedOutcome, view) {
10399
10581
  });
10400
10582
  }
10401
10583
 
10584
+ // packages/workit-core/src/core/methods.ts
10585
+ var METHODS = {
10586
+ "workit-challenge": { dimensions: ["challenge", "decisions"] },
10587
+ "workit-behavioral-tdd": { dimensions: ["testing"] },
10588
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
10589
+ "workit-plan": { dimensions: ["artifacts", "continuity"] },
10590
+ "workit-implement": { dimensions: ["delegation"] },
10591
+ "workit-debug": { ruleIds: ["root-cause-investigation"] },
10592
+ "workit-handoff": { ruleIds: ["durable-handoff"] },
10593
+ "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
10594
+ };
10595
+ var methodMatches = (id, definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true && !(id === "workit-review" && requirement.ruleId === "self-review") || definition.ruleIds?.includes(requirement.ruleId) === true;
10596
+ var assuranceFor = (id, definition, capabilities) => {
10597
+ const keys = new Set([
10598
+ id,
10599
+ ...definition.dimensions ?? [],
10600
+ ...definition.ruleIds ?? []
10601
+ ]);
10602
+ const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
10603
+ if (id === "workit-review" && matches.length === 0)
10604
+ return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
10605
+ if (matches.some((capability) => capability.assurance === "unavailable"))
10606
+ return {
10607
+ assurance: "unavailable",
10608
+ reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
10609
+ };
10610
+ if (matches.some((capability) => capability.assurance === "enforced"))
10611
+ return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
10612
+ return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
10613
+ };
10614
+ function selectMethods(policy, capabilities) {
10615
+ return Object.entries(METHODS).flatMap(([id, definition]) => {
10616
+ const requirements = policy.requirements.filter((requirement) => methodMatches(id, definition, requirement));
10617
+ if (requirements.length === 0)
10618
+ return [];
10619
+ const capability = assuranceFor(id, definition, capabilities);
10620
+ const reasons = [
10621
+ ...new Set(requirements.map((requirement) => requirement.reason)),
10622
+ ...capability.reasons
10623
+ ];
10624
+ if (capability.assurance === "unavailable")
10625
+ reasons.unshift("required capability is unavailable");
10626
+ return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
10627
+ });
10628
+ }
10629
+ var invariantBootstrap = () => `
10630
+ Workit keeps one accountable lead and one shared task state. Inspect current task
10631
+ state before acting. An empty task.list means no session yet, not permission to
10632
+ skip Workit; for user-requested product, debug, or behavior work with no active
10633
+ or paused task, run task.start then policy.assess before other product mutations.
10634
+ Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
10635
+ to the current records; explicit values are still concurrency-checked, so never
10636
+ copy revisions between calls. Happy path: task.list, task.start {intent} with no
10637
+ revisions, policy.assess {assessment: {facts, signals, consequences,
10638
+ verification}} where facts are inferred or observed with file refs,
10639
+ writer.acquire {taskId} before product writes, evidence.record {evidence} where
10640
+ check and review kinds auto-bind the current tree and need no digests. Record
10641
+ self-review requirements with kind review and reviewContext matching your own
10642
+ host session; the lead may also record checks. Fresh-context review requires a
10643
+ session distinct from the task creator and every other evidence recorder. Record
10644
+ verification evidence last: later evidence or commits can reopen fixed findings
10645
+ for re-verification, so verify, resolve, then close with nothing in between.
10646
+ decision.record {binding with taskId/workspaceId from inspect plus presented and
10647
+ approvedContent} only through a native approval question asked receipt-shaped:
10648
+ header Workit decision: <purpose>, the same label repeated in the question
10649
+ text (some host UIs do not render headers), exactly two options approved and
10650
+ rejected; ask once, never re-ask to mint a receipt. Before asking, present the
10651
+ item in the conversation (durable artifacts: complete digest plus exact path;
10652
+ inline plans: the content) and keep the question to one short scoped sentence;
10653
+ never ask to approve something unseen. An answer with custom text is steering,
10654
+ not approval: adjust, re-present, then ask a new scoped question. After the user
10655
+ approves a spec or plan, execute the remaining tasks continuously with one
10656
+ atomic commit per task and do not ask whether to continue; stop only for a new
10657
+ product decision, a failed safety or verification gate, a conflicting edit, or
10658
+ missing authority. Record the plan's commit list once through the plan-scoped
10659
+ action approval (git.commit with plan_steps and plan_branch) so each listed
10660
+ commit executes without a new question; an unlisted message needs a fresh
10661
+ approval. task.close {outcome,
10662
+ summary, decisionIds}. A completed native subagent run stops its bound worker
10663
+ by itself; if a worker strands in cancelling with its run verifiably over,
10664
+ repeat worker.cancel {reason} to confirm the stop. Check and review evidence
10665
+ without a bound candidate goes stale; findings close only as fixed with passing
10666
+ verification, dismissed with supporting evidence, or deferred under an approved
10667
+ limitation. Use only the
10668
+ shared operations for task, policy, evidence, finding, decision, worker, writer,
10669
+ and state changes. Authority is bounded by the requested scope, current
10670
+ revision, caller/session provenance, and observed capabilities. Never claim host
10671
+ enforcement or evidence that the host cannot provide. Shell-executed writes are
10672
+ unattested agent-guided work even when a writer is held. Preserve unresolved
10673
+ requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
10674
+ always load on demand. Model-invoked: new instructions, interruptions, or
10675
+ forgotten items mid-task → load workit-steer before responding; about to open
10676
+ a PR or fresh from implementing → load workit-deslop; CI red → load
10677
+ workit-green-run; small-looking change before close or merge → load
10678
+ workit-blast-radius; ambiguous, consequential, or disputed proposal before
10679
+ recommending → load workit-challenge; spec or plan needs a flow diagram →
10680
+ load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
10681
+ act from memory of it.
10682
+ `.trim();
10683
+
10402
10684
  // packages/workit-core/src/core/task-context.ts
10403
10685
  function reconcileResume(view, observations = []) {
10404
10686
  if (observations.length > 0)
@@ -10408,7 +10690,7 @@ function reconcileResume(view, observations = []) {
10408
10690
  return candidate;
10409
10691
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
10410
10692
  const blockers = [...view.task.progress.blockers];
10411
- if (view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
10693
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
10412
10694
  blockers.push({
10413
10695
  reason: "worker state requires reconciliation",
10414
10696
  dependentAction: "resume",
@@ -10458,6 +10740,11 @@ function compactTaskContext(view) {
10458
10740
  digest: entry.data.digest,
10459
10741
  references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
10460
10742
  }));
10743
+ const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
10744
+ id: method.id,
10745
+ assurance: method.assurance,
10746
+ reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
10747
+ })) : [];
10461
10748
  const gaps = Array.from(new Set([
10462
10749
  ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
10463
10750
  ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
@@ -10467,15 +10754,18 @@ function compactTaskContext(view) {
10467
10754
  objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
10468
10755
  status: view.task.status,
10469
10756
  decisions,
10757
+ methods,
10470
10758
  gaps,
10471
10759
  nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
10472
10760
  };
10473
10761
  let encoded = JSON.stringify(context);
10474
- while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.gaps.length)) {
10762
+ while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
10475
10763
  if (context.gaps.length)
10476
10764
  context.gaps.pop();
10477
- else
10765
+ else if (context.decisions.length)
10478
10766
  context.decisions.pop();
10767
+ else
10768
+ context.methods.pop();
10479
10769
  encoded = JSON.stringify(context);
10480
10770
  }
10481
10771
  if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
@@ -10491,51 +10781,49 @@ var exportDigest = (bundle) => sha256({
10491
10781
  sourceWorkspaceId: bundle.sourceWorkspaceId,
10492
10782
  task: bundle.task
10493
10783
  });
10494
- // packages/workit-core/src/core/methods.ts
10495
- var invariantBootstrap = () => `
10496
- Workit keeps one accountable lead and one shared task state. Inspect current task
10497
- state before acting. An empty task.list means no session yet, not permission to
10498
- skip Workit; for user-requested product, debug, or behavior work with no active
10499
- or paused task, run task.start then policy.assess before other product mutations.
10500
- Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
10501
- to the current records; explicit values are still concurrency-checked, so never
10502
- copy revisions between calls. Happy path: task.list, task.start {intent} with no
10503
- revisions, policy.assess {assessment: {facts, signals, consequences,
10504
- verification}} where facts are inferred or observed with file refs,
10505
- writer.acquire {taskId} before product writes, evidence.record {evidence} where
10506
- check and review kinds auto-bind the current tree and need no digests. Record
10507
- self-review requirements with kind review and reviewContext matching your own
10508
- host session; the lead may also record checks. Fresh-context review requires a
10509
- session distinct from the task creator and every other evidence recorder. Record
10510
- verification evidence last: later evidence or commits can reopen fixed findings
10511
- for re-verification, so verify, resolve, then close with nothing in between.
10512
- decision.record {binding with taskId/workspaceId from inspect plus presented and
10513
- approvedContent} only through a native approval question asked receipt-shaped:
10514
- header Workit decision: <purpose>, the same label repeated in the question
10515
- text (some host UIs do not render headers), exactly two options approved and
10516
- rejected; ask once, never re-ask to mint a receipt. task.close {outcome,
10517
- summary, decisionIds}. A completed native subagent run stops its bound worker
10518
- by itself; if a worker strands in cancelling with its run verifiably over,
10519
- repeat worker.cancel {reason} to confirm the stop. Check and review evidence
10520
- without a bound candidate goes stale; findings close only as fixed with passing
10521
- verification, dismissed with supporting evidence, or deferred under an approved
10522
- limitation. Use only the
10523
- shared operations for task, policy, evidence, finding, decision, worker, writer,
10524
- and state changes. Authority is bounded by the requested scope, current
10525
- revision, caller/session provenance, and observed capabilities. Never claim host
10526
- enforcement or evidence that the host cannot provide. Shell-executed writes are
10527
- unattested agent-guided work even when a writer is held. Preserve unresolved
10528
- requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
10529
- always load on demand. Model-invoked: new instructions, interruptions, or
10530
- forgotten items mid-task → load workit-steer before responding; about to open
10531
- a PR or fresh from implementing → load workit-deslop; CI red → load
10532
- workit-green-run; small-looking change before close or merge → load
10533
- workit-blast-radius; ambiguous, consequential, or disputed proposal before
10534
- recommending → load workit-challenge; spec or plan needs a flow diagram →
10535
- load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
10536
- act from memory of it.
10537
- `.trim();
10784
+ // packages/workit-core/src/core/route-intent.ts
10785
+ var GUIDANCE = {
10786
+ "git.branch_setup": "use the Workit git.branch_setup action (workit_external_action in hosts, or `workit action git.branch_setup --payload '{}' --confirm --json` in the CLI)",
10787
+ "hosting.pull_request": "use the Workit hosting.pull_request action (workit_external_action in hosts, or `workit action hosting.pull_request --payload '{...}' --confirm --json` in the CLI), then drive the created PR through workit-babysit"
10788
+ };
10789
+ var unbalancedQuotes = (segment) => {
10790
+ const text = segment.replace(/\\./gu, "");
10791
+ return (text.match(/"/gu)?.length ?? 0) % 2 !== 0 || (text.match(/'/gu)?.length ?? 0) % 2 !== 0;
10792
+ };
10793
+ var segmentTokens = (segment) => {
10794
+ if (segment.includes("`") || segment.includes("$("))
10795
+ return null;
10796
+ if (unbalancedQuotes(segment))
10797
+ return null;
10798
+ const parts = segment.trim().split(/\s+/u);
10799
+ while (parts.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/u.test(parts[0]))
10800
+ parts.shift();
10801
+ return parts.length > 0 ? parts : null;
10802
+ };
10803
+ var unquote = (token) => token.replace(/^['"]|['"]$/gu, "");
10804
+ function shellRouteIntent(command) {
10805
+ if (typeof command !== "string" || command.trim() === "")
10806
+ return null;
10807
+ for (const raw of command.split(/&&|\|\||;|\n/u)) {
10808
+ const parts = segmentTokens(raw);
10809
+ if (!parts)
10810
+ continue;
10811
+ const head = unquote(parts[0]);
10812
+ const second = unquote(parts[1] ?? "");
10813
+ const third = unquote(parts[2] ?? "");
10814
+ if (head === "git" && second === "switch" && (third === "-c" || third === "--create"))
10815
+ return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
10816
+ if (head === "git" && second === "checkout" && (third === "-b" || third === "-B"))
10817
+ return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
10818
+ if (head === "gh" && second === "pr" && third === "create")
10819
+ return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
10820
+ if (head === "glab" && second === "mr" && third === "create")
10821
+ return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
10822
+ }
10823
+ return null;
10824
+ }
10538
10825
  // packages/workit-core/src/core/external-action.ts
10826
+ import { execFileSync } from "node:child_process";
10539
10827
  var externalActionSchema = discriminatedUnion("operation", [
10540
10828
  object({
10541
10829
  operation: literal("git.branch_setup"),
@@ -10548,7 +10836,11 @@ var externalActionSchema = discriminatedUnion("operation", [
10548
10836
  }).strict(),
10549
10837
  object({
10550
10838
  operation: literal("git.commit"),
10551
- payload: object({ message: string2().min(1) }).strict()
10839
+ payload: object({
10840
+ message: string2().min(1).optional(),
10841
+ plan_steps: array(string2().min(1)).optional(),
10842
+ plan_branch: string2().min(1).optional()
10843
+ }).strict()
10552
10844
  }).strict(),
10553
10845
  object({
10554
10846
  operation: literal("git.push"),
@@ -10637,7 +10929,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
10637
10929
  payload,
10638
10930
  ...payload && typeof payload === "object" && !Array.isArray(payload) && "resolved" in payload && payload.resolved && typeof payload.resolved === "object" && Array.isArray(payload.resolved.steps) ? { steps: payload.resolved.steps } : {}
10639
10931
  });
10640
- var externalActionHelp = "Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch?,stash?}; git.commit {message}; git.push {branch?}; hosting.pull_request {title,body?,draft?,target_branch?,babysit?}; youtrack.update {issueId,markdown,minutes?}; youtrack.time {issueId,minutes,text?,dateMs?}; youtrack.meeting {issueId,minutes,text}; changelog.apply {entries?,path?,normalize_only?}; context.read {kind,range?,issueId?,issueUrl?,issueRef?,mode?,specPath?,planPath?}. context.read is read-only and needs no approval; all other operations require native approval (CLI uses a TTY; caller-unattested MCP cannot mutate).";
10932
+ var externalActionHelp = "Fixed actions: git.branch_setup {action?,sdd_dir?,target_branch(required unless reapply_stash; the working branch to create or switch to),stash?}; git.commit {message}; git.push {branch?}; hosting.pull_request {title,body?,draft?,target_branch?,babysit?}; youtrack.update {issueId,markdown,minutes?}; youtrack.time {issueId,minutes,text?,dateMs?}; youtrack.meeting {issueId,minutes,text}; changelog.apply {entries?,path?,normalize_only?}; context.read {kind,range?,issueId?,issueUrl?,issueRef?,mode?,specPath?,planPath?}. context.read is read-only and needs no approval; all other operations require native approval (CLI uses a TTY; caller-unattested MCP cannot mutate).";
10641
10933
  var externalActionRef = (host, actor, descriptor) => ({
10642
10934
  kind: "host",
10643
10935
  host,
@@ -10688,6 +10980,92 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
10688
10980
  return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
10689
10981
  return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
10690
10982
  };
10983
+ var planCommitDescriptor = (content) => {
10984
+ try {
10985
+ const value = JSON.parse(content);
10986
+ if (value.operation !== "git.commit" || !value.payload)
10987
+ return null;
10988
+ const steps = value.payload.plan_steps;
10989
+ const branch = value.payload.plan_branch;
10990
+ if (!Array.isArray(steps) || steps.some((step) => typeof step !== "string" || !step) || typeof branch !== "string" || !branch)
10991
+ return null;
10992
+ return { steps, branch };
10993
+ } catch {
10994
+ return null;
10995
+ }
10996
+ };
10997
+ var approvedPlanCommit = (store, host, actor, message) => {
10998
+ if (typeof message !== "string" || !message)
10999
+ return failure("invalid_input", "commit message is required");
11000
+ const listed = store.listTasks();
11001
+ const workspace = store.readWorkspace();
11002
+ if (!listed.ok || !workspace.ok)
11003
+ return failure("storage_error", "external action state is unavailable");
11004
+ if (!workspace.data)
11005
+ return failure("not_found", "workspace not found");
11006
+ const matches = listed.data.flatMap((task) => {
11007
+ if (task.status !== "active" || task.workspaceId !== workspace.data.id)
11008
+ return [];
11009
+ if (task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
11010
+ return [];
11011
+ return task.decisions.flatMap((entry) => {
11012
+ const decision = entry.data;
11013
+ if (decision.purpose !== "action" || decision.response !== "approved" || decision.revoked !== null)
11014
+ return [];
11015
+ if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
11016
+ return [];
11017
+ const plan = planCommitDescriptor(decision.binding.approvedContent);
11018
+ if (!plan)
11019
+ return [];
11020
+ const completed = task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ?? [];
11021
+ if (plan.steps[completed.length] !== message)
11022
+ return [];
11023
+ return [{ task, entry, steps: plan.steps, branch: plan.branch, completed: [...completed] }];
11024
+ });
11025
+ });
11026
+ if (matches.length !== 1)
11027
+ return failure("permission_denied", matches.length === 0 ? "no plan commit authorization matches this message" : "multiple plan authorizations match this message");
11028
+ return success(workspace.data.revision, null, matches[0]);
11029
+ };
11030
+ var commitMessageFromDescriptor = (operation) => {
11031
+ try {
11032
+ const descriptor = JSON.parse(operation);
11033
+ return descriptor.operation === "git.commit" && typeof descriptor.payload?.message === "string" ? descriptor.payload.message : undefined;
11034
+ } catch {
11035
+ return;
11036
+ }
11037
+ };
11038
+ var planCommitBinding = (store, host, actor, operation) => {
11039
+ const message = commitMessageFromDescriptor(operation);
11040
+ if (!message)
11041
+ return null;
11042
+ const plan = approvedPlanCommit(store, host, actor, message);
11043
+ if (!plan.ok)
11044
+ return null;
11045
+ let branch;
11046
+ try {
11047
+ branch = execFileSync("git", ["branch", "--show-current"], {
11048
+ cwd: store.root,
11049
+ encoding: "utf8",
11050
+ stdio: ["pipe", "pipe", "pipe"]
11051
+ }).trim();
11052
+ } catch {
11053
+ return null;
11054
+ }
11055
+ if (!branch || branch !== plan.data.branch)
11056
+ return null;
11057
+ const workspace = store.readWorkspace();
11058
+ if (!workspace.ok || !workspace.data)
11059
+ return null;
11060
+ return {
11061
+ taskId: plan.data.task.id,
11062
+ decisionId: plan.data.entry.id,
11063
+ binding: plan.data.entry.data.binding,
11064
+ expectedRevision: plan.data.task.revision,
11065
+ expectedWorkspaceRevision: workspace.data.revision,
11066
+ step: message
11067
+ };
11068
+ };
10691
11069
  var approvedExternalAction = (store, host, actor, operation) => {
10692
11070
  const selected = externalActionState(store, host, actor, operation);
10693
11071
  if (!selected.ok)
@@ -10696,6 +11074,23 @@ var approvedExternalAction = (store, host, actor, operation) => {
10696
11074
  return failure(selected.data.entry.data.consumption.state === "uncertain" ? "external_outcome_unknown" : "permission_denied", selected.data.entry.data.consumption.state === "uncertain" ? "previous external action outcome is unknown" : "external action was already settled");
10697
11075
  return selected;
10698
11076
  };
11077
+ var priorResolvedDrift = (store, host, actor, operation, payload, currentResolved) => {
11078
+ const prior = priorExternalAction(store, host, actor, operation, payload);
11079
+ if (!prior.ok)
11080
+ return success(null, null, null);
11081
+ let baseline;
11082
+ try {
11083
+ const parsed = JSON.parse(prior.data.entry.data.binding.approvedContent);
11084
+ baseline = parsed.payload?.resolved;
11085
+ } catch {
11086
+ return success(null, null, null);
11087
+ }
11088
+ if (baseline === undefined || baseline === null)
11089
+ return success(null, null, null);
11090
+ if (canonicalJson(baseline) === canonicalJson(currentResolved))
11091
+ return success(null, null, null);
11092
+ return failure("invalid_input", "repository state changed since approval; show the current state and approve the action again", { outcome: "not_started", operation });
11093
+ };
10699
11094
  var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
10700
11095
  let steps = [];
10701
11096
  try {
@@ -10973,7 +11368,7 @@ function resolveRequirements(input) {
10973
11368
  ].join(", ")}`,
10974
11369
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
10975
11370
  before: "dependent_action",
10976
- dependentAction: "dependent-action",
11371
+ dependentAction: null,
10977
11372
  acceptanceAllowed: false
10978
11373
  }));
10979
11374
  if (signals.productChoiceOpen.value === true)
@@ -10984,7 +11379,7 @@ function resolveRequirements(input) {
10984
11379
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
10985
11380
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
10986
11381
  before: "dependent_action",
10987
- dependentAction: "dependent-action",
11382
+ dependentAction: null,
10988
11383
  acceptanceAllowed: false
10989
11384
  }));
10990
11385
  if (signals.behaviorChange.value === true)
@@ -11142,59 +11537,6 @@ function diffPolicy(previous, next, reason, now) {
11142
11537
  return parsed.data;
11143
11538
  }
11144
11539
 
11145
- // packages/workit-core/src/core/workers.ts
11146
- var same = (left, right) => {
11147
- try {
11148
- return canonicalJson(left) === canonicalJson(right);
11149
- } catch {
11150
- return false;
11151
- }
11152
- };
11153
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
11154
- var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
11155
- const code = char.charCodeAt(0);
11156
- return code < 32 || code === 127;
11157
- }) && !value.split(/[\\/]/).includes("..");
11158
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
11159
- var workerIdOf = (value) => value.workerId ?? null;
11160
- function assertProductWriteAllowed(input) {
11161
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
11162
- return failure("invalid_input", "invalid product write path");
11163
- if (!input.store)
11164
- return failure("permission_denied", "write authorization requires core state");
11165
- const task = input.store.readTask(input.task.id);
11166
- if (!task.ok)
11167
- return task;
11168
- const workspace = input.store.readWorkspace();
11169
- if (!workspace.ok)
11170
- return workspace;
11171
- if (!workspace.data)
11172
- return failure("not_found", "workspace not found");
11173
- const workspaceRecord = workspace.data;
11174
- if (task.data.status !== "active")
11175
- return failure("invalid_transition", "paused or closed tasks cannot own product writes");
11176
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
11177
- return failure("recovery_required", "task and workspace bindings are invalid");
11178
- const writer = workspaceRecord.writer;
11179
- if (!writer)
11180
- return failure("permission_denied", "checkout has no writer owner");
11181
- if (writer.state === "uncertain")
11182
- return failure("recovery_required", "checkout writer requires recovery", {
11183
- owner: writer.owner
11184
- });
11185
- const caller = callerValue(input.caller);
11186
- const workerId = workerIdOf(input.caller);
11187
- const owner = writer.owner;
11188
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
11189
- return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
11190
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
11191
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
11192
- return failure("permission_denied", "only implementers can own product writes");
11193
- 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))))
11194
- return failure("recovery_required", "worker session is not an active writer");
11195
- return success(null, workspaceRecord.revision, owner);
11196
- }
11197
-
11198
11540
  // packages/workit-core/src/core/task-engine.ts
11199
11541
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
11200
11542
  kind,
@@ -11313,10 +11655,10 @@ var applyWorkerLifecycle = (input) => {
11313
11655
  const notStarted = input.dispatch === true;
11314
11656
  if (notStarted && (input.state !== "stopped" || input.session !== null))
11315
11657
  return failure("invalid_input", "a never-dispatched worker stops with no session");
11316
- if (notStarted && (entry.data.state === "stopped" || entry.data.state === "unknown"))
11317
- return failure("invalid_transition", "worker is no longer awaiting dispatch");
11318
- if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
11658
+ if (notStarted && entry.data.session !== null)
11319
11659
  return failure("invalid_transition", "worker already has an observed session");
11660
+ if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
11661
+ return failure("invalid_transition", "worker launch was not claimed");
11320
11662
  if ((input.state === "running" || input.state === "cancelling" || input.state === "stopped" && !notStarted) && !input.session)
11321
11663
  return failure("invalid_input", "running worker observations require a session");
11322
11664
  if (entry.data.session && !sameValue(entry.data.session, input.session))
@@ -11325,15 +11667,20 @@ var applyWorkerLifecycle = (input) => {
11325
11667
  return failure("invalid_transition", "worker has not started");
11326
11668
  if (input.state === "running" && entry.data.state === "stopped")
11327
11669
  return failure("invalid_transition", "stopped worker cannot run again");
11670
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
11328
11671
  const nextEntry = {
11329
11672
  ...entry,
11330
11673
  recordedAt: input.now ?? entry.recordedAt,
11331
11674
  provenance: authority.provenance,
11332
- data: { ...entry.data, state: input.state, session: input.session }
11675
+ data: {
11676
+ ...entry.data,
11677
+ state: terminalCancel ? "cancelling" : input.state,
11678
+ session: input.session
11679
+ }
11333
11680
  };
11334
11681
  const shouldClear = input.state === "stopped" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
11335
11682
  const shouldUncertain = input.state === "unknown" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
11336
- if (entry.data.state === input.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
11683
+ if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
11337
11684
  return success(task.data.revision, workspace.data.revision, entry);
11338
11685
  const changed = input.store.mutateTaskAndWorkspace({
11339
11686
  taskId: task.data.id,
@@ -11703,7 +12050,7 @@ class WorkitCore {
11703
12050
  activeWorkerBlocker(task, workspace) {
11704
12051
  if (workspace.writer)
11705
12052
  return failure("recovery_required", "writer ownership must be released first");
11706
- if (task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
12053
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
11707
12054
  return failure("recovery_required", "worker state requires reconciliation");
11708
12055
  return null;
11709
12056
  }
@@ -11940,11 +12287,6 @@ class WorkitCore {
11940
12287
  if (helper && !helper.ok)
11941
12288
  return helper;
11942
12289
  const evidence = input.evidence;
11943
- if (helper?.ok) {
11944
- const assignment = helper.data.data.assignment;
11945
- 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))
11946
- return failure("permission_denied", "evidence is outside the worker assignment");
11947
- }
11948
12290
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
11949
12291
  return failure("invalid_input", "missing or skipped evidence requires a reason");
11950
12292
  if (evidence.kind === "review") {
@@ -11961,6 +12303,11 @@ class WorkitCore {
11961
12303
  evidence.beforeCandidateId ??= null;
11962
12304
  evidence.candidateId ??= null;
11963
12305
  }
12306
+ if (helper?.ok) {
12307
+ const assignment = helper.data.data.assignment;
12308
+ 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))
12309
+ return failure("permission_denied", "evidence is outside the worker assignment");
12310
+ }
11964
12311
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
11965
12312
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
11966
12313
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -12467,7 +12814,12 @@ class WorkitCore {
12467
12814
  return failure("invalid_transition", "worker is not awaiting dispatch");
12468
12815
  return success(mutation.revision, null, {
12469
12816
  ...current,
12470
- workers: current.workers.map((item) => item.id === input.workerId ? { ...item, recordedAt: mutation.now, provenance: attested.data } : item)
12817
+ workers: current.workers.map((item) => item.id === input.workerId ? {
12818
+ ...item,
12819
+ recordedAt: mutation.now,
12820
+ provenance: attested.data,
12821
+ data: { ...item.data, state: "dispatching" }
12822
+ } : item)
12471
12823
  });
12472
12824
  }
12473
12825
  });
@@ -12851,6 +13203,9 @@ class WorkitCore {
12851
13203
  id: task.id,
12852
13204
  revision: task.revision,
12853
13205
  workspaceRevision: workspace.data.revision,
13206
+ createdAt: task.createdAt,
13207
+ updatedAt: task.updatedAt,
13208
+ runtime: task.runtime ?? null,
12854
13209
  objective: task.intent.data.objective,
12855
13210
  status: task.status,
12856
13211
  closure: task.closure,
@@ -12936,7 +13291,7 @@ class WorkitCore {
12936
13291
  const base = reconcileResume(effectiveView);
12937
13292
  if (!base.ok)
12938
13293
  return base;
12939
- const needsReconciliation = view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
13294
+ const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
12940
13295
  const blockers = [...base.data.blockers];
12941
13296
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
12942
13297
  blockers.push({
@@ -13003,7 +13358,8 @@ class WorkitCore {
13003
13358
  ...current,
13004
13359
  status,
13005
13360
  candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
13006
- progress: status === "paused" ? { ...current.progress, summary: input.reason } : current.progress
13361
+ progress: current.progress,
13362
+ pauseReason: status === "paused" ? input.reason ?? null : null
13007
13363
  })
13008
13364
  });
13009
13365
  if (!changed.ok)
@@ -13039,7 +13395,7 @@ class WorkitCore {
13039
13395
  return workspace;
13040
13396
  if (!workspace.data)
13041
13397
  return failure("not_found", "workspace not found");
13042
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
13398
+ if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
13043
13399
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
13044
13400
  this.fillRevisions(input, task.data, workspace.data);
13045
13401
  const changed = this.store.mutateTaskAndWorkspace({
@@ -13157,15 +13513,15 @@ function resolveGitRevision(root, value) {
13157
13513
  }
13158
13514
  function resolveInside(root, candidate) {
13159
13515
  const base = realpathSync5(root);
13160
- const target = path6.resolve(base, candidate);
13161
- if (target !== base && !target.startsWith(base + path6.sep)) {
13516
+ const target = path7.resolve(base, candidate);
13517
+ if (target !== base && !target.startsWith(base + path7.sep)) {
13162
13518
  throw new Error("path must stay inside repository root");
13163
13519
  }
13164
13520
  let ancestor = target;
13165
- while (!existsSync3(ancestor))
13166
- ancestor = path6.dirname(ancestor);
13521
+ while (!existsSync4(ancestor))
13522
+ ancestor = path7.dirname(ancestor);
13167
13523
  const canonicalAncestor = realpathSync5(ancestor);
13168
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path6.sep)) {
13524
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
13169
13525
  throw new Error("path must stay inside repository root");
13170
13526
  }
13171
13527
  return target;
@@ -13207,7 +13563,7 @@ if (false) {}
13207
13563
  // packages/workit-core/src/core/logger.ts
13208
13564
  import { appendFileSync, mkdirSync as mkdirSync2, readdirSync as readdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
13209
13565
  import os from "node:os";
13210
- import path7 from "node:path";
13566
+ import path8 from "node:path";
13211
13567
  var REDACTED = "[REDACTED]";
13212
13568
  var DEFAULT_MAX_RATE = 20;
13213
13569
  var DEFAULT_RATE_WINDOW_MS = 1000;
@@ -13263,14 +13619,14 @@ var resolveStateDir = () => {
13263
13619
  return override;
13264
13620
  const home = os.homedir();
13265
13621
  if (process.env.XDG_STATE_HOME)
13266
- return path7.join(process.env.XDG_STATE_HOME, "workit");
13622
+ return path8.join(process.env.XDG_STATE_HOME, "workit");
13267
13623
  switch (os.platform()) {
13268
13624
  case "darwin":
13269
- return path7.join(home, "Library", "Application Support", "workit");
13625
+ return path8.join(home, "Library", "Application Support", "workit");
13270
13626
  case "win32":
13271
- return path7.join(process.env.LOCALAPPDATA ?? path7.join(home, "AppData", "Local"), "workit");
13627
+ return path8.join(process.env.LOCALAPPDATA ?? path8.join(home, "AppData", "Local"), "workit");
13272
13628
  default:
13273
- return path7.join(process.env.HOME ?? home, ".local", "state", "workit");
13629
+ return path8.join(process.env.HOME ?? home, ".local", "state", "workit");
13274
13630
  }
13275
13631
  };
13276
13632
  var homePattern = new RegExp(`${escapeRegExp(os.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
@@ -13343,14 +13699,14 @@ var pruneOldFiles = (dir) => {
13343
13699
  const files = readdirSync3(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
13344
13700
  for (const file of files.slice(RETAINED_DAYS)) {
13345
13701
  try {
13346
- unlinkSync2(path7.join(dir, file));
13702
+ unlinkSync2(path8.join(dir, file));
13347
13703
  } catch {}
13348
13704
  }
13349
13705
  } catch {}
13350
13706
  };
13351
13707
  var createLogger = (options = {}) => {
13352
13708
  const stateDir = options.stateDir ?? resolveStateDir();
13353
- const logDir = path7.join(stateDir, "logs");
13709
+ const logDir = path8.join(stateDir, "logs");
13354
13710
  const now = options.now ?? (() => new Date);
13355
13711
  const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
13356
13712
  const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
@@ -13384,7 +13740,7 @@ var createLogger = (options = {}) => {
13384
13740
  const line = `${JSON.stringify(event)}
13385
13741
  `;
13386
13742
  mkdirSync2(logDir, { recursive: true, mode: 448 });
13387
- appendFileSync(path7.join(logDir, dailyFileName(date)), line, { mode: 384 });
13743
+ appendFileSync(path8.join(logDir, dailyFileName(date)), line, { mode: 384 });
13388
13744
  pruneOldFiles(logDir);
13389
13745
  } catch {
13390
13746
  try {
@@ -13395,7 +13751,7 @@ var createLogger = (options = {}) => {
13395
13751
  context: { redaction_failed: true }
13396
13752
  };
13397
13753
  mkdirSync2(logDir, { recursive: true, mode: 448 });
13398
- appendFileSync(path7.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
13754
+ appendFileSync(path8.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
13399
13755
  `, {
13400
13756
  mode: 384
13401
13757
  });
@@ -13478,6 +13834,24 @@ OpenCode Workit decisions use the native question header "Workit decision: <purp
13478
13834
  return cached2;
13479
13835
  };
13480
13836
 
13837
+ // packages/workit-opencode/src/stale-sources.ts
13838
+ import { existsSync as existsSync5 } from "node:fs";
13839
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
13840
+ var pluginSourceFiles = [
13841
+ fileURLToPath2(import.meta.url),
13842
+ fileURLToPath2(new URL("./plugin.ts", import.meta.url)),
13843
+ fileURLToPath2(new URL("./tools/workit.ts", import.meta.url)),
13844
+ ...[
13845
+ "task-contract.ts",
13846
+ "task-engine.ts",
13847
+ "task-evaluation.ts",
13848
+ "task-store.ts",
13849
+ "workers.ts",
13850
+ "authority.ts",
13851
+ "methods.ts"
13852
+ ].map((file) => fileURLToPath2(new URL(`../../workit-core/src/core/${file}`, import.meta.url)))
13853
+ ].filter((file) => existsSync5(file));
13854
+
13481
13855
  // packages/workit-opencode/src/tools/repo.ts
13482
13856
  import path22 from "node:path";
13483
13857
  import { realpathSync as realpathSync8 } from "node:fs";
@@ -25807,25 +26181,6 @@ import fs7 from "node:fs";
25807
26181
  import path9 from "node:path";
25808
26182
  import { createHash as createHash5 } from "node:crypto";
25809
26183
 
25810
- // packages/workit-core/src/core/package-root.ts
25811
- import { existsSync as existsSync4 } from "node:fs";
25812
- import path8 from "node:path";
25813
- import { fileURLToPath } from "node:url";
25814
- var packageRoot = () => {
25815
- let dir = path8.dirname(fileURLToPath(import.meta.url));
25816
- while (true) {
25817
- const parent = path8.dirname(dir);
25818
- if (existsSync4(path8.join(dir, "package.json")) || parent === dir)
25819
- return dir;
25820
- dir = parent;
25821
- }
25822
- };
25823
- var assetRoot = () => {
25824
- const root = packageRoot();
25825
- const assets = path8.join(root, "assets");
25826
- return existsSync4(assets) ? assets : root;
25827
- };
25828
-
25829
26184
  // packages/workit-core/src/core/scripts.ts
25830
26185
  var PLUGIN_ROOT = packageRoot();
25831
26186
  var ASSET_ROOT = assetRoot();
@@ -26158,10 +26513,10 @@ function changelogApply({
26158
26513
  }
26159
26514
 
26160
26515
  // packages/workit-core/src/core/git.ts
26161
- import { execFileSync } from "node:child_process";
26516
+ import { execFileSync as execFileSync2 } from "node:child_process";
26162
26517
  var run2 = (cwd, args) => {
26163
26518
  try {
26164
- const stdout = execFileSync("git", args, {
26519
+ const stdout = execFileSync2("git", args, {
26165
26520
  cwd,
26166
26521
  encoding: "utf8",
26167
26522
  stdio: ["pipe", "pipe", "pipe"]
@@ -26279,15 +26634,15 @@ function parseVerifyOutput(stdout) {
26279
26634
  }
26280
26635
 
26281
26636
  // packages/workit-core/src/core/branch.ts
26282
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
26283
- import { execFileSync as execFileSync2 } from "node:child_process";
26637
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
26638
+ import { execFileSync as execFileSync3 } from "node:child_process";
26284
26639
  import path13 from "node:path";
26285
26640
 
26286
26641
  // packages/workit-core/src/core/config.ts
26287
26642
  import {
26288
26643
  copyFileSync,
26289
26644
  cpSync,
26290
- existsSync as existsSync5,
26645
+ existsSync as existsSync6,
26291
26646
  mkdirSync as mkdirSync3,
26292
26647
  readFileSync as readFileSync4,
26293
26648
  readdirSync as readdirSync4,
@@ -26327,11 +26682,11 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
26327
26682
  return dir;
26328
26683
  }
26329
26684
  const legacy = path10.join(process.env.XDG_CONFIG_HOME || path10.join(os2.homedir(), ".config"), "workflow-toolkit");
26330
- if (!existsSync5(legacy)) {
26685
+ if (!existsSync6(legacy)) {
26331
26686
  migratedDir = dir;
26332
26687
  return dir;
26333
26688
  }
26334
- if (!migrationFailed && existsSync5(dir)) {
26689
+ if (!migrationFailed && existsSync6(dir)) {
26335
26690
  migratedDir = dir;
26336
26691
  return dir;
26337
26692
  }
@@ -26341,7 +26696,7 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
26341
26696
  for (const entry of readdirSync4(legacy, { withFileTypes: true })) {
26342
26697
  const src = path10.join(legacy, entry.name);
26343
26698
  const dest = path10.join(dir, entry.name);
26344
- if (existsSync5(dest))
26699
+ if (existsSync6(dest))
26345
26700
  continue;
26346
26701
  try {
26347
26702
  if (entry.isDirectory())
@@ -26919,7 +27274,7 @@ var baseBranch = (cwd) => {
26919
27274
  return { base: String(resolved.defaultTargetBranch ?? "develop") };
26920
27275
  };
26921
27276
  var originBaseReady = (cwd, base) => {
26922
- const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
27277
+ const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
26923
27278
  try {
26924
27279
  try {
26925
27280
  run(["fetch", "origin", base, "--prune"]);
@@ -26927,7 +27282,7 @@ var originBaseReady = (cwd, base) => {
26927
27282
  run(["fetch", "origin", "--prune"]);
26928
27283
  }
26929
27284
  try {
26930
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
27285
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
26931
27286
  cwd,
26932
27287
  stdio: "pipe"
26933
27288
  });
@@ -26946,11 +27301,11 @@ var originBaseReady = (cwd, base) => {
26946
27301
  }
26947
27302
  };
26948
27303
  var fastForwardBase = (cwd, base) => {
26949
- const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
27304
+ const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
26950
27305
  try {
26951
27306
  let hasLocalBase = true;
26952
27307
  try {
26953
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
27308
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
26954
27309
  cwd,
26955
27310
  stdio: "pipe"
26956
27311
  });
@@ -26991,18 +27346,17 @@ var branchSetup = ({
26991
27346
  log
26992
27347
  }) => {
26993
27348
  const cwd = path13.resolve(workspace_root);
26994
- const exec = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
27349
+ const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
26995
27350
  const current = gitContext(cwd).branch;
26996
27351
  if (!current || current === "unknown")
26997
- return { error: "not in a git repository" };
27352
+ return { error: "not in a git repository", phase: "preflight" };
26998
27353
  const sdd = sdd_dir ?? "docs";
26999
27354
  const manifestPath = path13.isAbsolute(sdd) ? path13.join(sdd, "manifest.json") : path13.join(cwd, sdd, "manifest.json");
27000
27355
  const ensureManifestDir = () => {
27001
27356
  const dir = path13.dirname(manifestPath);
27002
- if (!existsSync6(dir))
27357
+ if (!existsSync7(dir))
27003
27358
  mkdirSync4(dir, { recursive: true, mode: 493 });
27004
27359
  };
27005
- ensureManifestDir();
27006
27360
  const readManifest = () => {
27007
27361
  try {
27008
27362
  return JSON.parse(readFileSync6(manifestPath, "utf8"));
@@ -27020,13 +27374,16 @@ var branchSetup = ({
27020
27374
  const manifest = readManifest();
27021
27375
  const ref = manifest.stash_ref;
27022
27376
  if (!ref)
27023
- return { error: "no stash_ref in manifest" };
27377
+ return { error: "no stash_ref in manifest", phase: "preflight" };
27024
27378
  journal(`pre-pop: ${String(ref)}`);
27025
27379
  try {
27026
27380
  exec(["stash", "pop", String(ref)]);
27027
27381
  } catch (error) {
27028
27382
  journal("pop: failed");
27029
- return { error: error instanceof Error ? error.message : "stash pop failed" };
27383
+ return {
27384
+ error: error instanceof Error ? error.message : "stash pop failed",
27385
+ phase: "post_mutation"
27386
+ };
27030
27387
  }
27031
27388
  journal("pop: ok");
27032
27389
  delete manifest.stash_ref;
@@ -27036,11 +27393,17 @@ var branchSetup = ({
27036
27393
  }
27037
27394
  const target = target_branch ?? "";
27038
27395
  if (!target)
27039
- return { error: "target branch required" };
27396
+ return {
27397
+ error: "target_branch (the working branch to create or switch to) is required for setup",
27398
+ phase: "preflight"
27399
+ };
27040
27400
  if (isProtected(cwd, target))
27041
- return { error: `protected branch ${target}` };
27401
+ return { error: `protected branch ${target}`, phase: "preflight" };
27042
27402
  if (!allowedBranch(cwd, target))
27043
- return { error: `target branch ${target} is not allowed by the branch policy` };
27403
+ return {
27404
+ error: `target branch ${target} is not allowed by the branch policy`,
27405
+ phase: "preflight"
27406
+ };
27044
27407
  let base;
27045
27408
  let targetExists = true;
27046
27409
  try {
@@ -27051,12 +27414,12 @@ var branchSetup = ({
27051
27414
  if (!targetExists) {
27052
27415
  const baseResolved = baseBranch(cwd);
27053
27416
  if ("error" in baseResolved)
27054
- return { error: baseResolved.error };
27417
+ return { error: baseResolved.error, phase: "preflight" };
27055
27418
  base = baseResolved.base;
27056
27419
  }
27057
27420
  journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
27058
27421
  let stash_ref;
27059
- const failAfterStash = (message) => {
27422
+ const failAfterStash = (message, phase = "post_mutation") => {
27060
27423
  let suffix = "";
27061
27424
  if (stash_ref) {
27062
27425
  journal(`pre-pop: ${stash_ref}`);
@@ -27069,27 +27432,31 @@ var branchSetup = ({
27069
27432
  suffix = " (changes preserved in stash)";
27070
27433
  }
27071
27434
  }
27072
- return { error: `${message}${suffix}` };
27435
+ return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
27073
27436
  };
27074
27437
  if (current !== target) {
27075
27438
  const dirty = Boolean(gitContext(cwd).status_short.trim());
27076
27439
  if (dirty && stash !== "yes") {
27077
27440
  return {
27078
- error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes"
27441
+ error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
27442
+ phase: "preflight"
27079
27443
  };
27080
27444
  }
27081
27445
  let validatedBase;
27082
27446
  if (!targetExists && base !== undefined) {
27083
27447
  const ready = originBaseReady(cwd, base);
27084
27448
  if (!ready.ok)
27085
- return { error: ready.error ?? "ensure-base-branch failed" };
27449
+ return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
27086
27450
  validatedBase = base;
27087
27451
  }
27088
27452
  if (dirty) {
27089
27453
  try {
27090
27454
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
27091
27455
  } catch (error) {
27092
- return { error: error instanceof Error ? error.message : "stash push failed" };
27456
+ return {
27457
+ error: error instanceof Error ? error.message : "stash push failed",
27458
+ phase: "post_mutation"
27459
+ };
27093
27460
  }
27094
27461
  stash_ref = "stash@{0}";
27095
27462
  journal(`stash push: ${stash_ref}`);
@@ -27100,14 +27467,14 @@ var branchSetup = ({
27100
27467
  } catch (error) {
27101
27468
  const message = error instanceof Error ? error.message : "checkout failed";
27102
27469
  if (/worktree/i.test(message)) {
27103
- return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`);
27470
+ return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
27104
27471
  }
27105
27472
  try {
27106
27473
  let effectiveBase = base;
27107
27474
  if (effectiveBase === undefined) {
27108
27475
  const lateResolved = baseBranch(cwd);
27109
27476
  if ("error" in lateResolved)
27110
- return failAfterStash(lateResolved.error);
27477
+ return failAfterStash(lateResolved.error, "preflight");
27111
27478
  effectiveBase = lateResolved.base;
27112
27479
  }
27113
27480
  const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
@@ -27153,7 +27520,7 @@ var branchSetup = ({
27153
27520
  };
27154
27521
 
27155
27522
  // packages/workit-core/src/core/gitignore.ts
27156
- import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
27523
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
27157
27524
  import path14 from "node:path";
27158
27525
  var GITIGNORE_ENTRIES = [
27159
27526
  "# workit: SDD working state (never commit)",
@@ -27175,7 +27542,7 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
27175
27542
  if (!confirmed)
27176
27543
  return { ok: false, error: "confirmed: true required" };
27177
27544
  const file = path14.join(workspaceRoot, ".gitignore");
27178
- const existing = existsSync7(file) ? readFileSync7(file, "utf8") : "";
27545
+ const existing = existsSync8(file) ? readFileSync7(file, "utf8") : "";
27179
27546
  const existingLines = new Set(existing.split(`
27180
27547
  `).map((l) => l.trim()).filter(Boolean));
27181
27548
  const added = [];
@@ -27194,19 +27561,19 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
27194
27561
  ` : "") + append.join(`
27195
27562
  `) + `
27196
27563
  `, "utf8");
27197
- } else if (!existsSync7(file)) {
27564
+ } else if (!existsSync8(file)) {
27198
27565
  writeFileSync4(file, "", "utf8");
27199
27566
  }
27200
27567
  return { ok: true, path: file, added };
27201
27568
  };
27202
27569
 
27203
27570
  // packages/workit-core/src/core/hygiene.ts
27204
- import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
27571
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
27205
27572
  import path15 from "node:path";
27206
27573
  var repoRoot = assetRoot();
27207
27574
  var templatesDir = () => path15.join(repoRoot, "templates", "hygiene");
27208
27575
  var packageJson = (root) => {
27209
- if (!existsSync8(path15.join(root, "package.json")))
27576
+ if (!existsSync9(path15.join(root, "package.json")))
27210
27577
  return null;
27211
27578
  try {
27212
27579
  return JSON.parse(readFileSync8(path15.join(root, "package.json"), "utf8"));
@@ -27215,7 +27582,7 @@ var packageJson = (root) => {
27215
27582
  }
27216
27583
  };
27217
27584
  var isOpenSource = (root) => {
27218
- if (existsSync8(path15.join(root, "LICENSE")))
27585
+ if (existsSync9(path15.join(root, "LICENSE")))
27219
27586
  return true;
27220
27587
  const pkg = packageJson(root);
27221
27588
  if (pkg && !pkg.private)
@@ -27243,10 +27610,10 @@ var planHygieneFiles = (root, opts = {}) => {
27243
27610
  files.push("LICENSE", "CONTRIBUTING.md");
27244
27611
  const planned = [];
27245
27612
  for (const file of files) {
27246
- if (existsSync8(path15.join(root, file)))
27613
+ if (existsSync9(path15.join(root, file)))
27247
27614
  continue;
27248
27615
  const tpl = path15.join(templatesDir(), file);
27249
- if (!existsSync8(tpl))
27616
+ if (!existsSync9(tpl))
27250
27617
  continue;
27251
27618
  const content = readFileSync8(tpl, "utf8").replace(/<PROJECT>/g, path15.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
27252
27619
  planned.push({ path: path15.join(root, file), content });
@@ -27266,7 +27633,7 @@ var ensureHygieneFiles = (root, opts) => {
27266
27633
 
27267
27634
  // packages/workit-core/src/core/repo-context.ts
27268
27635
  import { spawnSync as spawnSync4 } from "node:child_process";
27269
- import { existsSync as existsSync9, readFileSync as readFileSync9, readdirSync as readdirSync5 } from "node:fs";
27636
+ import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync5 } from "node:fs";
27270
27637
  import path16 from "node:path";
27271
27638
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
27272
27639
  try {
@@ -27433,7 +27800,7 @@ var diffStatForRange = (cwd, range) => {
27433
27800
  };
27434
27801
  var packageScripts = (cwd, keys) => {
27435
27802
  const pkgPath = path16.join(cwd, "package.json");
27436
- if (!existsSync9(pkgPath))
27803
+ if (!existsSync10(pkgPath))
27437
27804
  return [];
27438
27805
  try {
27439
27806
  const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
@@ -27468,7 +27835,7 @@ var documentationFiles = (cwd) => {
27468
27835
  return matches.sort().slice(0, 200);
27469
27836
  };
27470
27837
  var readTrimmed = (file, maxLines) => {
27471
- if (!existsSync9(file))
27838
+ if (!existsSync10(file))
27472
27839
  return "";
27473
27840
  const lines = readFileSync9(file, "utf8").split(`
27474
27841
  `);
@@ -27544,7 +27911,7 @@ ${status.stderr}` : "");
27544
27911
  stdout += `
27545
27912
  `;
27546
27913
  stdout += printSection("Recent Validation Signals");
27547
- if (existsSync9(path16.join(cwd, "package.json"))) {
27914
+ if (existsSync10(path16.join(cwd, "package.json"))) {
27548
27915
  stdout += `package.json detected. Common scripts:
27549
27916
  `;
27550
27917
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -27553,7 +27920,7 @@ ${status.stderr}` : "");
27553
27920
  `) + `
27554
27921
  `;
27555
27922
  }
27556
- if (existsSync9(path16.join(cwd, "Cargo.toml")) || existsSync9(path16.join(cwd, "src-tauri/Cargo.toml"))) {
27923
+ if (existsSync10(path16.join(cwd, "Cargo.toml")) || existsSync10(path16.join(cwd, "src-tauri/Cargo.toml"))) {
27557
27924
  stdout += `Rust project detected.
27558
27925
  `;
27559
27926
  }
@@ -27605,7 +27972,7 @@ function changelogContext(root, range) {
27605
27972
  `;
27606
27973
  stdout += printSection("Existing CHANGELOG.md");
27607
27974
  const changelogPath = path16.join(cwd, "CHANGELOG.md");
27608
- if (existsSync9(changelogPath)) {
27975
+ if (existsSync10(changelogPath)) {
27609
27976
  stdout += readTrimmed(changelogPath, 260) + `
27610
27977
  `;
27611
27978
  } else {
@@ -27647,7 +28014,7 @@ function docsRefreshContext(root, range) {
27647
28014
  `;
27648
28015
  stdout += printSection("README Preview");
27649
28016
  const readme = path16.join(cwd, "README.md");
27650
- if (existsSync9(readme)) {
28017
+ if (existsSync10(readme)) {
27651
28018
  stdout += readTrimmed(readme, 220) + `
27652
28019
  `;
27653
28020
  } else {
@@ -27656,7 +28023,7 @@ function docsRefreshContext(root, range) {
27656
28023
  }
27657
28024
  stdout += printSection("Package Scripts");
27658
28025
  const pkgPath = path16.join(cwd, "package.json");
27659
- if (existsSync9(pkgPath)) {
28026
+ if (existsSync10(pkgPath)) {
27660
28027
  try {
27661
28028
  const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
27662
28029
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
@@ -27707,7 +28074,7 @@ function releaseNotesContext(root, rangeOrTag) {
27707
28074
  `;
27708
28075
  stdout += printSection("Existing Release Files");
27709
28076
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
27710
- if (existsSync9(path16.join(cwd, rel)))
28077
+ if (existsSync10(path16.join(cwd, rel)))
27711
28078
  stdout += rel + `
27712
28079
  `;
27713
28080
  }
@@ -27736,7 +28103,7 @@ function releaseNotesContext(root, rangeOrTag) {
27736
28103
 
27737
28104
  // packages/workit-core/src/core/verify-project.ts
27738
28105
  import { spawnSync as spawnSync5 } from "node:child_process";
27739
- import { existsSync as existsSync10, readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
28106
+ import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
27740
28107
  import path17 from "node:path";
27741
28108
  var commandOnPath = (name) => {
27742
28109
  const dirs = (process.env.PATH ?? "").split(path17.delimiter);
@@ -27761,7 +28128,7 @@ var commandOnPath = (name) => {
27761
28128
  };
27762
28129
  var hasScript = (cwd, name) => {
27763
28130
  const pkgPath = path17.join(cwd, "package.json");
27764
- if (!existsSync10(pkgPath))
28131
+ if (!existsSync11(pkgPath))
27765
28132
  return false;
27766
28133
  try {
27767
28134
  const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
@@ -27771,10 +28138,10 @@ var hasScript = (cwd, name) => {
27771
28138
  }
27772
28139
  };
27773
28140
  var packageRunner = (cwd) => {
27774
- if (existsSync10(path17.join(cwd, "pnpm-lock.yaml")))
28141
+ if (existsSync11(path17.join(cwd, "pnpm-lock.yaml")))
27775
28142
  return "pnpm";
27776
28143
  const pkgPath = path17.join(cwd, "package.json");
27777
- if (existsSync10(pkgPath)) {
28144
+ if (existsSync11(pkgPath)) {
27778
28145
  try {
27779
28146
  const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
27780
28147
  if (typeof pkg.packageManager === "string" && pkg.packageManager.startsWith("pnpm")) {
@@ -27782,7 +28149,7 @@ var packageRunner = (cwd) => {
27782
28149
  }
27783
28150
  } catch {}
27784
28151
  }
27785
- if (existsSync10(path17.join(cwd, "yarn.lock")))
28152
+ if (existsSync11(path17.join(cwd, "yarn.lock")))
27786
28153
  return "yarn";
27787
28154
  return "npm";
27788
28155
  };
@@ -27843,7 +28210,7 @@ root: ${cwd}
27843
28210
  lines.push(`dry_run: ${String(dryRun)}
27844
28211
  `);
27845
28212
  const pkgPath = path17.join(cwd, "package.json");
27846
- if (existsSync10(pkgPath)) {
28213
+ if (existsSync11(pkgPath)) {
27847
28214
  const runner = packageRunner(cwd);
27848
28215
  for (const script of ["lint", "format:check", "test", "build"]) {
27849
28216
  if (hasScript(cwd, script)) {
@@ -27858,7 +28225,7 @@ root: ${cwd}
27858
28225
  } else {
27859
28226
  skipCheck("javascript", "package.json not found");
27860
28227
  }
27861
- const cargoManifest = existsSync10(path17.join(cwd, "Cargo.toml")) ? "Cargo.toml" : existsSync10(path17.join(cwd, "src-tauri/Cargo.toml")) ? "src-tauri/Cargo.toml" : "";
28228
+ const cargoManifest = existsSync11(path17.join(cwd, "Cargo.toml")) ? "Cargo.toml" : existsSync11(path17.join(cwd, "src-tauri/Cargo.toml")) ? "src-tauri/Cargo.toml" : "";
27862
28229
  if (cargoManifest) {
27863
28230
  runCheck("cargo fmt", "cargo", ["fmt", "--manifest-path", cargoManifest, "--", "--check"]);
27864
28231
  runCheck("cargo clippy", "cargo", [
@@ -27874,7 +28241,7 @@ root: ${cwd}
27874
28241
  } else {
27875
28242
  skipCheck("rust", "Cargo.toml not found");
27876
28243
  }
27877
- if (existsSync10(path17.join(cwd, "pyproject.toml")) || existsSync10(path17.join(cwd, "pytest.ini")) || existsSync10(path17.join(cwd, "tests"))) {
28244
+ if (existsSync11(path17.join(cwd, "pyproject.toml")) || existsSync11(path17.join(cwd, "pytest.ini")) || existsSync11(path17.join(cwd, "tests"))) {
27878
28245
  if (commandOnPath("pytest"))
27879
28246
  runCheck("pytest", "pytest", []);
27880
28247
  else
@@ -27896,7 +28263,7 @@ root: ${cwd}
27896
28263
  lines.push(`status: skipped (dry run)
27897
28264
  `);
27898
28265
  skipped += 1;
27899
- } else if (existsSync10(changelogPath)) {
28266
+ } else if (existsSync11(changelogPath)) {
27900
28267
  if (/## \[Unreleased\]/i.test(readFileSync10(changelogPath, "utf8"))) {
27901
28268
  lines.push(`status: pass
27902
28269
  `);
@@ -28231,7 +28598,7 @@ function writeFileExclusive(file, content, mode) {
28231
28598
  import {
28232
28599
  copyFileSync as copyFileSync2,
28233
28600
  cpSync as cpSync2,
28234
- existsSync as existsSync11,
28601
+ existsSync as existsSync12,
28235
28602
  lstatSync as lstatSync4,
28236
28603
  mkdirSync as mkdirSync5,
28237
28604
  mkdtempSync,
@@ -29772,7 +30139,7 @@ function createRepoTools(runtime = defaultRuntime) {
29772
30139
  return output(legacyScriptResult({
29773
30140
  stdout: JSON.stringify(result),
29774
30141
  stderr: "",
29775
- exitCode: result.error ? 1 : 0,
30142
+ exitCode: "error" in result ? 1 : 0,
29776
30143
  cwd: context.directory
29777
30144
  }));
29778
30145
  }
@@ -30420,6 +30787,94 @@ var safePath = (root, value) => {
30420
30787
  return null;
30421
30788
  }
30422
30789
  };
30790
+ var branchCreationBaseline = (root, target, base) => {
30791
+ const exists = gitValue(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]) !== null;
30792
+ if (exists)
30793
+ return { base_branch: base, target_exists: true, remote_base: null };
30794
+ const remote = run4(root, ["ls-remote", "origin", `refs/heads/${base}`]);
30795
+ const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
30796
+ return {
30797
+ base_branch: base,
30798
+ target_exists: false,
30799
+ remote_base: /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null
30800
+ };
30801
+ };
30802
+ var shortSha = (value) => typeof value === "string" && value ? value.slice(0, 8) : "unknown";
30803
+ var isProtectedBranch2 = (root, branch) => resolveBranchPolicyFor(root).protected.has(branch.toLowerCase());
30804
+ var actionProposalQuestion = (request, descriptorPayload) => {
30805
+ const payload = descriptorPayload ?? {};
30806
+ const resolved = payload.resolved ?? {};
30807
+ const payloadTarget = String(payload.target_branch ?? "");
30808
+ if (request.operation === "git.commit" && Array.isArray(payload.plan_steps)) {
30809
+ const steps = payload.plan_steps;
30810
+ return {
30811
+ presented: `Workit decision: action — Approve committing the plan's ${steps.length} listed tasks on \`${String(payload.plan_branch ?? "")}\`?`,
30812
+ approvedText: `Approve the ${steps.length} listed plan commits.`
30813
+ };
30814
+ }
30815
+ switch (request.operation) {
30816
+ case "git.branch_setup": {
30817
+ if (request.payload.action === "reapply_stash")
30818
+ return {
30819
+ presented: "Workit decision: action — Reapply the pre-checkout stash recorded in the branch manifest?",
30820
+ approvedText: "Reapply the recorded stash."
30821
+ };
30822
+ const target = String(payload.target_branch ?? "");
30823
+ if (resolved.target_exists === true)
30824
+ return {
30825
+ presented: `Workit decision: action — Switch to branch \`${target}\`?`,
30826
+ approvedText: `Switch to \`${target}\`.`
30827
+ };
30828
+ const base = String(resolved.base_branch ?? "the base branch");
30829
+ return {
30830
+ presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
30831
+ approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
30832
+ };
30833
+ }
30834
+ case "git.commit": {
30835
+ const paths = Array.isArray(resolved.paths) ? resolved.paths : [];
30836
+ return {
30837
+ presented: `Workit decision: action — Commit ${paths.length} staged file(s) as "${String(request.payload.message)}"?`,
30838
+ approvedText: `Commit as "${String(request.payload.message)}".`
30839
+ };
30840
+ }
30841
+ case "git.push":
30842
+ return {
30843
+ presented: `Workit decision: action — Push branch \`${String(payload.branch ?? "")}\` to origin?`,
30844
+ approvedText: `Push \`${String(payload.branch ?? "")}\` to origin.`
30845
+ };
30846
+ case "hosting.pull_request":
30847
+ return {
30848
+ presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
30849
+ approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
30850
+ };
30851
+ case "changelog.apply":
30852
+ return {
30853
+ presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
30854
+ approvedText: `Apply changelog changes at ${String(payload.path ?? "CHANGELOG.md")}.`
30855
+ };
30856
+ case "youtrack.update":
30857
+ return {
30858
+ presented: `Workit decision: action — Update YouTrack issue ${String(request.payload.issueId)}${request.payload.minutes ? ` and log ${String(request.payload.minutes)}m` : ""}?`,
30859
+ approvedText: `Update YouTrack issue ${String(request.payload.issueId)}.`
30860
+ };
30861
+ case "youtrack.time":
30862
+ return {
30863
+ presented: `Workit decision: action — Log ${String(request.payload.minutes)}m to YouTrack issue ${String(request.payload.issueId)}?`,
30864
+ approvedText: `Log time to YouTrack issue ${String(request.payload.issueId)}.`
30865
+ };
30866
+ case "youtrack.meeting":
30867
+ return {
30868
+ presented: `Workit decision: action — Log meeting time to YouTrack issue ${String(request.payload.issueId)}?`,
30869
+ approvedText: `Log meeting time to YouTrack issue ${String(request.payload.issueId)}.`
30870
+ };
30871
+ default:
30872
+ return {
30873
+ presented: `Workit decision: action — Approve ${request.operation}?`,
30874
+ approvedText: `Approve ${request.operation}.`
30875
+ };
30876
+ }
30877
+ };
30423
30878
  var approvedResolvedExternalAction = (content) => {
30424
30879
  try {
30425
30880
  const value = JSON.parse(content);
@@ -30683,24 +31138,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30683
31138
  const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
30684
31139
  if (!sdd_dir)
30685
31140
  return failure("invalid_input", "sdd_dir must stay inside the workspace");
31141
+ const action = request.payload.action ?? "setup";
30686
31142
  const policy = resolveBranchPolicyFor(root);
30687
- const target_branch = request.payload.target_branch ?? policy.defaultTargetBranch;
31143
+ const target_branch = request.payload.target_branch;
31144
+ if (action !== "reapply_stash" && !target_branch)
31145
+ return failure("invalid_input", "target_branch (the working branch to create or switch to) is required for setup", {
31146
+ outcome: "not_started",
31147
+ fields: [{ path: "target_branch", reason: "required for action=setup" }]
31148
+ });
30688
31149
  const head = gitValue(root, ["rev-parse", "HEAD"]);
30689
31150
  if (!head)
30690
31151
  return failure("storage_error", "current Git commit could not be resolved");
31152
+ const dirty = Boolean((gitRaw(root, ["status", "--short"]) ?? "").trim());
31153
+ const creation = action === "reapply_stash" || !target_branch ? { base_branch: policy.defaultTargetBranch } : branchCreationBaseline(root, target_branch, policy.defaultTargetBranch);
30691
31154
  const normalized = {
30692
31155
  operation: request.operation,
30693
31156
  payload: {
30694
31157
  ...request.payload,
30695
- action: request.payload.action ?? "setup",
31158
+ action,
30696
31159
  sdd_dir,
30697
31160
  stash: request.payload.stash ?? "no",
30698
- target_branch
31161
+ ...target_branch ? { target_branch } : {}
30699
31162
  }
30700
31163
  };
30701
31164
  return success(null, null, {
30702
31165
  request: normalized,
30703
- descriptorPayload: { ...normalized.payload, resolved: { head, target_branch } }
31166
+ descriptorPayload: {
31167
+ ...normalized.payload,
31168
+ resolved: { head, dirty, ...creation }
31169
+ }
30704
31170
  });
30705
31171
  }
30706
31172
  case "git.push": {
@@ -30714,7 +31180,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30714
31180
  }) : failure("invalid_input", "git push requires a resolvable current branch");
30715
31181
  }
30716
31182
  case "git.commit": {
31183
+ const planSteps = request.payload.plan_steps;
31184
+ const planBranch = request.payload.plan_branch;
30717
31185
  const head = gitValue(root, ["rev-parse", "HEAD"]);
31186
+ if (!head)
31187
+ return failure("storage_error", "Git state could not be resolved");
31188
+ const branch = gitValue(root, ["branch", "--show-current"]);
31189
+ if (planSteps !== undefined || planBranch !== undefined) {
31190
+ if (!Array.isArray(planSteps) || planSteps.some((step) => typeof step !== "string" || !step.trim()) || new Set(planSteps).size !== planSteps.length || planSteps.length === 0 || planSteps.length > 32)
31191
+ return failure("invalid_input", "plan_steps must be 1-32 unique non-empty commit messages", {
31192
+ outcome: "not_started",
31193
+ fields: [{ path: "plan_steps", reason: "unique strings" }]
31194
+ });
31195
+ if (typeof planBranch !== "string" || !planBranch.trim() || planBranch !== branch)
31196
+ return failure("invalid_input", "plan_branch must name the current working branch", {
31197
+ outcome: "not_started",
31198
+ fields: [{ path: "plan_branch", reason: "current branch" }]
31199
+ });
31200
+ if (isProtectedBranch2(root, planBranch))
31201
+ return failure("invalid_input", "plan_branch must not be a protected branch", {
31202
+ outcome: "not_started",
31203
+ fields: [{ path: "plan_branch", reason: "protected branch" }]
31204
+ });
31205
+ return success(null, null, {
31206
+ request,
31207
+ descriptorPayload: {
31208
+ plan_steps: planSteps,
31209
+ plan_branch: planBranch,
31210
+ resolved: { head, branch, steps: planSteps }
31211
+ }
31212
+ });
31213
+ }
30718
31214
  const staged = gitRaw(root, [
30719
31215
  "diff",
30720
31216
  "--cached",
@@ -30725,7 +31221,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30725
31221
  "HEAD"
30726
31222
  ]);
30727
31223
  const paths = stagedPaths(root);
30728
- if (!head || staged === null || paths === null)
31224
+ const message = request.payload.message;
31225
+ if (typeof message !== "string" || !message)
31226
+ return failure("invalid_input", "git.commit requires a commit message", {
31227
+ outcome: "not_started",
31228
+ fields: [{ path: "message", reason: "required unless plan_steps is provided" }]
31229
+ });
31230
+ if (staged === null || paths === null)
30729
31231
  return failure("storage_error", "Git state could not be resolved");
30730
31232
  let policy;
30731
31233
  try {
@@ -30740,13 +31242,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30740
31242
  `).filter(Boolean));
30741
31243
  flavor = detected.flavor ?? "conventional";
30742
31244
  }
30743
- if (!matchCommitFlavor(request.payload.message, flavor, policy.pattern))
31245
+ if (!matchCommitFlavor(message, flavor, policy.pattern))
30744
31246
  return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
30745
31247
  return success(null, null, {
30746
31248
  request,
30747
31249
  descriptorPayload: {
30748
31250
  ...request.payload,
30749
- resolved: { head, staged: sha256(staged), paths }
31251
+ resolved: { head, branch, staged: sha256(staged), paths }
30750
31252
  }
30751
31253
  });
30752
31254
  }
@@ -30916,10 +31418,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
30916
31418
  sdd_dir: request.payload.sdd_dir ?? "docs",
30917
31419
  workspace_root: root
30918
31420
  });
30919
- return result.error ? unknown3(request.operation) : success(null, null, result);
31421
+ if ("error" in result) {
31422
+ if (result.phase === "preflight")
31423
+ return failure("invalid_input", result.error, {
31424
+ outcome: "not_started",
31425
+ operation: request.operation
31426
+ });
31427
+ return unknown3(request.operation);
31428
+ }
31429
+ return success(null, null, result);
30920
31430
  }
30921
31431
  case "git.commit": {
30922
- const result = run4(root, ["commit", "-m", request.payload.message]);
31432
+ const message = request.payload.message;
31433
+ if (typeof message !== "string" || !message)
31434
+ return failure("invalid_input", "git.commit requires a commit message", {
31435
+ outcome: "not_started",
31436
+ fields: [{ path: "message", reason: "required" }]
31437
+ });
31438
+ const result = run4(root, ["commit", "-m", message]);
30923
31439
  return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown3(request.operation);
30924
31440
  }
30925
31441
  case "git.push": {
@@ -31031,6 +31547,14 @@ var sessionParent = (session) => {
31031
31547
  };
31032
31548
  var freshMs = 5 * 60 * 1000;
31033
31549
  var rejectedDescription = "Reject this decision";
31550
+ var isSelfAuthorizingActionContent = (content) => {
31551
+ try {
31552
+ const value = JSON.parse(content);
31553
+ return typeof value.operation === "string" && value.operation.length > 0;
31554
+ } catch {
31555
+ return /^[a-z][a-z_]*\.[a-z_]+$/.test(content);
31556
+ }
31557
+ };
31034
31558
  var decisionContent = (purpose, question, approvedContent) => ({
31035
31559
  header: `Workit decision: ${purpose}`,
31036
31560
  question,
@@ -31040,11 +31564,12 @@ var decisionContent = (purpose, question, approvedContent) => ({
31040
31564
  ]
31041
31565
  });
31042
31566
  var decisionOptions = (options) => Array.isArray(options) && options.length === 2 && options.every((option) => typeof option === "object" && option !== null && typeof option.label === "string" && typeof option.description === "string" && Object.keys(option).length === 2 && Object.keys(option).every((key) => key === "label" || key === "description")) ? options : null;
31567
+ var normalizeReceiptLabel = (label) => label.trim().replace(/(?:\s*[([]\s*[^()[\]]*\s*[)\]]\s*)+$/u, "").trim().toLowerCase();
31043
31568
  var purposeForQuestion = (question) => {
31044
31569
  const header = typeof question.header === "string" ? question.header.trim() : "";
31045
31570
  const options = decisionOptions(question.options);
31046
31571
  const decisionPurpose = header.match(/^Workit decision: (design|action|limitation|preference)$/)?.[1];
31047
- if (decisionPurpose && options !== null && options[0].label === "approved" && options[1].label === "rejected" && options[1].description === rejectedDescription)
31572
+ if (decisionPurpose && options !== null && normalizeReceiptLabel(options[0].label) === "approved" && normalizeReceiptLabel(options[1].label) === "rejected")
31048
31573
  return "decision";
31049
31574
  return;
31050
31575
  };
@@ -31053,6 +31578,10 @@ class NativeReceiptStore {
31053
31578
  #receipts = new Map;
31054
31579
  #observations = new WeakSet;
31055
31580
  #pending = new Map;
31581
+ #now;
31582
+ constructor(options = {}) {
31583
+ this.#now = options.now ?? Date.now;
31584
+ }
31056
31585
  recordRequest(requestID, sessionID, callID, questions) {
31057
31586
  if (typeof requestID !== "string" || !requestID)
31058
31587
  return;
@@ -31090,7 +31619,7 @@ class NativeReceiptStore {
31090
31619
  const options = decisionOptions(question.options);
31091
31620
  if (!options)
31092
31621
  return false;
31093
- const selected = options?.find((option) => option.label === answer);
31622
+ const selected = options.find((option) => normalizeReceiptLabel(option.label) === normalizeReceiptLabel(answer));
31094
31623
  if (!selected)
31095
31624
  return false;
31096
31625
  const decisionPurpose = typeof question.header === "string" ? question.header.match(/^Workit decision: (design|action|limitation|preference)$/)?.[1] : undefined;
@@ -31106,7 +31635,7 @@ class NativeReceiptStore {
31106
31635
  question: content.question,
31107
31636
  purpose,
31108
31637
  contentDigest: sha256(canonicalJson(content)),
31109
- recordedAt: Date.now()
31638
+ recordedAt: this.#now()
31110
31639
  };
31111
31640
  const queue = this.#receipts.get(sessionID) ?? [];
31112
31641
  queue.push(receipt);
@@ -31126,15 +31655,12 @@ class NativeReceiptStore {
31126
31655
  const receipt = queue[i];
31127
31656
  if (receipt.purpose !== purpose)
31128
31657
  continue;
31129
- if (expected.callID !== undefined && receipt.callID !== expected.callID || expected.selectedLabel !== undefined && receipt.selectedLabel !== expected.selectedLabel || expected.selectedDescription !== undefined && receipt.selectedDescription !== expected.selectedDescription || expected.decisionPurpose !== undefined && receipt.decisionPurpose !== expected.decisionPurpose || expected.contentDigest !== undefined && receipt.contentDigest !== expected.contentDigest || expected.question !== undefined && receipt.question !== expected.question)
31130
- return {
31131
- ok: false,
31132
- error: "permission_denied: native question receipt does not match the requested content " + "(binding questions must be asked receipt-shaped: header `Workit decision: <purpose>` " + "with the same label in the question text and exactly approved/rejected options)"
31133
- };
31658
+ if (expected.callID !== undefined && receipt.callID !== expected.callID || expected.selectedLabel !== undefined && normalizeReceiptLabel(receipt.selectedLabel) !== normalizeReceiptLabel(expected.selectedLabel) || expected.selectedDescription !== undefined && receipt.selectedDescription !== expected.selectedDescription || expected.decisionPurpose !== undefined && receipt.decisionPurpose !== expected.decisionPurpose || expected.contentDigest !== undefined && receipt.contentDigest !== expected.contentDigest || expected.question !== undefined && receipt.question !== expected.question)
31659
+ continue;
31134
31660
  queue.splice(i, 1);
31135
31661
  if (!queue.length)
31136
31662
  this.#receipts.delete(sessionID);
31137
- if (Date.now() - receipt.recordedAt > freshMs)
31663
+ if (this.#now() - receipt.recordedAt > freshMs)
31138
31664
  return { ok: false, error: "permission_denied: native question receipt is stale" };
31139
31665
  const observation = { receipt };
31140
31666
  this.#observations.add(observation);
@@ -31142,7 +31668,27 @@ class NativeReceiptStore {
31142
31668
  }
31143
31669
  return {
31144
31670
  ok: false,
31145
- error: "permission_denied: no native question receipt for this purpose " + "(ordinary multi-option questions mint no receipt; ask binding questions receipt-shaped: " + "header `Workit decision: <purpose>`, the same label in the question text, with exactly " + "approved/rejected options)"
31671
+ error: (() => {
31672
+ const samePurpose = queue.filter((receipt) => receipt.purpose === purpose);
31673
+ const base = "permission_denied: no matching native question receipt for this purpose";
31674
+ if (samePurpose.length) {
31675
+ const closest = samePurpose[samePurpose.length - 1];
31676
+ const reasons = [];
31677
+ if (expected.selectedLabel !== undefined && normalizeReceiptLabel(closest.selectedLabel) !== normalizeReceiptLabel(expected.selectedLabel))
31678
+ reasons.push(`selected answer was "${closest.selectedLabel}"`);
31679
+ if (expected.selectedDescription !== undefined && closest.selectedDescription !== expected.selectedDescription)
31680
+ reasons.push("approved option description did not match");
31681
+ if (expected.decisionPurpose !== undefined && closest.decisionPurpose !== expected.decisionPurpose)
31682
+ reasons.push(`purpose was "${closest.decisionPurpose}"`);
31683
+ if (expected.contentDigest !== undefined && closest.contentDigest !== expected.contentDigest)
31684
+ reasons.push("question or approved content differed from the asked receipt");
31685
+ if (this.#now() - closest.recordedAt > freshMs)
31686
+ reasons.push("receipt is stale (older than 5 minutes)");
31687
+ if (reasons.length)
31688
+ return `${base} (closest ${purpose} receipt: ${reasons.join("; ")})`;
31689
+ }
31690
+ return `${base}; if the user already settled this choice in conversation, do not ask ` + `again — record the settled choice in task progress and reassess so the ` + `requirement can retire. Otherwise ask once, receipt-shaped: header ` + `\`Workit decision: <purpose>\`, the same label repeated in the question text, ` + `with exactly approved/rejected options`;
31691
+ })()
31146
31692
  };
31147
31693
  }
31148
31694
  verify(observation, sessionID, purpose) {
@@ -31244,6 +31790,13 @@ var opencodeCapabilities = () => [
31244
31790
  reason: "nested native task launches are denied and observed child sessions are parent-bound",
31245
31791
  refs: [hostRef("task")]
31246
31792
  },
31793
+ {
31794
+ name: "fresh-context-review",
31795
+ surface: "task",
31796
+ assurance: "agent_guided",
31797
+ reason: "independent review runs as a native direct-child session; evidence evaluation enforces creator and duplicate-reviewer exclusion",
31798
+ refs: [hostRef("task")]
31799
+ },
31247
31800
  {
31248
31801
  name: "known_product_writes",
31249
31802
  surface: "write/edit/bash",
@@ -31262,7 +31815,8 @@ var opencodeCapabilities = () => [
31262
31815
  var nativeAuthority = (receipts, actor, reconciliationTokens = new WeakSet) => ({
31263
31816
  verifyDecision: ({ observation, expected, caller }) => {
31264
31817
  const receipt = receipts.verify(observation, actor, "decision");
31265
- if (!receipt || caller.host !== "opencode" || caller.actor !== actor || receipt.selectedLabel !== expected.response || receipt.selectedDescription !== (expected.response === "approved" ? expected.binding.approvedContent : rejectedDescription) || receipt.decisionPurpose !== expected.purpose || receipt.contentDigest !== sha256(canonicalJson(decisionContent(expected.purpose, expected.binding.presented, expected.binding.approvedContent))) || receipt.question !== expected.binding.presented)
31818
+ const expectedApproved = typeof expected.binding.displayed === "string" ? expected.binding.displayed : expected.binding.approvedContent;
31819
+ if (!receipt || caller.host !== "opencode" || caller.actor !== actor || normalizeReceiptLabel(receipt.selectedLabel) !== normalizeReceiptLabel(expected.response) || expected.response === "approved" && receipt.selectedDescription !== expectedApproved || receipt.decisionPurpose !== expected.purpose || receipt.contentDigest !== sha256(canonicalJson(decisionContent(expected.purpose, expected.binding.presented, expectedApproved))) || receipt.question !== expected.binding.presented)
31266
31820
  return failure("permission_denied", "native decision receipt is not bound to this session");
31267
31821
  return success(null, null, {
31268
31822
  kind: "host_observed",
@@ -31337,8 +31891,34 @@ var nativeDispatchFor = (directChildren, actor, generation) => ({
31337
31891
  var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
31338
31892
  const store = new TaskStore(root);
31339
31893
  const selected = approvedExternalAction(store, "opencode", actor, operation);
31340
- if (!selected.ok)
31894
+ if (!selected.ok) {
31895
+ const plan = planCommitBinding(store, "opencode", actor, operation);
31896
+ if (plan) {
31897
+ const actionRef = externalActionRef("opencode", actor, operation);
31898
+ return {
31899
+ taskId: plan.taskId,
31900
+ decisionId: plan.decisionId,
31901
+ actionRef,
31902
+ expectedRevision: plan.expectedRevision,
31903
+ expectedWorkspaceRevision: plan.expectedWorkspaceRevision,
31904
+ binding: plan.binding,
31905
+ step: plan.step,
31906
+ refresh: () => {
31907
+ const task = store.readTask(plan.taskId);
31908
+ const freshWorkspace = store.readWorkspace();
31909
+ if (!task.ok || !freshWorkspace.ok || !freshWorkspace.data)
31910
+ throw new Error("external action state changed");
31911
+ return {
31912
+ expectedRevision: task.data.revision,
31913
+ expectedWorkspaceRevision: freshWorkspace.data.revision
31914
+ };
31915
+ },
31916
+ reserveObservation: nativeExternalActionObservation(actor, actionRef, "reserve"),
31917
+ settleObservation: (outcome, revisions) => nativeExternalActionObservation(actor, actionRef, outcome, actionRef.kind === "host" ? actionRef.handle : "external-action", revisions)
31918
+ };
31919
+ }
31341
31920
  return selected;
31921
+ }
31342
31922
  const actionRef = externalActionRef("opencode", actor, operation);
31343
31923
  return {
31344
31924
  taskId: selected.data.task.id,
@@ -31382,8 +31962,10 @@ var workerIdFor = (store, actor, current, directChildren) => {
31382
31962
  var createWorkitTools = ({
31383
31963
  client,
31384
31964
  receipts = new NativeReceiptStore,
31385
- directChildren = new Map
31965
+ directChildren = new Map,
31966
+ now = Date.now
31386
31967
  } = {}) => {
31968
+ const actionProposals = new Map;
31387
31969
  const make = (family) => tool({
31388
31970
  description: `Workit ${family} operations backed by the shared task contract.`,
31389
31971
  args: operationShapeFor(family),
@@ -31417,16 +31999,45 @@ var createWorkitTools = ({
31417
31999
  let result;
31418
32000
  if (family === "decision" && args.action === "record") {
31419
32001
  const decision = parsed.data;
31420
- const observed = receipts.consume(context.sessionID, "decision", {
32002
+ const budgetIssue = workitBindingQuestionIssue([
32003
+ decisionContent(decision.purpose, decision.binding.presented, decision.binding.approvedContent)
32004
+ ]);
32005
+ if (budgetIssue)
32006
+ return output2(failure("invalid_input", budgetIssue));
32007
+ const expectation = {
31421
32008
  selectedLabel: decision.response,
31422
- selectedDescription: decision.response === "approved" ? decision.binding.approvedContent : rejectedDescription,
31423
32009
  decisionPurpose: decision.purpose,
31424
32010
  contentDigest: sha256(canonicalJson(decisionContent(decision.purpose, decision.binding.presented, decision.binding.approvedContent))),
31425
32011
  question: decision.binding.presented
31426
- });
32012
+ };
32013
+ if (decision.response === "approved")
32014
+ expectation.selectedDescription = decision.binding.approvedContent;
32015
+ const observed = receipts.consume(context.sessionID, "decision", expectation);
31427
32016
  if (!observed.ok)
31428
32017
  return output2(failure("permission_denied", observed.error));
31429
- result = core.observeDecision(parsed.data, observed.observation);
32018
+ let recordInput = parsed.data;
32019
+ if (decision.purpose === "action" && decision.response === "approved") {
32020
+ const queue = actionProposals.get(context.sessionID) ?? [];
32021
+ const textMatches = queue.filter((pending) => pending.presented === decision.binding.presented && pending.approvedText === decision.binding.approvedContent);
32022
+ const matches = textMatches.filter((pending) => now() - pending.createdAt <= 5 * 60 * 1000);
32023
+ if (matches.length > 1)
32024
+ return output2(failure("invalid_input", "multiple action proposals match this approval; resolve the action again"));
32025
+ const pending = matches[0];
32026
+ if (pending) {
32027
+ actionProposals.set(context.sessionID, queue.filter((candidate) => candidate !== pending));
32028
+ recordInput = {
32029
+ ...recordInput,
32030
+ binding: {
32031
+ ...recordInput.binding,
32032
+ approvedContent: pending.descriptor,
32033
+ displayed: pending.approvedText
32034
+ }
32035
+ };
32036
+ } else if (!isSelfAuthorizingActionContent(decision.binding.approvedContent)) {
32037
+ return output2(failure("invalid_input", textMatches.length > 0 ? "action proposal expired; resolve the action again for a fresh proposal" : "no matching action proposal; resolve the action through the action tool first"));
32038
+ }
32039
+ }
32040
+ result = core.observeDecision(recordInput, observed.observation);
31430
32041
  } else {
31431
32042
  const run = core[family];
31432
32043
  result = run.call(core, parsed.data);
@@ -31527,6 +32138,64 @@ var createWorkitTools = ({
31527
32138
  return output2(failure("external_outcome_unknown", "previous external action outcome is unknown"));
31528
32139
  }
31529
32140
  const descriptor = externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload);
32141
+ const drift = resolved.data.request.operation === "git.branch_setup" ? priorResolvedDrift(store, "opencode", context.sessionID, resolved.data.request.operation, resolved.data.request.payload, resolved.data.descriptorPayload.resolved) : success(null, null, null);
32142
+ if (!drift.ok)
32143
+ return output2(drift);
32144
+ const localOperation = [
32145
+ "git.branch_setup",
32146
+ "git.commit",
32147
+ "git.push",
32148
+ "hosting.pull_request",
32149
+ "changelog.apply"
32150
+ ].includes(resolved.data.request.operation);
32151
+ if (localOperation) {
32152
+ const writer = assertLocalExternalActionWriter(context.directory, {
32153
+ host: "opencode",
32154
+ actor: context.sessionID
32155
+ });
32156
+ if (!writer.ok)
32157
+ return output2(failure("needs_input", "writer ownership is required before this action", {
32158
+ outcome: "not_started",
32159
+ operation: resolved.data.request.operation,
32160
+ guidance: "Acquire checkout writer ownership first (writer.acquire) and retry the action."
32161
+ }));
32162
+ }
32163
+ const selected = approvedExternalAction(store, "opencode", context.sessionID, descriptor);
32164
+ let planAuthorized = false;
32165
+ if (!selected.ok) {
32166
+ const commitMessage = resolved.data.request.operation === "git.commit" ? resolved.data.request.payload.message : undefined;
32167
+ const currentBranch = resolved.data.descriptorPayload.resolved?.branch;
32168
+ if (typeof commitMessage === "string" && commitMessage) {
32169
+ const plan = approvedPlanCommit(store, "opencode", context.sessionID, commitMessage);
32170
+ planAuthorized = plan.ok && plan.data.branch === currentBranch;
32171
+ }
32172
+ }
32173
+ if (!selected.ok && !planAuthorized) {
32174
+ if (selected.error.startsWith("no approved action")) {
32175
+ const proposal = actionProposalQuestion(resolved.data.request, resolved.data.descriptorPayload);
32176
+ const pending = actionProposals.get(context.sessionID) ?? [];
32177
+ pending.push({
32178
+ descriptor,
32179
+ presented: proposal.presented,
32180
+ approvedText: proposal.approvedText,
32181
+ createdAt: now()
32182
+ });
32183
+ if (pending.length > 8)
32184
+ pending.shift();
32185
+ actionProposals.set(context.sessionID, pending);
32186
+ return output2(failure("needs_input", proposal.presented, {
32187
+ outcome: "not_started",
32188
+ operation: resolved.data.request.operation,
32189
+ proposal: {
32190
+ presented: proposal.presented,
32191
+ approvedContent: proposal.approvedText,
32192
+ descriptorDigest: sha256(descriptor)
32193
+ },
32194
+ guidance: "Ask one native question with header `Workit decision: action`, the presented text as the question text, and exactly two options: approved (description = the proposal approvedContent) and rejected (description = `Reject this decision`). Then call decision.record with the same presented/approvedContent and the native receipt; the adapter binds the exact descriptor. Never show the raw descriptor."
32195
+ }));
32196
+ }
32197
+ return output2(selected);
32198
+ }
31530
32199
  const result = await nativeExternalActionRunner(context.directory, context.sessionID, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, context.directory, step, {
31531
32200
  host: "opencode",
31532
32201
  actor: context.sessionID
@@ -31626,26 +32295,14 @@ var loadProvenance = (logger, pkgUrl) => {
31626
32295
  };
31627
32296
 
31628
32297
  // packages/workit-opencode/src/plugin.ts
31629
- var root = fileURLToPath2(new URL("../assets/", import.meta.url));
32298
+ var root = fileURLToPath3(new URL("../assets/", import.meta.url));
31630
32299
  var skillsPath = path25.join(root, "skills");
31631
32300
  var logger = createLogger({
31632
32301
  appLog: () => {
31633
32302
  return;
31634
32303
  }
31635
32304
  });
31636
- var sourceMarker = markSourcesLoaded([
31637
- fileURLToPath2(import.meta.url),
31638
- fileURLToPath2(new URL("./tools/workit.ts", import.meta.url)),
31639
- ...[
31640
- "task-contract.ts",
31641
- "task-engine.ts",
31642
- "task-evaluation.ts",
31643
- "task-store.ts",
31644
- "workers.ts",
31645
- "authority.ts",
31646
- "methods.ts"
31647
- ].map((file) => fileURLToPath2(new URL(`../../../workit-core/src/core/${file}`, import.meta.url)))
31648
- ].filter((file) => existsSync12(file)));
32305
+ var sourceMarker = markSourcesLoaded(pluginSourceFiles);
31649
32306
  var staleSourcesWarned = false;
31650
32307
  var warnStaleSources = () => {
31651
32308
  if (staleSourcesWarned)
@@ -31685,7 +32342,6 @@ var plugin = async ({ client, directory }) => {
31685
32342
  const lifecycleBindings = new Map;
31686
32343
  const unresolvedTaskLaunches = new Set;
31687
32344
  const dispatches = new Map;
31688
- const bootstrapped = new Set;
31689
32345
  try {
31690
32346
  logger.info(EVENT.initialization, { host: "opencode", plugin_root: root });
31691
32347
  logger.info(EVENT.provenance, loadProvenance(logger, new URL("../package.json", import.meta.url)));
@@ -31713,16 +32369,20 @@ var plugin = async ({ client, directory }) => {
31713
32369
  return [...items].sort((a, b) => a.entry.recordedAt < b.entry.recordedAt ? -1 : a.entry.recordedAt > b.entry.recordedAt ? 1 : a.entry.id < b.entry.id ? -1 : 1)[0];
31714
32370
  };
31715
32371
  const prepareDispatch = (coordinator, callID) => {
31716
- dispatches.delete(coordinator);
32372
+ if (dispatches.has(coordinator))
32373
+ return "unsettled";
31717
32374
  const store = new TaskStore(directory);
31718
32375
  const workspace = store.readWorkspace();
31719
32376
  const listed = store.listTasks();
31720
32377
  if (!workspace.ok || !workspace.data || !listed.ok)
31721
- return;
31722
- const eligible = listed.data.flatMap((task) => task.status === "active" && task.workspaceId === workspace.data?.id ? task.workers.filter((entry) => entry.data.state === "assigned" && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === coordinator).map((entry) => ({ task, entry })) : []);
32378
+ return "unmanaged";
32379
+ const owned = listed.data.filter((task) => task.status === "active" && task.workspaceId === workspace.data?.id && task.intent.provenance.session?.kind === "host" && task.intent.provenance.session.host === "opencode" && task.intent.provenance.session.handle === coordinator);
32380
+ if (owned.length === 0)
32381
+ return "unmanaged";
32382
+ const eligible = owned.flatMap((task) => task.workers.filter((entry) => entry.data.state === "assigned" && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === coordinator).map((entry) => ({ task, entry })));
31723
32383
  const next = oldestOfSingleTask(eligible);
31724
32384
  if (!next)
31725
- return;
32385
+ return "unbound";
31726
32386
  const generation = {
31727
32387
  coordinator,
31728
32388
  callID,
@@ -31745,7 +32405,7 @@ var plugin = async ({ client, directory }) => {
31745
32405
  observation: { stage: "prepare", sessionID: coordinator, callID }
31746
32406
  });
31747
32407
  if (!prepared.ok)
31748
- return;
32408
+ return "unbound";
31749
32409
  dispatches.set(coordinator, {
31750
32410
  generation,
31751
32411
  dispatch: prepared.data,
@@ -31753,6 +32413,7 @@ var plugin = async ({ client, directory }) => {
31753
32413
  taskId: next.task.id,
31754
32414
  workerId: next.entry.id
31755
32415
  });
32416
+ return "prepared";
31756
32417
  };
31757
32418
  const commitDispatchStart = (coordinator, childID) => {
31758
32419
  const pending = dispatches.get(coordinator);
@@ -31888,7 +32549,7 @@ var plugin = async ({ client, directory }) => {
31888
32549
  const listed = store.listTasks();
31889
32550
  if (!workspace.ok || !workspace.data || !listed.ok)
31890
32551
  return;
31891
- const candidates = listed.data.flatMap((task) => task.status === "active" && task.workspaceId === workspace.data?.id ? task.workers.filter((entry) => entry.data.state === "assigned" && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === info.parentID).map((entry) => ({ task, entry })) : []);
32552
+ const candidates = listed.data.flatMap((task) => task.status === "active" && task.workspaceId === workspace.data?.id ? task.workers.filter((entry) => (entry.data.state === "assigned" || entry.data.state === "dispatching") && entry.data.session === null && entry.provenance.session?.kind === "host" && entry.provenance.session.host === "opencode" && entry.provenance.session.handle === info.parentID).map((entry) => ({ task, entry })) : []);
31892
32553
  const next = oldestOfSingleTask(candidates);
31893
32554
  if (!next)
31894
32555
  return;
@@ -31951,8 +32612,21 @@ var plugin = async ({ client, directory }) => {
31951
32612
  unresolvedTaskLaunches.add(input.sessionID);
31952
32613
  }
31953
32614
  },
31954
- "tool.execute.before": async (input, _output) => {
32615
+ "tool.execute.before": async (input, output) => {
31955
32616
  warnStaleSources();
32617
+ if (input.tool === "bash") {
32618
+ const command = output?.args?.command;
32619
+ const route = typeof command === "string" ? shellRouteIntent(command) : null;
32620
+ if (route)
32621
+ throw new Error(`recovery_required: direct branch or PR creation bypasses the Workit route; ${route.guidance}`);
32622
+ return;
32623
+ }
32624
+ if (input.tool === "question") {
32625
+ const issue = workitBindingQuestionIssue(output?.args?.questions);
32626
+ if (issue)
32627
+ throw new Error(issue);
32628
+ return;
32629
+ }
31956
32630
  if (input.tool === "task") {
31957
32631
  const listed = new TaskStore(directory).listTasks();
31958
32632
  if (listed.ok && listed.data.some((task) => task.workers.some((worker) => (worker.data.state === "cancelling" || worker.data.state === "unknown") && worker.provenance.session?.kind === "host" && worker.provenance.session.handle === input.sessionID)))
@@ -31962,13 +32636,17 @@ var plugin = async ({ client, directory }) => {
31962
32636
  const session = await sessionData2(client, input.sessionID);
31963
32637
  if (!client || !trustedSession(directory, input.sessionID, session) || session.parentID)
31964
32638
  throw new Error("delegation_lineage_denied: native task workers must be direct children of the coordinator");
31965
- prepareDispatch(input.sessionID, input.callID);
32639
+ const prepared = prepareDispatch(input.sessionID, input.callID);
32640
+ if (prepared === "unsettled")
32641
+ throw new Error("recovery_required: a previous native task launch is unsettled; wait for it to settle or reconcile its worker before launching again");
32642
+ if (prepared === "unbound")
32643
+ throw new Error("recovery_required: no assigned Workit worker is attributable to this coordinator; assign one with workit_worker before launching a native task");
31966
32644
  }
31967
32645
  },
31968
32646
  config: async (config) => {
31969
32647
  const mutable = config;
31970
32648
  const paths = [...mutable.skills?.paths ?? []];
31971
- if (existsSync12(skillsPath) && !paths.includes(skillsPath))
32649
+ if (existsSync13(skillsPath) && !paths.includes(skillsPath))
31972
32650
  paths.push(skillsPath);
31973
32651
  mutable.skills ??= {};
31974
32652
  mutable.skills.paths = paths;
@@ -32011,8 +32689,6 @@ var plugin = async ({ client, directory }) => {
32011
32689
  if (!first || !first.parts.length)
32012
32690
  return;
32013
32691
  const sessionID = first.info.sessionID;
32014
- if (bootstrapped.has(sessionID))
32015
- return;
32016
32692
  const anchor = first.parts[0];
32017
32693
  const session = await sessionData2(client, sessionID);
32018
32694
  if (!trustedSession(directory, sessionID, session))
@@ -32037,7 +32713,6 @@ var plugin = async ({ client, directory }) => {
32037
32713
  text: `<workit-worker-context>${workerContext}</workit-worker-context>`
32038
32714
  });
32039
32715
  }
32040
- bootstrapped.add(sessionID);
32041
32716
  }
32042
32717
  };
32043
32718
  };