@brainervirus/workit-opencode 1.0.9 → 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,
@@ -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)
@@ -10403,7 +10585,7 @@ function evaluateClosure(requestedOutcome, view) {
10403
10585
  var METHODS = {
10404
10586
  "workit-challenge": { dimensions: ["challenge", "decisions"] },
10405
10587
  "workit-behavioral-tdd": { dimensions: ["testing"] },
10406
- "workit-review": { dimensions: ["review"] },
10588
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
10407
10589
  "workit-plan": { dimensions: ["artifacts", "continuity"] },
10408
10590
  "workit-implement": { dimensions: ["delegation"] },
10409
10591
  "workit-debug": { ruleIds: ["root-cause-investigation"] },
@@ -10465,7 +10647,18 @@ decision.record {binding with taskId/workspaceId from inspect plus presented and
10465
10647
  approvedContent} only through a native approval question asked receipt-shaped:
10466
10648
  header Workit decision: <purpose>, the same label repeated in the question
10467
10649
  text (some host UIs do not render headers), exactly two options approved and
10468
- rejected; ask once, never re-ask to mint a receipt. task.close {outcome,
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,
10469
10662
  summary, decisionIds}. A completed native subagent run stops its bound worker
10470
10663
  by itself; if a worker strands in cancelling with its run verifiably over,
10471
10664
  repeat worker.cancel {reason} to confirm the stop. Check and review evidence
@@ -10497,7 +10690,7 @@ function reconcileResume(view, observations = []) {
10497
10690
  return candidate;
10498
10691
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
10499
10692
  const blockers = [...view.task.progress.blockers];
10500
- if (view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
10693
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
10501
10694
  blockers.push({
10502
10695
  reason: "worker state requires reconciliation",
10503
10696
  dependentAction: "resume",
@@ -10630,6 +10823,7 @@ function shellRouteIntent(command) {
10630
10823
  return null;
10631
10824
  }
10632
10825
  // packages/workit-core/src/core/external-action.ts
10826
+ import { execFileSync } from "node:child_process";
10633
10827
  var externalActionSchema = discriminatedUnion("operation", [
10634
10828
  object({
10635
10829
  operation: literal("git.branch_setup"),
@@ -10642,7 +10836,11 @@ var externalActionSchema = discriminatedUnion("operation", [
10642
10836
  }).strict(),
10643
10837
  object({
10644
10838
  operation: literal("git.commit"),
10645
- 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()
10646
10844
  }).strict(),
10647
10845
  object({
10648
10846
  operation: literal("git.push"),
@@ -10731,7 +10929,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
10731
10929
  payload,
10732
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 } : {}
10733
10931
  });
10734
- 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).";
10735
10933
  var externalActionRef = (host, actor, descriptor) => ({
10736
10934
  kind: "host",
10737
10935
  host,
@@ -10782,6 +10980,92 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
10782
10980
  return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
10783
10981
  return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
10784
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
+ };
10785
11069
  var approvedExternalAction = (store, host, actor, operation) => {
10786
11070
  const selected = externalActionState(store, host, actor, operation);
10787
11071
  if (!selected.ok)
@@ -10790,6 +11074,23 @@ var approvedExternalAction = (store, host, actor, operation) => {
10790
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");
10791
11075
  return selected;
10792
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
+ };
10793
11094
  var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
10794
11095
  let steps = [];
10795
11096
  try {
@@ -11067,7 +11368,7 @@ function resolveRequirements(input) {
11067
11368
  ].join(", ")}`,
11068
11369
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
11069
11370
  before: "dependent_action",
11070
- dependentAction: "dependent-action",
11371
+ dependentAction: null,
11071
11372
  acceptanceAllowed: false
11072
11373
  }));
11073
11374
  if (signals.productChoiceOpen.value === true)
@@ -11078,7 +11379,7 @@ function resolveRequirements(input) {
11078
11379
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
11079
11380
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
11080
11381
  before: "dependent_action",
11081
- dependentAction: "dependent-action",
11382
+ dependentAction: null,
11082
11383
  acceptanceAllowed: false
11083
11384
  }));
11084
11385
  if (signals.behaviorChange.value === true)
@@ -11236,59 +11537,6 @@ function diffPolicy(previous, next, reason, now) {
11236
11537
  return parsed.data;
11237
11538
  }
11238
11539
 
11239
- // packages/workit-core/src/core/workers.ts
11240
- var same = (left, right) => {
11241
- try {
11242
- return canonicalJson(left) === canonicalJson(right);
11243
- } catch {
11244
- return false;
11245
- }
11246
- };
11247
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
11248
- var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
11249
- const code = char.charCodeAt(0);
11250
- return code < 32 || code === 127;
11251
- }) && !value.split(/[\\/]/).includes("..");
11252
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
11253
- var workerIdOf = (value) => value.workerId ?? null;
11254
- function assertProductWriteAllowed(input) {
11255
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
11256
- return failure("invalid_input", "invalid product write path");
11257
- if (!input.store)
11258
- return failure("permission_denied", "write authorization requires core state");
11259
- const task = input.store.readTask(input.task.id);
11260
- if (!task.ok)
11261
- return task;
11262
- const workspace = input.store.readWorkspace();
11263
- if (!workspace.ok)
11264
- return workspace;
11265
- if (!workspace.data)
11266
- return failure("not_found", "workspace not found");
11267
- const workspaceRecord = workspace.data;
11268
- if (task.data.status !== "active")
11269
- return failure("invalid_transition", "paused or closed tasks cannot own product writes");
11270
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
11271
- return failure("recovery_required", "task and workspace bindings are invalid");
11272
- const writer = workspaceRecord.writer;
11273
- if (!writer)
11274
- return failure("permission_denied", "checkout has no writer owner");
11275
- if (writer.state === "uncertain")
11276
- return failure("recovery_required", "checkout writer requires recovery", {
11277
- owner: writer.owner
11278
- });
11279
- const caller = callerValue(input.caller);
11280
- const workerId = workerIdOf(input.caller);
11281
- const owner = writer.owner;
11282
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
11283
- return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
11284
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
11285
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
11286
- return failure("permission_denied", "only implementers can own product writes");
11287
- 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))))
11288
- return failure("recovery_required", "worker session is not an active writer");
11289
- return success(null, workspaceRecord.revision, owner);
11290
- }
11291
-
11292
11540
  // packages/workit-core/src/core/task-engine.ts
11293
11541
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
11294
11542
  kind,
@@ -11419,15 +11667,20 @@ var applyWorkerLifecycle = (input) => {
11419
11667
  return failure("invalid_transition", "worker has not started");
11420
11668
  if (input.state === "running" && entry.data.state === "stopped")
11421
11669
  return failure("invalid_transition", "stopped worker cannot run again");
11670
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
11422
11671
  const nextEntry = {
11423
11672
  ...entry,
11424
11673
  recordedAt: input.now ?? entry.recordedAt,
11425
11674
  provenance: authority.provenance,
11426
- 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
+ }
11427
11680
  };
11428
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;
11429
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;
11430
- 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)
11431
11684
  return success(task.data.revision, workspace.data.revision, entry);
11432
11685
  const changed = input.store.mutateTaskAndWorkspace({
11433
11686
  taskId: task.data.id,
@@ -11797,7 +12050,7 @@ class WorkitCore {
11797
12050
  activeWorkerBlocker(task, workspace) {
11798
12051
  if (workspace.writer)
11799
12052
  return failure("recovery_required", "writer ownership must be released first");
11800
- if (task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
12053
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
11801
12054
  return failure("recovery_required", "worker state requires reconciliation");
11802
12055
  return null;
11803
12056
  }
@@ -12034,11 +12287,6 @@ class WorkitCore {
12034
12287
  if (helper && !helper.ok)
12035
12288
  return helper;
12036
12289
  const evidence = input.evidence;
12037
- if (helper?.ok) {
12038
- const assignment = helper.data.data.assignment;
12039
- 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))
12040
- return failure("permission_denied", "evidence is outside the worker assignment");
12041
- }
12042
12290
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
12043
12291
  return failure("invalid_input", "missing or skipped evidence requires a reason");
12044
12292
  if (evidence.kind === "review") {
@@ -12055,6 +12303,11 @@ class WorkitCore {
12055
12303
  evidence.beforeCandidateId ??= null;
12056
12304
  evidence.candidateId ??= null;
12057
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
+ }
12058
12311
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
12059
12312
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
12060
12313
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -12950,6 +13203,9 @@ class WorkitCore {
12950
13203
  id: task.id,
12951
13204
  revision: task.revision,
12952
13205
  workspaceRevision: workspace.data.revision,
13206
+ createdAt: task.createdAt,
13207
+ updatedAt: task.updatedAt,
13208
+ runtime: task.runtime ?? null,
12953
13209
  objective: task.intent.data.objective,
12954
13210
  status: task.status,
12955
13211
  closure: task.closure,
@@ -13035,7 +13291,7 @@ class WorkitCore {
13035
13291
  const base = reconcileResume(effectiveView);
13036
13292
  if (!base.ok)
13037
13293
  return base;
13038
- const needsReconciliation = view.task.workers.some((entry) => ["dispatching", "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));
13039
13295
  const blockers = [...base.data.blockers];
13040
13296
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
13041
13297
  blockers.push({
@@ -13102,7 +13358,8 @@ class WorkitCore {
13102
13358
  ...current,
13103
13359
  status,
13104
13360
  candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
13105
- progress: status === "paused" ? { ...current.progress, summary: input.reason } : current.progress
13361
+ progress: current.progress,
13362
+ pauseReason: status === "paused" ? input.reason ?? null : null
13106
13363
  })
13107
13364
  });
13108
13365
  if (!changed.ok)
@@ -13138,7 +13395,7 @@ class WorkitCore {
13138
13395
  return workspace;
13139
13396
  if (!workspace.data)
13140
13397
  return failure("not_found", "workspace not found");
13141
- 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)))
13142
13399
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
13143
13400
  this.fillRevisions(input, task.data, workspace.data);
13144
13401
  const changed = this.store.mutateTaskAndWorkspace({
@@ -13256,15 +13513,15 @@ function resolveGitRevision(root, value) {
13256
13513
  }
13257
13514
  function resolveInside(root, candidate) {
13258
13515
  const base = realpathSync5(root);
13259
- const target = path6.resolve(base, candidate);
13260
- if (target !== base && !target.startsWith(base + path6.sep)) {
13516
+ const target = path7.resolve(base, candidate);
13517
+ if (target !== base && !target.startsWith(base + path7.sep)) {
13261
13518
  throw new Error("path must stay inside repository root");
13262
13519
  }
13263
13520
  let ancestor = target;
13264
- while (!existsSync3(ancestor))
13265
- ancestor = path6.dirname(ancestor);
13521
+ while (!existsSync4(ancestor))
13522
+ ancestor = path7.dirname(ancestor);
13266
13523
  const canonicalAncestor = realpathSync5(ancestor);
13267
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path6.sep)) {
13524
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
13268
13525
  throw new Error("path must stay inside repository root");
13269
13526
  }
13270
13527
  return target;
@@ -13306,7 +13563,7 @@ if (false) {}
13306
13563
  // packages/workit-core/src/core/logger.ts
13307
13564
  import { appendFileSync, mkdirSync as mkdirSync2, readdirSync as readdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
13308
13565
  import os from "node:os";
13309
- import path7 from "node:path";
13566
+ import path8 from "node:path";
13310
13567
  var REDACTED = "[REDACTED]";
13311
13568
  var DEFAULT_MAX_RATE = 20;
13312
13569
  var DEFAULT_RATE_WINDOW_MS = 1000;
@@ -13362,14 +13619,14 @@ var resolveStateDir = () => {
13362
13619
  return override;
13363
13620
  const home = os.homedir();
13364
13621
  if (process.env.XDG_STATE_HOME)
13365
- return path7.join(process.env.XDG_STATE_HOME, "workit");
13622
+ return path8.join(process.env.XDG_STATE_HOME, "workit");
13366
13623
  switch (os.platform()) {
13367
13624
  case "darwin":
13368
- return path7.join(home, "Library", "Application Support", "workit");
13625
+ return path8.join(home, "Library", "Application Support", "workit");
13369
13626
  case "win32":
13370
- 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");
13371
13628
  default:
13372
- return path7.join(process.env.HOME ?? home, ".local", "state", "workit");
13629
+ return path8.join(process.env.HOME ?? home, ".local", "state", "workit");
13373
13630
  }
13374
13631
  };
13375
13632
  var homePattern = new RegExp(`${escapeRegExp(os.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
@@ -13442,14 +13699,14 @@ var pruneOldFiles = (dir) => {
13442
13699
  const files = readdirSync3(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
13443
13700
  for (const file of files.slice(RETAINED_DAYS)) {
13444
13701
  try {
13445
- unlinkSync2(path7.join(dir, file));
13702
+ unlinkSync2(path8.join(dir, file));
13446
13703
  } catch {}
13447
13704
  }
13448
13705
  } catch {}
13449
13706
  };
13450
13707
  var createLogger = (options = {}) => {
13451
13708
  const stateDir = options.stateDir ?? resolveStateDir();
13452
- const logDir = path7.join(stateDir, "logs");
13709
+ const logDir = path8.join(stateDir, "logs");
13453
13710
  const now = options.now ?? (() => new Date);
13454
13711
  const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
13455
13712
  const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
@@ -13483,7 +13740,7 @@ var createLogger = (options = {}) => {
13483
13740
  const line = `${JSON.stringify(event)}
13484
13741
  `;
13485
13742
  mkdirSync2(logDir, { recursive: true, mode: 448 });
13486
- appendFileSync(path7.join(logDir, dailyFileName(date)), line, { mode: 384 });
13743
+ appendFileSync(path8.join(logDir, dailyFileName(date)), line, { mode: 384 });
13487
13744
  pruneOldFiles(logDir);
13488
13745
  } catch {
13489
13746
  try {
@@ -13494,7 +13751,7 @@ var createLogger = (options = {}) => {
13494
13751
  context: { redaction_failed: true }
13495
13752
  };
13496
13753
  mkdirSync2(logDir, { recursive: true, mode: 448 });
13497
- appendFileSync(path7.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
13754
+ appendFileSync(path8.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
13498
13755
  `, {
13499
13756
  mode: 384
13500
13757
  });
@@ -13577,6 +13834,24 @@ OpenCode Workit decisions use the native question header "Workit decision: <purp
13577
13834
  return cached2;
13578
13835
  };
13579
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
+
13580
13855
  // packages/workit-opencode/src/tools/repo.ts
13581
13856
  import path22 from "node:path";
13582
13857
  import { realpathSync as realpathSync8 } from "node:fs";
@@ -25906,25 +26181,6 @@ import fs7 from "node:fs";
25906
26181
  import path9 from "node:path";
25907
26182
  import { createHash as createHash5 } from "node:crypto";
25908
26183
 
25909
- // packages/workit-core/src/core/package-root.ts
25910
- import { existsSync as existsSync4 } from "node:fs";
25911
- import path8 from "node:path";
25912
- import { fileURLToPath } from "node:url";
25913
- var packageRoot = () => {
25914
- let dir = path8.dirname(fileURLToPath(import.meta.url));
25915
- while (true) {
25916
- const parent = path8.dirname(dir);
25917
- if (existsSync4(path8.join(dir, "package.json")) || parent === dir)
25918
- return dir;
25919
- dir = parent;
25920
- }
25921
- };
25922
- var assetRoot = () => {
25923
- const root = packageRoot();
25924
- const assets = path8.join(root, "assets");
25925
- return existsSync4(assets) ? assets : root;
25926
- };
25927
-
25928
26184
  // packages/workit-core/src/core/scripts.ts
25929
26185
  var PLUGIN_ROOT = packageRoot();
25930
26186
  var ASSET_ROOT = assetRoot();
@@ -26257,10 +26513,10 @@ function changelogApply({
26257
26513
  }
26258
26514
 
26259
26515
  // packages/workit-core/src/core/git.ts
26260
- import { execFileSync } from "node:child_process";
26516
+ import { execFileSync as execFileSync2 } from "node:child_process";
26261
26517
  var run2 = (cwd, args) => {
26262
26518
  try {
26263
- const stdout = execFileSync("git", args, {
26519
+ const stdout = execFileSync2("git", args, {
26264
26520
  cwd,
26265
26521
  encoding: "utf8",
26266
26522
  stdio: ["pipe", "pipe", "pipe"]
@@ -26378,15 +26634,15 @@ function parseVerifyOutput(stdout) {
26378
26634
  }
26379
26635
 
26380
26636
  // packages/workit-core/src/core/branch.ts
26381
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
26382
- 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";
26383
26639
  import path13 from "node:path";
26384
26640
 
26385
26641
  // packages/workit-core/src/core/config.ts
26386
26642
  import {
26387
26643
  copyFileSync,
26388
26644
  cpSync,
26389
- existsSync as existsSync5,
26645
+ existsSync as existsSync6,
26390
26646
  mkdirSync as mkdirSync3,
26391
26647
  readFileSync as readFileSync4,
26392
26648
  readdirSync as readdirSync4,
@@ -26426,11 +26682,11 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
26426
26682
  return dir;
26427
26683
  }
26428
26684
  const legacy = path10.join(process.env.XDG_CONFIG_HOME || path10.join(os2.homedir(), ".config"), "workflow-toolkit");
26429
- if (!existsSync5(legacy)) {
26685
+ if (!existsSync6(legacy)) {
26430
26686
  migratedDir = dir;
26431
26687
  return dir;
26432
26688
  }
26433
- if (!migrationFailed && existsSync5(dir)) {
26689
+ if (!migrationFailed && existsSync6(dir)) {
26434
26690
  migratedDir = dir;
26435
26691
  return dir;
26436
26692
  }
@@ -26440,7 +26696,7 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
26440
26696
  for (const entry of readdirSync4(legacy, { withFileTypes: true })) {
26441
26697
  const src = path10.join(legacy, entry.name);
26442
26698
  const dest = path10.join(dir, entry.name);
26443
- if (existsSync5(dest))
26699
+ if (existsSync6(dest))
26444
26700
  continue;
26445
26701
  try {
26446
26702
  if (entry.isDirectory())
@@ -27018,7 +27274,7 @@ var baseBranch = (cwd) => {
27018
27274
  return { base: String(resolved.defaultTargetBranch ?? "develop") };
27019
27275
  };
27020
27276
  var originBaseReady = (cwd, base) => {
27021
- 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"] });
27022
27278
  try {
27023
27279
  try {
27024
27280
  run(["fetch", "origin", base, "--prune"]);
@@ -27026,7 +27282,7 @@ var originBaseReady = (cwd, base) => {
27026
27282
  run(["fetch", "origin", "--prune"]);
27027
27283
  }
27028
27284
  try {
27029
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
27285
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
27030
27286
  cwd,
27031
27287
  stdio: "pipe"
27032
27288
  });
@@ -27045,11 +27301,11 @@ var originBaseReady = (cwd, base) => {
27045
27301
  }
27046
27302
  };
27047
27303
  var fastForwardBase = (cwd, base) => {
27048
- 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"] });
27049
27305
  try {
27050
27306
  let hasLocalBase = true;
27051
27307
  try {
27052
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
27308
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
27053
27309
  cwd,
27054
27310
  stdio: "pipe"
27055
27311
  });
@@ -27090,18 +27346,17 @@ var branchSetup = ({
27090
27346
  log
27091
27347
  }) => {
27092
27348
  const cwd = path13.resolve(workspace_root);
27093
- 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"] });
27094
27350
  const current = gitContext(cwd).branch;
27095
27351
  if (!current || current === "unknown")
27096
- return { error: "not in a git repository" };
27352
+ return { error: "not in a git repository", phase: "preflight" };
27097
27353
  const sdd = sdd_dir ?? "docs";
27098
27354
  const manifestPath = path13.isAbsolute(sdd) ? path13.join(sdd, "manifest.json") : path13.join(cwd, sdd, "manifest.json");
27099
27355
  const ensureManifestDir = () => {
27100
27356
  const dir = path13.dirname(manifestPath);
27101
- if (!existsSync6(dir))
27357
+ if (!existsSync7(dir))
27102
27358
  mkdirSync4(dir, { recursive: true, mode: 493 });
27103
27359
  };
27104
- ensureManifestDir();
27105
27360
  const readManifest = () => {
27106
27361
  try {
27107
27362
  return JSON.parse(readFileSync6(manifestPath, "utf8"));
@@ -27119,13 +27374,16 @@ var branchSetup = ({
27119
27374
  const manifest = readManifest();
27120
27375
  const ref = manifest.stash_ref;
27121
27376
  if (!ref)
27122
- return { error: "no stash_ref in manifest" };
27377
+ return { error: "no stash_ref in manifest", phase: "preflight" };
27123
27378
  journal(`pre-pop: ${String(ref)}`);
27124
27379
  try {
27125
27380
  exec(["stash", "pop", String(ref)]);
27126
27381
  } catch (error) {
27127
27382
  journal("pop: failed");
27128
- 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
+ };
27129
27387
  }
27130
27388
  journal("pop: ok");
27131
27389
  delete manifest.stash_ref;
@@ -27135,11 +27393,17 @@ var branchSetup = ({
27135
27393
  }
27136
27394
  const target = target_branch ?? "";
27137
27395
  if (!target)
27138
- 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
+ };
27139
27400
  if (isProtected(cwd, target))
27140
- return { error: `protected branch ${target}` };
27401
+ return { error: `protected branch ${target}`, phase: "preflight" };
27141
27402
  if (!allowedBranch(cwd, target))
27142
- 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
+ };
27143
27407
  let base;
27144
27408
  let targetExists = true;
27145
27409
  try {
@@ -27150,12 +27414,12 @@ var branchSetup = ({
27150
27414
  if (!targetExists) {
27151
27415
  const baseResolved = baseBranch(cwd);
27152
27416
  if ("error" in baseResolved)
27153
- return { error: baseResolved.error };
27417
+ return { error: baseResolved.error, phase: "preflight" };
27154
27418
  base = baseResolved.base;
27155
27419
  }
27156
27420
  journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
27157
27421
  let stash_ref;
27158
- const failAfterStash = (message) => {
27422
+ const failAfterStash = (message, phase = "post_mutation") => {
27159
27423
  let suffix = "";
27160
27424
  if (stash_ref) {
27161
27425
  journal(`pre-pop: ${stash_ref}`);
@@ -27168,27 +27432,31 @@ var branchSetup = ({
27168
27432
  suffix = " (changes preserved in stash)";
27169
27433
  }
27170
27434
  }
27171
- return { error: `${message}${suffix}` };
27435
+ return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
27172
27436
  };
27173
27437
  if (current !== target) {
27174
27438
  const dirty = Boolean(gitContext(cwd).status_short.trim());
27175
27439
  if (dirty && stash !== "yes") {
27176
27440
  return {
27177
- 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"
27178
27443
  };
27179
27444
  }
27180
27445
  let validatedBase;
27181
27446
  if (!targetExists && base !== undefined) {
27182
27447
  const ready = originBaseReady(cwd, base);
27183
27448
  if (!ready.ok)
27184
- return { error: ready.error ?? "ensure-base-branch failed" };
27449
+ return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
27185
27450
  validatedBase = base;
27186
27451
  }
27187
27452
  if (dirty) {
27188
27453
  try {
27189
27454
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
27190
27455
  } catch (error) {
27191
- 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
+ };
27192
27460
  }
27193
27461
  stash_ref = "stash@{0}";
27194
27462
  journal(`stash push: ${stash_ref}`);
@@ -27199,14 +27467,14 @@ var branchSetup = ({
27199
27467
  } catch (error) {
27200
27468
  const message = error instanceof Error ? error.message : "checkout failed";
27201
27469
  if (/worktree/i.test(message)) {
27202
- 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");
27203
27471
  }
27204
27472
  try {
27205
27473
  let effectiveBase = base;
27206
27474
  if (effectiveBase === undefined) {
27207
27475
  const lateResolved = baseBranch(cwd);
27208
27476
  if ("error" in lateResolved)
27209
- return failAfterStash(lateResolved.error);
27477
+ return failAfterStash(lateResolved.error, "preflight");
27210
27478
  effectiveBase = lateResolved.base;
27211
27479
  }
27212
27480
  const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
@@ -27252,7 +27520,7 @@ var branchSetup = ({
27252
27520
  };
27253
27521
 
27254
27522
  // packages/workit-core/src/core/gitignore.ts
27255
- 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";
27256
27524
  import path14 from "node:path";
27257
27525
  var GITIGNORE_ENTRIES = [
27258
27526
  "# workit: SDD working state (never commit)",
@@ -27274,7 +27542,7 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
27274
27542
  if (!confirmed)
27275
27543
  return { ok: false, error: "confirmed: true required" };
27276
27544
  const file = path14.join(workspaceRoot, ".gitignore");
27277
- const existing = existsSync7(file) ? readFileSync7(file, "utf8") : "";
27545
+ const existing = existsSync8(file) ? readFileSync7(file, "utf8") : "";
27278
27546
  const existingLines = new Set(existing.split(`
27279
27547
  `).map((l) => l.trim()).filter(Boolean));
27280
27548
  const added = [];
@@ -27293,19 +27561,19 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
27293
27561
  ` : "") + append.join(`
27294
27562
  `) + `
27295
27563
  `, "utf8");
27296
- } else if (!existsSync7(file)) {
27564
+ } else if (!existsSync8(file)) {
27297
27565
  writeFileSync4(file, "", "utf8");
27298
27566
  }
27299
27567
  return { ok: true, path: file, added };
27300
27568
  };
27301
27569
 
27302
27570
  // packages/workit-core/src/core/hygiene.ts
27303
- 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";
27304
27572
  import path15 from "node:path";
27305
27573
  var repoRoot = assetRoot();
27306
27574
  var templatesDir = () => path15.join(repoRoot, "templates", "hygiene");
27307
27575
  var packageJson = (root) => {
27308
- if (!existsSync8(path15.join(root, "package.json")))
27576
+ if (!existsSync9(path15.join(root, "package.json")))
27309
27577
  return null;
27310
27578
  try {
27311
27579
  return JSON.parse(readFileSync8(path15.join(root, "package.json"), "utf8"));
@@ -27314,7 +27582,7 @@ var packageJson = (root) => {
27314
27582
  }
27315
27583
  };
27316
27584
  var isOpenSource = (root) => {
27317
- if (existsSync8(path15.join(root, "LICENSE")))
27585
+ if (existsSync9(path15.join(root, "LICENSE")))
27318
27586
  return true;
27319
27587
  const pkg = packageJson(root);
27320
27588
  if (pkg && !pkg.private)
@@ -27342,10 +27610,10 @@ var planHygieneFiles = (root, opts = {}) => {
27342
27610
  files.push("LICENSE", "CONTRIBUTING.md");
27343
27611
  const planned = [];
27344
27612
  for (const file of files) {
27345
- if (existsSync8(path15.join(root, file)))
27613
+ if (existsSync9(path15.join(root, file)))
27346
27614
  continue;
27347
27615
  const tpl = path15.join(templatesDir(), file);
27348
- if (!existsSync8(tpl))
27616
+ if (!existsSync9(tpl))
27349
27617
  continue;
27350
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));
27351
27619
  planned.push({ path: path15.join(root, file), content });
@@ -27365,7 +27633,7 @@ var ensureHygieneFiles = (root, opts) => {
27365
27633
 
27366
27634
  // packages/workit-core/src/core/repo-context.ts
27367
27635
  import { spawnSync as spawnSync4 } from "node:child_process";
27368
- 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";
27369
27637
  import path16 from "node:path";
27370
27638
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
27371
27639
  try {
@@ -27532,7 +27800,7 @@ var diffStatForRange = (cwd, range) => {
27532
27800
  };
27533
27801
  var packageScripts = (cwd, keys) => {
27534
27802
  const pkgPath = path16.join(cwd, "package.json");
27535
- if (!existsSync9(pkgPath))
27803
+ if (!existsSync10(pkgPath))
27536
27804
  return [];
27537
27805
  try {
27538
27806
  const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
@@ -27567,7 +27835,7 @@ var documentationFiles = (cwd) => {
27567
27835
  return matches.sort().slice(0, 200);
27568
27836
  };
27569
27837
  var readTrimmed = (file, maxLines) => {
27570
- if (!existsSync9(file))
27838
+ if (!existsSync10(file))
27571
27839
  return "";
27572
27840
  const lines = readFileSync9(file, "utf8").split(`
27573
27841
  `);
@@ -27643,7 +27911,7 @@ ${status.stderr}` : "");
27643
27911
  stdout += `
27644
27912
  `;
27645
27913
  stdout += printSection("Recent Validation Signals");
27646
- if (existsSync9(path16.join(cwd, "package.json"))) {
27914
+ if (existsSync10(path16.join(cwd, "package.json"))) {
27647
27915
  stdout += `package.json detected. Common scripts:
27648
27916
  `;
27649
27917
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -27652,7 +27920,7 @@ ${status.stderr}` : "");
27652
27920
  `) + `
27653
27921
  `;
27654
27922
  }
27655
- 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"))) {
27656
27924
  stdout += `Rust project detected.
27657
27925
  `;
27658
27926
  }
@@ -27704,7 +27972,7 @@ function changelogContext(root, range) {
27704
27972
  `;
27705
27973
  stdout += printSection("Existing CHANGELOG.md");
27706
27974
  const changelogPath = path16.join(cwd, "CHANGELOG.md");
27707
- if (existsSync9(changelogPath)) {
27975
+ if (existsSync10(changelogPath)) {
27708
27976
  stdout += readTrimmed(changelogPath, 260) + `
27709
27977
  `;
27710
27978
  } else {
@@ -27746,7 +28014,7 @@ function docsRefreshContext(root, range) {
27746
28014
  `;
27747
28015
  stdout += printSection("README Preview");
27748
28016
  const readme = path16.join(cwd, "README.md");
27749
- if (existsSync9(readme)) {
28017
+ if (existsSync10(readme)) {
27750
28018
  stdout += readTrimmed(readme, 220) + `
27751
28019
  `;
27752
28020
  } else {
@@ -27755,7 +28023,7 @@ function docsRefreshContext(root, range) {
27755
28023
  }
27756
28024
  stdout += printSection("Package Scripts");
27757
28025
  const pkgPath = path16.join(cwd, "package.json");
27758
- if (existsSync9(pkgPath)) {
28026
+ if (existsSync10(pkgPath)) {
27759
28027
  try {
27760
28028
  const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
27761
28029
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
@@ -27806,7 +28074,7 @@ function releaseNotesContext(root, rangeOrTag) {
27806
28074
  `;
27807
28075
  stdout += printSection("Existing Release Files");
27808
28076
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
27809
- if (existsSync9(path16.join(cwd, rel)))
28077
+ if (existsSync10(path16.join(cwd, rel)))
27810
28078
  stdout += rel + `
27811
28079
  `;
27812
28080
  }
@@ -27835,7 +28103,7 @@ function releaseNotesContext(root, rangeOrTag) {
27835
28103
 
27836
28104
  // packages/workit-core/src/core/verify-project.ts
27837
28105
  import { spawnSync as spawnSync5 } from "node:child_process";
27838
- 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";
27839
28107
  import path17 from "node:path";
27840
28108
  var commandOnPath = (name) => {
27841
28109
  const dirs = (process.env.PATH ?? "").split(path17.delimiter);
@@ -27860,7 +28128,7 @@ var commandOnPath = (name) => {
27860
28128
  };
27861
28129
  var hasScript = (cwd, name) => {
27862
28130
  const pkgPath = path17.join(cwd, "package.json");
27863
- if (!existsSync10(pkgPath))
28131
+ if (!existsSync11(pkgPath))
27864
28132
  return false;
27865
28133
  try {
27866
28134
  const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
@@ -27870,10 +28138,10 @@ var hasScript = (cwd, name) => {
27870
28138
  }
27871
28139
  };
27872
28140
  var packageRunner = (cwd) => {
27873
- if (existsSync10(path17.join(cwd, "pnpm-lock.yaml")))
28141
+ if (existsSync11(path17.join(cwd, "pnpm-lock.yaml")))
27874
28142
  return "pnpm";
27875
28143
  const pkgPath = path17.join(cwd, "package.json");
27876
- if (existsSync10(pkgPath)) {
28144
+ if (existsSync11(pkgPath)) {
27877
28145
  try {
27878
28146
  const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
27879
28147
  if (typeof pkg.packageManager === "string" && pkg.packageManager.startsWith("pnpm")) {
@@ -27881,7 +28149,7 @@ var packageRunner = (cwd) => {
27881
28149
  }
27882
28150
  } catch {}
27883
28151
  }
27884
- if (existsSync10(path17.join(cwd, "yarn.lock")))
28152
+ if (existsSync11(path17.join(cwd, "yarn.lock")))
27885
28153
  return "yarn";
27886
28154
  return "npm";
27887
28155
  };
@@ -27942,7 +28210,7 @@ root: ${cwd}
27942
28210
  lines.push(`dry_run: ${String(dryRun)}
27943
28211
  `);
27944
28212
  const pkgPath = path17.join(cwd, "package.json");
27945
- if (existsSync10(pkgPath)) {
28213
+ if (existsSync11(pkgPath)) {
27946
28214
  const runner = packageRunner(cwd);
27947
28215
  for (const script of ["lint", "format:check", "test", "build"]) {
27948
28216
  if (hasScript(cwd, script)) {
@@ -27957,7 +28225,7 @@ root: ${cwd}
27957
28225
  } else {
27958
28226
  skipCheck("javascript", "package.json not found");
27959
28227
  }
27960
- 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" : "";
27961
28229
  if (cargoManifest) {
27962
28230
  runCheck("cargo fmt", "cargo", ["fmt", "--manifest-path", cargoManifest, "--", "--check"]);
27963
28231
  runCheck("cargo clippy", "cargo", [
@@ -27973,7 +28241,7 @@ root: ${cwd}
27973
28241
  } else {
27974
28242
  skipCheck("rust", "Cargo.toml not found");
27975
28243
  }
27976
- 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"))) {
27977
28245
  if (commandOnPath("pytest"))
27978
28246
  runCheck("pytest", "pytest", []);
27979
28247
  else
@@ -27995,7 +28263,7 @@ root: ${cwd}
27995
28263
  lines.push(`status: skipped (dry run)
27996
28264
  `);
27997
28265
  skipped += 1;
27998
- } else if (existsSync10(changelogPath)) {
28266
+ } else if (existsSync11(changelogPath)) {
27999
28267
  if (/## \[Unreleased\]/i.test(readFileSync10(changelogPath, "utf8"))) {
28000
28268
  lines.push(`status: pass
28001
28269
  `);
@@ -28330,7 +28598,7 @@ function writeFileExclusive(file, content, mode) {
28330
28598
  import {
28331
28599
  copyFileSync as copyFileSync2,
28332
28600
  cpSync as cpSync2,
28333
- existsSync as existsSync11,
28601
+ existsSync as existsSync12,
28334
28602
  lstatSync as lstatSync4,
28335
28603
  mkdirSync as mkdirSync5,
28336
28604
  mkdtempSync,
@@ -29871,7 +30139,7 @@ function createRepoTools(runtime = defaultRuntime) {
29871
30139
  return output(legacyScriptResult({
29872
30140
  stdout: JSON.stringify(result),
29873
30141
  stderr: "",
29874
- exitCode: result.error ? 1 : 0,
30142
+ exitCode: "error" in result ? 1 : 0,
29875
30143
  cwd: context.directory
29876
30144
  }));
29877
30145
  }
@@ -30519,6 +30787,94 @@ var safePath = (root, value) => {
30519
30787
  return null;
30520
30788
  }
30521
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
+ };
30522
30878
  var approvedResolvedExternalAction = (content) => {
30523
30879
  try {
30524
30880
  const value = JSON.parse(content);
@@ -30782,24 +31138,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30782
31138
  const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
30783
31139
  if (!sdd_dir)
30784
31140
  return failure("invalid_input", "sdd_dir must stay inside the workspace");
31141
+ const action = request.payload.action ?? "setup";
30785
31142
  const policy = resolveBranchPolicyFor(root);
30786
- 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
+ });
30787
31149
  const head = gitValue(root, ["rev-parse", "HEAD"]);
30788
31150
  if (!head)
30789
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);
30790
31154
  const normalized = {
30791
31155
  operation: request.operation,
30792
31156
  payload: {
30793
31157
  ...request.payload,
30794
- action: request.payload.action ?? "setup",
31158
+ action,
30795
31159
  sdd_dir,
30796
31160
  stash: request.payload.stash ?? "no",
30797
- target_branch
31161
+ ...target_branch ? { target_branch } : {}
30798
31162
  }
30799
31163
  };
30800
31164
  return success(null, null, {
30801
31165
  request: normalized,
30802
- descriptorPayload: { ...normalized.payload, resolved: { head, target_branch } }
31166
+ descriptorPayload: {
31167
+ ...normalized.payload,
31168
+ resolved: { head, dirty, ...creation }
31169
+ }
30803
31170
  });
30804
31171
  }
30805
31172
  case "git.push": {
@@ -30813,7 +31180,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30813
31180
  }) : failure("invalid_input", "git push requires a resolvable current branch");
30814
31181
  }
30815
31182
  case "git.commit": {
31183
+ const planSteps = request.payload.plan_steps;
31184
+ const planBranch = request.payload.plan_branch;
30816
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
+ }
30817
31214
  const staged = gitRaw(root, [
30818
31215
  "diff",
30819
31216
  "--cached",
@@ -30824,7 +31221,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30824
31221
  "HEAD"
30825
31222
  ]);
30826
31223
  const paths = stagedPaths(root);
30827
- 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)
30828
31231
  return failure("storage_error", "Git state could not be resolved");
30829
31232
  let policy;
30830
31233
  try {
@@ -30839,13 +31242,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30839
31242
  `).filter(Boolean));
30840
31243
  flavor = detected.flavor ?? "conventional";
30841
31244
  }
30842
- if (!matchCommitFlavor(request.payload.message, flavor, policy.pattern))
31245
+ if (!matchCommitFlavor(message, flavor, policy.pattern))
30843
31246
  return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
30844
31247
  return success(null, null, {
30845
31248
  request,
30846
31249
  descriptorPayload: {
30847
31250
  ...request.payload,
30848
- resolved: { head, staged: sha256(staged), paths }
31251
+ resolved: { head, branch, staged: sha256(staged), paths }
30849
31252
  }
30850
31253
  });
30851
31254
  }
@@ -31015,10 +31418,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
31015
31418
  sdd_dir: request.payload.sdd_dir ?? "docs",
31016
31419
  workspace_root: root
31017
31420
  });
31018
- 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);
31019
31430
  }
31020
31431
  case "git.commit": {
31021
- 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]);
31022
31439
  return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown3(request.operation);
31023
31440
  }
31024
31441
  case "git.push": {
@@ -31130,6 +31547,14 @@ var sessionParent = (session) => {
31130
31547
  };
31131
31548
  var freshMs = 5 * 60 * 1000;
31132
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
+ };
31133
31558
  var decisionContent = (purpose, question, approvedContent) => ({
31134
31559
  header: `Workit decision: ${purpose}`,
31135
31560
  question,
@@ -31243,7 +31668,27 @@ class NativeReceiptStore {
31243
31668
  }
31244
31669
  return {
31245
31670
  ok: false,
31246
- error: "permission_denied: no matching native question receipt for this purpose; 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"
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
+ })()
31247
31692
  };
31248
31693
  }
31249
31694
  verify(observation, sessionID, purpose) {
@@ -31345,6 +31790,13 @@ var opencodeCapabilities = () => [
31345
31790
  reason: "nested native task launches are denied and observed child sessions are parent-bound",
31346
31791
  refs: [hostRef("task")]
31347
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
+ },
31348
31800
  {
31349
31801
  name: "known_product_writes",
31350
31802
  surface: "write/edit/bash",
@@ -31363,7 +31815,8 @@ var opencodeCapabilities = () => [
31363
31815
  var nativeAuthority = (receipts, actor, reconciliationTokens = new WeakSet) => ({
31364
31816
  verifyDecision: ({ observation, expected, caller }) => {
31365
31817
  const receipt = receipts.verify(observation, actor, "decision");
31366
- if (!receipt || caller.host !== "opencode" || caller.actor !== actor || normalizeReceiptLabel(receipt.selectedLabel) !== normalizeReceiptLabel(expected.response) || expected.response === "approved" && receipt.selectedDescription !== expected.binding.approvedContent || 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)
31367
31820
  return failure("permission_denied", "native decision receipt is not bound to this session");
31368
31821
  return success(null, null, {
31369
31822
  kind: "host_observed",
@@ -31438,8 +31891,34 @@ var nativeDispatchFor = (directChildren, actor, generation) => ({
31438
31891
  var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
31439
31892
  const store = new TaskStore(root);
31440
31893
  const selected = approvedExternalAction(store, "opencode", actor, operation);
31441
- 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
+ }
31442
31920
  return selected;
31921
+ }
31443
31922
  const actionRef = externalActionRef("opencode", actor, operation);
31444
31923
  return {
31445
31924
  taskId: selected.data.task.id,
@@ -31483,8 +31962,10 @@ var workerIdFor = (store, actor, current, directChildren) => {
31483
31962
  var createWorkitTools = ({
31484
31963
  client,
31485
31964
  receipts = new NativeReceiptStore,
31486
- directChildren = new Map
31965
+ directChildren = new Map,
31966
+ now = Date.now
31487
31967
  } = {}) => {
31968
+ const actionProposals = new Map;
31488
31969
  const make = (family) => tool({
31489
31970
  description: `Workit ${family} operations backed by the shared task contract.`,
31490
31971
  args: operationShapeFor(family),
@@ -31518,6 +31999,11 @@ var createWorkitTools = ({
31518
31999
  let result;
31519
32000
  if (family === "decision" && args.action === "record") {
31520
32001
  const decision = parsed.data;
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));
31521
32007
  const expectation = {
31522
32008
  selectedLabel: decision.response,
31523
32009
  decisionPurpose: decision.purpose,
@@ -31529,7 +32015,29 @@ var createWorkitTools = ({
31529
32015
  const observed = receipts.consume(context.sessionID, "decision", expectation);
31530
32016
  if (!observed.ok)
31531
32017
  return output2(failure("permission_denied", observed.error));
31532
- 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);
31533
32041
  } else {
31534
32042
  const run = core[family];
31535
32043
  result = run.call(core, parsed.data);
@@ -31630,6 +32138,64 @@ var createWorkitTools = ({
31630
32138
  return output2(failure("external_outcome_unknown", "previous external action outcome is unknown"));
31631
32139
  }
31632
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
+ }
31633
32199
  const result = await nativeExternalActionRunner(context.directory, context.sessionID, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, context.directory, step, {
31634
32200
  host: "opencode",
31635
32201
  actor: context.sessionID
@@ -31729,26 +32295,14 @@ var loadProvenance = (logger, pkgUrl) => {
31729
32295
  };
31730
32296
 
31731
32297
  // packages/workit-opencode/src/plugin.ts
31732
- var root = fileURLToPath2(new URL("../assets/", import.meta.url));
32298
+ var root = fileURLToPath3(new URL("../assets/", import.meta.url));
31733
32299
  var skillsPath = path25.join(root, "skills");
31734
32300
  var logger = createLogger({
31735
32301
  appLog: () => {
31736
32302
  return;
31737
32303
  }
31738
32304
  });
31739
- var sourceMarker = markSourcesLoaded([
31740
- fileURLToPath2(import.meta.url),
31741
- fileURLToPath2(new URL("./tools/workit.ts", import.meta.url)),
31742
- ...[
31743
- "task-contract.ts",
31744
- "task-engine.ts",
31745
- "task-evaluation.ts",
31746
- "task-store.ts",
31747
- "workers.ts",
31748
- "authority.ts",
31749
- "methods.ts"
31750
- ].map((file) => fileURLToPath2(new URL(`../../../workit-core/src/core/${file}`, import.meta.url)))
31751
- ].filter((file) => existsSync12(file)));
32305
+ var sourceMarker = markSourcesLoaded(pluginSourceFiles);
31752
32306
  var staleSourcesWarned = false;
31753
32307
  var warnStaleSources = () => {
31754
32308
  if (staleSourcesWarned)
@@ -32067,6 +32621,12 @@ var plugin = async ({ client, directory }) => {
32067
32621
  throw new Error(`recovery_required: direct branch or PR creation bypasses the Workit route; ${route.guidance}`);
32068
32622
  return;
32069
32623
  }
32624
+ if (input.tool === "question") {
32625
+ const issue = workitBindingQuestionIssue(output?.args?.questions);
32626
+ if (issue)
32627
+ throw new Error(issue);
32628
+ return;
32629
+ }
32070
32630
  if (input.tool === "task") {
32071
32631
  const listed = new TaskStore(directory).listTasks();
32072
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)))
@@ -32086,7 +32646,7 @@ var plugin = async ({ client, directory }) => {
32086
32646
  config: async (config) => {
32087
32647
  const mutable = config;
32088
32648
  const paths = [...mutable.skills?.paths ?? []];
32089
- if (existsSync12(skillsPath) && !paths.includes(skillsPath))
32649
+ if (existsSync13(skillsPath) && !paths.includes(skillsPath))
32090
32650
  paths.push(skillsPath);
32091
32651
  mutable.skills ??= {};
32092
32652
  mutable.skills.paths = paths;