@brainervirus/workit-opencode 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,10 +7590,12 @@ var decisionSchema = object({
7590
7590
  scope: scopeSchema,
7591
7591
  presented: text,
7592
7592
  approvedContent: text,
7593
- contentRefs: array(refSchema)
7593
+ displayed: text.optional(),
7594
+ contentRefs: array(refSchema),
7595
+ statedChoice: object({ ref: text, text }).strict().optional()
7594
7596
  }).strict(),
7595
7597
  digest,
7596
- response: _enum(["approved", "rejected"]),
7598
+ response: _enum(["approved", "rejected", "stated"]),
7597
7599
  requirementIds: array(digest),
7598
7600
  revoked: object({ at: utc, reason: text }).strict().nullable(),
7599
7601
  consumption: object({
@@ -7602,6 +7604,27 @@ var decisionSchema = object({
7602
7604
  actionRef: refSchema
7603
7605
  }).strict().nullable()
7604
7606
  }).strict();
7607
+ var BINDING_QUESTION_BUDGET = 300;
7608
+ var bindingQuestionHeader = /^Workit decision: (design|action|limitation|preference)$/;
7609
+ var workitBindingQuestionIssue = (questions) => {
7610
+ if (!Array.isArray(questions) || questions.length !== 1)
7611
+ return null;
7612
+ const question = questions[0];
7613
+ if (!question || typeof question !== "object")
7614
+ return null;
7615
+ if (typeof question.header !== "string" || !bindingQuestionHeader.test(question.header.trim()))
7616
+ return null;
7617
+ 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.`;
7618
+ if (typeof question.question === "string" && question.question.length > BINDING_QUESTION_BUDGET)
7619
+ return over("the question text", question.question);
7620
+ if (Array.isArray(question.options))
7621
+ for (const option of question.options) {
7622
+ const description = option.description;
7623
+ if (typeof description === "string" && description.length > BINDING_QUESTION_BUDGET)
7624
+ return over("an option description", description);
7625
+ }
7626
+ return null;
7627
+ };
7605
7628
  var findingSchema = object({
7606
7629
  claim: text,
7607
7630
  consequence: text,
@@ -7668,6 +7691,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
7668
7691
  path: ["decisionId"]
7669
7692
  });
7670
7693
  });
7694
+ var runtimeSchema = object({
7695
+ createdWith: text.nullable(),
7696
+ updatedWith: text
7697
+ }).strict();
7671
7698
  var taskRecordSchema = object({
7672
7699
  schemaVersion: literal(1),
7673
7700
  id,
@@ -7681,6 +7708,8 @@ var taskRecordSchema = object({
7681
7708
  status: _enum(["active", "paused", "closed"]),
7682
7709
  closure: closureSchema.nullable(),
7683
7710
  progress: progressSchema,
7711
+ pauseReason: text.nullable().optional(),
7712
+ runtime: runtimeSchema.optional(),
7684
7713
  assessments: array(entrySchema(assessmentSchema)),
7685
7714
  policy: policySchema.nullable(),
7686
7715
  policyChanges: array(policyChangeSchema),
@@ -7696,6 +7725,7 @@ var workspaceRecordSchema = object({
7696
7725
  id,
7697
7726
  revision,
7698
7727
  root: nonEmpty,
7728
+ runtime: runtimeSchema.optional(),
7699
7729
  writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
7700
7730
  }).strict();
7701
7731
  var capabilitySchema = object({
@@ -7723,6 +7753,9 @@ var taskSummarySchema = object({
7723
7753
  id,
7724
7754
  revision,
7725
7755
  workspaceRevision: revision,
7756
+ createdAt: utc,
7757
+ updatedAt: utc,
7758
+ runtime: runtimeSchema.nullable(),
7726
7759
  objective: text,
7727
7760
  status: _enum(["active", "paused", "closed"]),
7728
7761
  closure: closureSchema.nullable(),
@@ -7793,7 +7826,7 @@ var taskOperations = {
7793
7826
  expectedWorkspaceRevision: revision.optional(),
7794
7827
  outcome: outcomeSchema,
7795
7828
  summary: text,
7796
- decisionIds: array(id)
7829
+ decisionIds: array(id).optional()
7797
7830
  })
7798
7831
  };
7799
7832
  var policyOperations = {
@@ -7885,14 +7918,15 @@ var writerOperations = {
7885
7918
  ...taskId,
7886
7919
  expectedRevision: revision.optional(),
7887
7920
  expectedWorkspaceRevision: revision.optional(),
7888
- workerId: nullableId.optional()
7921
+ workerId: nullableId.optional(),
7922
+ reason: text.optional()
7889
7923
  }),
7890
7924
  release: operation({
7891
7925
  action: literal("release"),
7892
7926
  ...taskId,
7893
7927
  expectedRevision: revision.optional(),
7894
7928
  expectedWorkspaceRevision: revision.optional(),
7895
- reason: text
7929
+ reason: text.optional()
7896
7930
  })
7897
7931
  };
7898
7932
  var stateOperations = {
@@ -8045,7 +8079,26 @@ function requirementId(input) {
8045
8079
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
8046
8080
  import * as fs4 from "node:fs";
8047
8081
  import { hostname } from "node:os";
8048
- import path3 from "node:path";
8082
+ import path4 from "node:path";
8083
+
8084
+ // packages/workit-core/src/core/package-root.ts
8085
+ import { existsSync } from "node:fs";
8086
+ import path from "node:path";
8087
+ import { fileURLToPath } from "node:url";
8088
+ var packageRoot = () => {
8089
+ let dir = path.dirname(fileURLToPath(import.meta.url));
8090
+ while (true) {
8091
+ const parent = path.dirname(dir);
8092
+ if (existsSync(path.join(dir, "package.json")) || parent === dir)
8093
+ return dir;
8094
+ dir = parent;
8095
+ }
8096
+ };
8097
+ var assetRoot = () => {
8098
+ const root = packageRoot();
8099
+ const assets = path.join(root, "assets");
8100
+ return existsSync(assets) ? assets : root;
8101
+ };
8049
8102
 
8050
8103
  // node_modules/@openclaw/fs-safe/dist/errors.js
8051
8104
  var OPERATIONAL_CODES = new Set([
@@ -8096,7 +8149,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
8096
8149
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
8097
8150
  import { randomBytes } from "node:crypto";
8098
8151
  import fsSync from "node:fs";
8099
- import path from "node:path";
8152
+ import path2 from "node:path";
8100
8153
 
8101
8154
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
8102
8155
  import fs from "node:fs";
@@ -8186,13 +8239,13 @@ ${ownershipToken}
8186
8239
  return { raw, ownershipToken };
8187
8240
  }
8188
8241
  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)) {
8242
+ const resolved = path2.resolve(lockPath);
8243
+ const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
8244
+ const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
8245
+ if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
8193
8246
  throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
8194
8247
  }
8195
- return relative.split(path.sep).join(path.posix.sep);
8248
+ return relative.split(path2.sep).join(path2.posix.sep);
8196
8249
  }
8197
8250
  function parseSidecarLockPayload(raw, parser) {
8198
8251
  if (parser) {
@@ -8339,7 +8392,7 @@ function getFsSafeLockConfig() {
8339
8392
 
8340
8393
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
8341
8394
  import fs3 from "node:fs";
8342
- import path2 from "node:path";
8395
+ import path3 from "node:path";
8343
8396
 
8344
8397
  // node_modules/@openclaw/fs-safe/dist/timing.js
8345
8398
  function sleepSync(ms) {
@@ -8430,26 +8483,26 @@ function canonicalLockParentSync(parent) {
8430
8483
  return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
8431
8484
  }
8432
8485
  function normalizeTargetPath(targetPath) {
8433
- const resolved = path2.resolve(targetPath);
8434
- fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
8486
+ const resolved = path3.resolve(targetPath);
8487
+ fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
8435
8488
  try {
8436
- return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
8489
+ return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
8437
8490
  } catch {
8438
8491
  return resolved;
8439
8492
  }
8440
8493
  }
8441
8494
  function boundedLockPath(lockPath, lockRoot) {
8442
- const resolved = path2.resolve(lockPath);
8495
+ const resolved = path3.resolve(lockPath);
8443
8496
  if (!lockRoot)
8444
8497
  return resolved;
8445
8498
  relativeSidecarLockPath(lockRoot, resolved);
8446
- const parent = path2.dirname(resolved);
8499
+ const parent = path3.dirname(resolved);
8447
8500
  const parentReal = canonicalLockParentSync(parent);
8448
- const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
8449
- if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
8501
+ const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
8502
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
8450
8503
  throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
8451
8504
  }
8452
- return path2.join(parentReal, path2.basename(resolved));
8505
+ return path3.join(parentReal, path3.basename(resolved));
8453
8506
  }
8454
8507
  function defaultShouldReclaim(snapshot, staleMs, nowMs) {
8455
8508
  const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
@@ -8662,6 +8715,33 @@ var metadataLockSchema = object({
8662
8715
  nonce: string2().min(1)
8663
8716
  }).strict();
8664
8717
  var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
8718
+ var cachedRuntimeVersion = null;
8719
+ var runtimeVersion = () => {
8720
+ if (cachedRuntimeVersion === null) {
8721
+ try {
8722
+ const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
8723
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
8724
+ } catch {
8725
+ cachedRuntimeVersion = "0.0.0";
8726
+ }
8727
+ }
8728
+ return cachedRuntimeVersion;
8729
+ };
8730
+ var stampNew = (record) => ({
8731
+ createdWith: record.runtime?.createdWith ?? null,
8732
+ updatedWith: runtimeVersion()
8733
+ });
8734
+ var isNewerVersion = (candidate, current) => {
8735
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
8736
+ const [next, now] = [parts(candidate), parts(current)];
8737
+ for (let index = 0;index < 3; index += 1) {
8738
+ const left = next[index] ?? 0;
8739
+ const right = now[index] ?? 0;
8740
+ if (left !== right)
8741
+ return left > right;
8742
+ }
8743
+ return false;
8744
+ };
8665
8745
  var jsonBytes = (value) => `${canonicalJson(value)}
8666
8746
  `;
8667
8747
  var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
@@ -8688,12 +8768,12 @@ var sameMetadataLock = (left, right) => {
8688
8768
  class TaskStore {
8689
8769
  root;
8690
8770
  constructor(root) {
8691
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path3.resolve(root);
8771
+ this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
8692
8772
  }
8693
8773
  readTask(taskId) {
8694
8774
  if (!validId(taskId))
8695
8775
  return failure("invalid_input", "task ID is invalid", { taskId });
8696
- const result = this.readRecord(path3.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
8776
+ const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
8697
8777
  if (!result.exists)
8698
8778
  return failure("not_found", "task not found", { taskId });
8699
8779
  if (result.result.ok && result.result.data.id !== taskId)
@@ -8727,7 +8807,7 @@ class TaskStore {
8727
8807
  });
8728
8808
  const tasks = [];
8729
8809
  for (const name of names.sort()) {
8730
- const item = this.readRecord(path3.join(this.tasksDir, name), taskRecordSchema);
8810
+ const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
8731
8811
  if (!item.exists)
8732
8812
  continue;
8733
8813
  if (!item.result.ok)
@@ -8761,7 +8841,7 @@ class TaskStore {
8761
8841
  if (!current.ok)
8762
8842
  return current;
8763
8843
  if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
8764
- return failure("revision_conflict", "workspace revision does not match", {
8844
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
8765
8845
  expectedWorkspaceRevision: value.expectedWorkspaceRevision,
8766
8846
  actualWorkspaceRevision: current.data?.revision ?? null
8767
8847
  });
@@ -8773,11 +8853,17 @@ class TaskStore {
8773
8853
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
8774
8854
  if (current.data && !previousWorkspaceBytes)
8775
8855
  return failure("storage_error", "workspace snapshot disappeared during creation");
8776
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
8856
+ const workspace = current.data ? {
8857
+ ...current.data,
8858
+ revision: newRevision(),
8859
+ root: this.root,
8860
+ runtime: stampNew(current.data)
8861
+ } : {
8777
8862
  schemaVersion: SCHEMA_VERSION,
8778
8863
  id: newId(),
8779
8864
  revision: newRevision(),
8780
8865
  root: this.root,
8866
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8781
8867
  writer: null
8782
8868
  };
8783
8869
  const timestamp = value.now ?? now();
@@ -8799,6 +8885,7 @@ class TaskStore {
8799
8885
  status: "active",
8800
8886
  closure: null,
8801
8887
  progress: { summary: "", nextAction: null, blockers: [] },
8888
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8802
8889
  assessments: [],
8803
8890
  policy: null,
8804
8891
  policyChanges: [],
@@ -8829,7 +8916,7 @@ class TaskStore {
8829
8916
  if (!current.ok)
8830
8917
  return current;
8831
8918
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
8832
- return failure("revision_conflict", "workspace revision does not match", {
8919
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
8833
8920
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
8834
8921
  actualWorkspaceRevision: current.data?.revision ?? null
8835
8922
  });
@@ -8837,11 +8924,17 @@ class TaskStore {
8837
8924
  if (current.data && !previousWorkspaceBytes)
8838
8925
  return failure("storage_error", "workspace snapshot disappeared during import");
8839
8926
  const timestamp = input.now ?? now();
8840
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
8927
+ const workspace = current.data ? {
8928
+ ...current.data,
8929
+ revision: newRevision(),
8930
+ root: this.root,
8931
+ runtime: stampNew(current.data)
8932
+ } : {
8841
8933
  schemaVersion: SCHEMA_VERSION,
8842
8934
  id: input.workspaceId ?? newId(),
8843
8935
  revision: newRevision(),
8844
8936
  root: this.root,
8937
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
8845
8938
  writer: null
8846
8939
  };
8847
8940
  const task = {
@@ -8849,7 +8942,11 @@ class TaskStore {
8849
8942
  workspaceId: workspace.id,
8850
8943
  revision: newRevision(),
8851
8944
  createdAt: timestamp,
8852
- updatedAt: timestamp
8945
+ updatedAt: timestamp,
8946
+ runtime: {
8947
+ createdWith: input.task.runtime?.createdWith ?? null,
8948
+ updatedWith: runtimeVersion()
8949
+ }
8853
8950
  };
8854
8951
  const validTask = taskRecordSchema.safeParse(task);
8855
8952
  if (!validTask.success)
@@ -8898,7 +8995,8 @@ class TaskStore {
8898
8995
  workspaceId: current.data.workspaceId,
8899
8996
  createdAt: current.data.createdAt,
8900
8997
  revision: context.revision,
8901
- updatedAt: context.now
8998
+ updatedAt: context.now,
8999
+ runtime: stampNew(current.data)
8902
9000
  };
8903
9001
  const valid = taskRecordSchema.safeParse(record);
8904
9002
  if (!valid.success)
@@ -8932,7 +9030,8 @@ class TaskStore {
8932
9030
  ...changed.data,
8933
9031
  id: current.data.id,
8934
9032
  root: this.root,
8935
- revision: context.revision
9033
+ revision: context.revision,
9034
+ runtime: stampNew(current.data)
8936
9035
  };
8937
9036
  const valid = workspaceRecordSchema.safeParse(record);
8938
9037
  if (!valid.success)
@@ -8975,7 +9074,8 @@ class TaskStore {
8975
9074
  ...changedWorkspace.data,
8976
9075
  id: workspace.data.id,
8977
9076
  root: this.root,
8978
- revision: workspaceContext.revision
9077
+ revision: workspaceContext.revision,
9078
+ runtime: stampNew(workspace.data)
8979
9079
  });
8980
9080
  if (!nextWorkspace.success)
8981
9081
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -9000,7 +9100,8 @@ class TaskStore {
9000
9100
  workspaceId: task.data.workspaceId,
9001
9101
  createdAt: task.data.createdAt,
9002
9102
  revision: taskContext.revision,
9003
- updatedAt: taskContext.now
9103
+ updatedAt: taskContext.now,
9104
+ runtime: stampNew(task.data)
9004
9105
  });
9005
9106
  if (!nextTask.success) {
9006
9107
  this.markUncertain(nextWorkspace.data);
@@ -9027,7 +9128,7 @@ class TaskStore {
9027
9128
  if (match)
9028
9129
  candidates.push({
9029
9130
  target: match[1],
9030
- path: path3.join(this.recoveryDir, name),
9131
+ path: path4.join(this.recoveryDir, name),
9031
9132
  digest: match[3]
9032
9133
  });
9033
9134
  }
@@ -9220,7 +9321,8 @@ class TaskStore {
9220
9321
  const value = {
9221
9322
  ...workspace,
9222
9323
  writer: { ...workspace.writer, state: "uncertain" },
9223
- revision: newRevision()
9324
+ revision: newRevision(),
9325
+ runtime: stampNew(workspace)
9224
9326
  };
9225
9327
  this.replaceSnapshot(this.workspacePath, value, workspace);
9226
9328
  }
@@ -9298,7 +9400,7 @@ class TaskStore {
9298
9400
  }
9299
9401
  fs4.renameSync(temporary, file);
9300
9402
  temporary = undefined;
9301
- this.fsyncDirectory(path3.dirname(file));
9403
+ this.fsyncDirectory(path4.dirname(file));
9302
9404
  return success(null, null, value);
9303
9405
  } catch (error) {
9304
9406
  return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
@@ -9312,9 +9414,9 @@ class TaskStore {
9312
9414
  }
9313
9415
  }
9314
9416
  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`);
9417
+ const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
9418
+ const id = target === "task" ? path4.basename(file, ".json") : "workspace";
9419
+ const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
9318
9420
  if (fs4.existsSync(destination)) {
9319
9421
  if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
9320
9422
  return;
@@ -9392,7 +9494,7 @@ class TaskStore {
9392
9494
  for (const name of fs4.readdirSync(this.tasksDir)) {
9393
9495
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
9394
9496
  return null;
9395
- const parsed = this.parseBytes(fs4.readFileSync(path3.join(this.tasksDir, name)), taskRecordSchema);
9497
+ const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
9396
9498
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
9397
9499
  return null;
9398
9500
  ids.add(parsed.data.workspaceId);
@@ -9434,7 +9536,12 @@ class TaskStore {
9434
9536
  if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
9435
9537
  return failure("unsupported_version", "unsupported snapshot schema version");
9436
9538
  const parsed = schema.safeParse(value);
9437
- return parsed.success ? success(null, null, parsed.data) : failure("recovery_required", "snapshot does not satisfy its schema");
9539
+ if (parsed.success)
9540
+ return success(null, null, parsed.data);
9541
+ const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
9542
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
9543
+ return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
9544
+ return failure("recovery_required", "snapshot does not satisfy its schema");
9438
9545
  }
9439
9546
  processStart(pid) {
9440
9547
  try {
@@ -9444,43 +9551,98 @@ class TaskStore {
9444
9551
  }
9445
9552
  }
9446
9553
  conflict(expected, actual) {
9447
- return failure("revision_conflict", "snapshot revision does not match", {
9554
+ return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
9448
9555
  expectedRevision: expected,
9449
9556
  actualRevision: actual
9450
9557
  });
9451
9558
  }
9452
9559
  taskPath(taskId) {
9453
- return path3.join(this.tasksDir, `${taskId}.json`);
9560
+ return path4.join(this.tasksDir, `${taskId}.json`);
9454
9561
  }
9455
9562
  get workitDir() {
9456
- return path3.join(this.root, ".workit");
9563
+ return path4.join(this.root, ".workit");
9457
9564
  }
9458
9565
  get tasksDir() {
9459
- return path3.join(this.workitDir, "tasks");
9566
+ return path4.join(this.workitDir, "tasks");
9460
9567
  }
9461
9568
  get recoveryDir() {
9462
- return path3.join(this.workitDir, "recovery");
9569
+ return path4.join(this.workitDir, "recovery");
9463
9570
  }
9464
9571
  get workspacePath() {
9465
- return path3.join(this.workitDir, "workspace.json");
9572
+ return path4.join(this.workitDir, "workspace.json");
9466
9573
  }
9467
9574
  get lockPath() {
9468
- return path3.join(this.workitDir, "metadata.lock");
9575
+ return path4.join(this.workitDir, "metadata.lock");
9469
9576
  }
9470
9577
  get gitignorePath() {
9471
- return path3.join(this.workitDir, ".gitignore");
9578
+ return path4.join(this.workitDir, ".gitignore");
9472
9579
  }
9473
9580
  }
9581
+ // packages/workit-core/src/core/workers.ts
9582
+ var same = (left, right) => {
9583
+ try {
9584
+ return canonicalJson(left) === canonicalJson(right);
9585
+ } catch {
9586
+ return false;
9587
+ }
9588
+ };
9589
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
9590
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
9591
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
9592
+ var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
9593
+ const code = char.charCodeAt(0);
9594
+ return code < 32 || code === 127;
9595
+ }) && !value.split(/[\\/]/).includes("..");
9596
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
9597
+ var workerIdOf = (value) => value.workerId ?? null;
9598
+ function assertProductWriteAllowed(input) {
9599
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
9600
+ return failure("invalid_input", "invalid product write path");
9601
+ if (!input.store)
9602
+ return failure("permission_denied", "write authorization requires core state");
9603
+ const task = input.store.readTask(input.task.id);
9604
+ if (!task.ok)
9605
+ return task;
9606
+ const workspace = input.store.readWorkspace();
9607
+ if (!workspace.ok)
9608
+ return workspace;
9609
+ if (!workspace.data)
9610
+ return failure("not_found", "workspace not found");
9611
+ const workspaceRecord = workspace.data;
9612
+ if (task.data.status !== "active")
9613
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
9614
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
9615
+ return failure("recovery_required", "task and workspace bindings are invalid");
9616
+ const writer = workspaceRecord.writer;
9617
+ if (!writer)
9618
+ return failure("permission_denied", "checkout has no writer owner");
9619
+ if (writer.state === "uncertain")
9620
+ return failure("recovery_required", "checkout writer requires recovery", {
9621
+ owner: writer.owner
9622
+ });
9623
+ const caller = callerValue(input.caller);
9624
+ const workerId = workerIdOf(input.caller);
9625
+ const owner = writer.owner;
9626
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
9627
+ return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
9628
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
9629
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
9630
+ return failure("permission_denied", "only implementers can own product writes");
9631
+ 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))))
9632
+ return failure("recovery_required", "worker session is not an active writer");
9633
+ return success(null, workspaceRecord.revision, owner);
9634
+ }
9635
+
9474
9636
  // packages/workit-core/src/core/task-evaluation.ts
9475
9637
  import { createHash as createHash4 } from "node:crypto";
9476
9638
  import * as fs6 from "node:fs";
9477
9639
  import { spawnSync } from "node:child_process";
9478
- import path5 from "node:path";
9640
+ import path6 from "node:path";
9479
9641
 
9480
9642
  // packages/workit-core/src/core/authority.ts
9481
9643
  import { createHash as createHash3 } from "node:crypto";
9482
9644
  import * as fs5 from "node:fs";
9483
- import path4 from "node:path";
9645
+ import path5 from "node:path";
9484
9646
  var verifiedAuthorities = new WeakMap;
9485
9647
  var trustedAuthority = (kind, provenance, expected, binding, caller) => {
9486
9648
  const token = {};
@@ -9607,20 +9769,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
9607
9769
  continue;
9608
9770
  if (!reference.digest)
9609
9771
  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");
9772
+ const target = path5.resolve(root, reference.path);
9773
+ if (target !== root && !target.startsWith(`${root}${path5.sep}`))
9774
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
9775
+ fields: [
9776
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
9777
+ ]
9778
+ });
9613
9779
  try {
9614
- const relative = path4.relative(root, target);
9780
+ const relative = path5.relative(root, target);
9615
9781
  let current = root;
9616
- for (const segment of relative.split(path4.sep)) {
9782
+ for (const segment of relative.split(path5.sep)) {
9617
9783
  if (!segment)
9618
9784
  continue;
9619
- current = path4.join(current, segment);
9785
+ current = path5.join(current, segment);
9620
9786
  const stat = fs5.lstatSync(current);
9621
9787
  const real = fs5.realpathSync(current);
9622
- if (real !== root && !real.startsWith(`${root}${path4.sep}`))
9623
- return failure("invalid_input", "document reference escapes checkout");
9788
+ if (real !== root && !real.startsWith(`${root}${path5.sep}`))
9789
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
9790
+ fields: [
9791
+ {
9792
+ path: "contentRefs.path",
9793
+ reason: `${reference.path} resolves outside the checkout`
9794
+ }
9795
+ ]
9796
+ });
9624
9797
  if (stat.isSymbolicLink())
9625
9798
  return failure("invalid_input", "document reference uses a symlink");
9626
9799
  if (current === target && !stat.isFile())
@@ -9967,14 +10140,14 @@ var verifyDecisionContent = verifyContentRefs;
9967
10140
 
9968
10141
  // packages/workit-core/src/core/task-evaluation.ts
9969
10142
  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("/") || ".";
10143
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
10144
+ var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
9972
10145
  var canonicalPath = (value) => {
9973
10146
  if (typeof value !== "string" || !value)
9974
10147
  return null;
9975
10148
  if (value.includes("\\") || value.split("/").includes(".."))
9976
10149
  return null;
9977
- const normalized = path5.posix.normalize(value);
10150
+ const normalized = path6.posix.normalize(value);
9978
10151
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
9979
10152
  return null;
9980
10153
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -10022,14 +10195,14 @@ var gitPaths = (root) => {
10022
10195
  git: /not a git repository/i.test(stderr) ? false : true
10023
10196
  };
10024
10197
  }
10025
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")) : [];
10198
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
10026
10199
  const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
10027
10200
  if (stagedDeleted.status !== 0) {
10028
10201
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
10029
10202
  if (!/not a git repository/i.test(stderr))
10030
10203
  return { paths, uncertain: true, git: true };
10031
10204
  } else if (stagedDeleted.stdout) {
10032
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")));
10205
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
10033
10206
  }
10034
10207
  return { paths: [...new Set(paths)], uncertain: false, git: true };
10035
10208
  };
@@ -10044,7 +10217,7 @@ var walk = (root, directory, output) => {
10044
10217
  for (const name of names) {
10045
10218
  if (name === ".git" || name === ".workit")
10046
10219
  continue;
10047
- const target = path5.join(directory, name);
10220
+ const target = path6.join(directory, name);
10048
10221
  let stat;
10049
10222
  try {
10050
10223
  stat = fs6.lstatSync(target);
@@ -10062,17 +10235,17 @@ var walk = (root, directory, output) => {
10062
10235
  var scopeRoots = (root, scope) => {
10063
10236
  const roots = [];
10064
10237
  for (const item of scope.paths.length ? scope.paths : ["."]) {
10065
- const target = path5.resolve(root, item);
10238
+ const target = path6.resolve(root, item);
10066
10239
  if (!inside(root, target))
10067
- return failure("invalid_input", "candidate scope escapes checkout");
10240
+ 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
10241
  let ancestor = target;
10069
10242
  while (!fs6.existsSync(ancestor) && ancestor !== root)
10070
- ancestor = path5.dirname(ancestor);
10243
+ ancestor = path6.dirname(ancestor);
10071
10244
  try {
10072
10245
  if (!inside(root, fs6.realpathSync(ancestor)))
10073
- return failure("invalid_input", "candidate scope escapes checkout");
10246
+ 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
10247
  } catch {
10075
- return failure("invalid_input", "candidate scope cannot be inspected");
10248
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
10076
10249
  }
10077
10250
  roots.push(target);
10078
10251
  }
@@ -10087,7 +10260,7 @@ function captureCandidate(root, scope, environment = []) {
10087
10260
  }
10088
10261
  const normalizedScope = canonicalScope(scope);
10089
10262
  if (!normalizedScope)
10090
- return failure("invalid_input", "candidate scope is invalid");
10263
+ 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
10264
  const roots = scopeRoots(checkout, normalizedScope);
10092
10265
  if (!roots.ok)
10093
10266
  return roots;
@@ -10119,7 +10292,7 @@ function captureCandidate(root, scope, environment = []) {
10119
10292
  for (const item of [...names].sort()) {
10120
10293
  if (!scopeMatches(item, normalizedScope))
10121
10294
  continue;
10122
- const target = path5.join(checkout, item);
10295
+ const target = path6.join(checkout, item);
10123
10296
  try {
10124
10297
  const stat = fs6.lstatSync(target);
10125
10298
  if (stat.isSymbolicLink()) {
@@ -10241,7 +10414,7 @@ function evaluateEvidence(task, candidate) {
10241
10414
  }
10242
10415
  var scopeCovers2 = scopeCovers;
10243
10416
  var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
10244
- var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && data.response === "approved" && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
10417
+ var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
10245
10418
  var evidenceMatchesRequirement = (kind, evidenceKind, dimension) => {
10246
10419
  if (kind !== "passed")
10247
10420
  return false;
@@ -10276,7 +10449,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10276
10449
  return true;
10277
10450
  const reviewSession = sessionFromRef(entry.data.reviewContext);
10278
10451
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
10279
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
10452
+ const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
10280
10453
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
10281
10454
  });
10282
10455
  if (passed.length) {
@@ -10317,7 +10490,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10317
10490
  status: "satisfied",
10318
10491
  evidenceIds: [],
10319
10492
  decisionIds: decisions.map((decision) => decision.id),
10320
- reason: "an applicable approved decision satisfies the requirement"
10493
+ reason: "an applicable approved or stated decision satisfies the requirement"
10321
10494
  };
10322
10495
  const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
10323
10496
  if (limitations.length)
@@ -10372,7 +10545,17 @@ function evaluateClosure(requestedOutcome, view) {
10372
10545
  const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
10373
10546
  if (requestedOutcome !== "stopped" && blocking.length)
10374
10547
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
10375
- requirementIds: blocking.map((item) => item.requirementId)
10548
+ requirementIds: blocking.map((item) => item.requirementId),
10549
+ requirements: blocking.map((item) => {
10550
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
10551
+ return {
10552
+ requirementId: item.requirementId,
10553
+ ruleId: requirement?.ruleId ?? "unknown",
10554
+ reason: item.reason,
10555
+ satisfaction: requirement?.satisfaction ?? "",
10556
+ dependentAction: requirement?.dependentAction ?? null
10557
+ };
10558
+ })
10376
10559
  });
10377
10560
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
10378
10561
  if (requestedOutcome === "verified" && accepted.length)
@@ -10403,7 +10586,7 @@ function evaluateClosure(requestedOutcome, view) {
10403
10586
  var METHODS = {
10404
10587
  "workit-challenge": { dimensions: ["challenge", "decisions"] },
10405
10588
  "workit-behavioral-tdd": { dimensions: ["testing"] },
10406
- "workit-review": { dimensions: ["review"] },
10589
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
10407
10590
  "workit-plan": { dimensions: ["artifacts", "continuity"] },
10408
10591
  "workit-implement": { dimensions: ["delegation"] },
10409
10592
  "workit-debug": { ruleIds: ["root-cause-investigation"] },
@@ -10465,7 +10648,18 @@ decision.record {binding with taskId/workspaceId from inspect plus presented and
10465
10648
  approvedContent} only through a native approval question asked receipt-shaped:
10466
10649
  header Workit decision: <purpose>, the same label repeated in the question
10467
10650
  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,
10651
+ rejected; ask once, never re-ask to mint a receipt. Before asking, present the
10652
+ item in the conversation (durable artifacts: complete digest plus exact path;
10653
+ inline plans: the content) and keep the question to one short scoped sentence;
10654
+ never ask to approve something unseen. An answer with custom text is steering,
10655
+ not approval: adjust, re-present, then ask a new scoped question. After the user
10656
+ approves a spec or plan, execute the remaining tasks continuously with one
10657
+ atomic commit per task and do not ask whether to continue; stop only for a new
10658
+ product decision, a failed safety or verification gate, a conflicting edit, or
10659
+ missing authority. Record the plan's commit list once through the plan-scoped
10660
+ action approval (git.commit with plan_steps and plan_branch) so each listed
10661
+ commit executes without a new question; an unlisted message needs a fresh
10662
+ approval. task.close {outcome,
10469
10663
  summary, decisionIds}. A completed native subagent run stops its bound worker
10470
10664
  by itself; if a worker strands in cancelling with its run verifiably over,
10471
10665
  repeat worker.cancel {reason} to confirm the stop. Check and review evidence
@@ -10497,7 +10691,7 @@ function reconcileResume(view, observations = []) {
10497
10691
  return candidate;
10498
10692
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
10499
10693
  const blockers = [...view.task.progress.blockers];
10500
- if (view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
10694
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
10501
10695
  blockers.push({
10502
10696
  reason: "worker state requires reconciliation",
10503
10697
  dependentAction: "resume",
@@ -10629,7 +10823,25 @@ function shellRouteIntent(command) {
10629
10823
  }
10630
10824
  return null;
10631
10825
  }
10826
+ function hasLiveWorkitTask(dir) {
10827
+ try {
10828
+ if (typeof dir !== "string" || dir === "")
10829
+ return false;
10830
+ const listed = new TaskStore(dir).listTasks();
10831
+ if (!listed.ok)
10832
+ return false;
10833
+ return listed.data.some((task) => task.status === "active" || task.status === "paused");
10834
+ } catch {
10835
+ return false;
10836
+ }
10837
+ }
10838
+ function shouldDenyShellRoute(dir, command) {
10839
+ if (!hasLiveWorkitTask(dir))
10840
+ return null;
10841
+ return shellRouteIntent(command);
10842
+ }
10632
10843
  // packages/workit-core/src/core/external-action.ts
10844
+ import { execFileSync } from "node:child_process";
10633
10845
  var externalActionSchema = discriminatedUnion("operation", [
10634
10846
  object({
10635
10847
  operation: literal("git.branch_setup"),
@@ -10642,7 +10854,11 @@ var externalActionSchema = discriminatedUnion("operation", [
10642
10854
  }).strict(),
10643
10855
  object({
10644
10856
  operation: literal("git.commit"),
10645
- payload: object({ message: string2().min(1) }).strict()
10857
+ payload: object({
10858
+ message: string2().min(1).optional(),
10859
+ plan_steps: array(string2().min(1)).optional(),
10860
+ plan_branch: string2().min(1).optional()
10861
+ }).strict()
10646
10862
  }).strict(),
10647
10863
  object({
10648
10864
  operation: literal("git.push"),
@@ -10731,7 +10947,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
10731
10947
  payload,
10732
10948
  ...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
10949
  });
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).";
10950
+ 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
10951
  var externalActionRef = (host, actor, descriptor) => ({
10736
10952
  kind: "host",
10737
10953
  host,
@@ -10782,14 +10998,185 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
10782
10998
  return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
10783
10999
  return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
10784
11000
  };
11001
+ var planCommitDescriptor = (content) => {
11002
+ try {
11003
+ const value = JSON.parse(content);
11004
+ if (value.operation !== "git.commit" || !value.payload)
11005
+ return null;
11006
+ const steps = value.payload.plan_steps;
11007
+ const branch = value.payload.plan_branch;
11008
+ if (!Array.isArray(steps) || steps.some((step) => typeof step !== "string" || !step) || typeof branch !== "string" || !branch)
11009
+ return null;
11010
+ return { steps, branch };
11011
+ } catch {
11012
+ return null;
11013
+ }
11014
+ };
11015
+ var approvedPlanCommit = (store, host, actor, message) => {
11016
+ if (typeof message !== "string" || !message)
11017
+ return failure("invalid_input", "commit message is required");
11018
+ const listed = store.listTasks();
11019
+ const workspace = store.readWorkspace();
11020
+ if (!listed.ok || !workspace.ok)
11021
+ return failure("storage_error", "external action state is unavailable");
11022
+ if (!workspace.data)
11023
+ return failure("not_found", "workspace not found");
11024
+ const matches = listed.data.flatMap((task) => {
11025
+ if (task.status !== "active" || task.workspaceId !== workspace.data.id)
11026
+ return [];
11027
+ if (task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
11028
+ return [];
11029
+ return task.decisions.flatMap((entry) => {
11030
+ const decision = entry.data;
11031
+ if (decision.purpose !== "action" || decision.response !== "approved" || decision.revoked !== null)
11032
+ return [];
11033
+ if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
11034
+ return [];
11035
+ const plan = planCommitDescriptor(decision.binding.approvedContent);
11036
+ if (!plan)
11037
+ return [];
11038
+ const completed = task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ?? [];
11039
+ if (plan.steps[completed.length] !== message)
11040
+ return [];
11041
+ return [{ task, entry, steps: plan.steps, branch: plan.branch, completed: [...completed] }];
11042
+ });
11043
+ });
11044
+ if (matches.length !== 1)
11045
+ return failure("permission_denied", matches.length === 0 ? "no plan commit authorization matches this message" : "multiple plan authorizations match this message");
11046
+ return success(workspace.data.revision, null, matches[0]);
11047
+ };
11048
+ var commitMessageFromDescriptor = (operation) => {
11049
+ try {
11050
+ const descriptor = JSON.parse(operation);
11051
+ return descriptor.operation === "git.commit" && typeof descriptor.payload?.message === "string" ? descriptor.payload.message : undefined;
11052
+ } catch {
11053
+ return;
11054
+ }
11055
+ };
11056
+ var planCommitBinding = (store, host, actor, operation) => {
11057
+ const message = commitMessageFromDescriptor(operation);
11058
+ if (!message)
11059
+ return null;
11060
+ const plan = approvedPlanCommit(store, host, actor, message);
11061
+ if (!plan.ok)
11062
+ return null;
11063
+ let branch;
11064
+ try {
11065
+ branch = execFileSync("git", ["branch", "--show-current"], {
11066
+ cwd: store.root,
11067
+ encoding: "utf8",
11068
+ stdio: ["pipe", "pipe", "pipe"]
11069
+ }).trim();
11070
+ } catch {
11071
+ return null;
11072
+ }
11073
+ if (!branch || branch !== plan.data.branch)
11074
+ return null;
11075
+ const workspace = store.readWorkspace();
11076
+ if (!workspace.ok || !workspace.data)
11077
+ return null;
11078
+ return {
11079
+ taskId: plan.data.task.id,
11080
+ decisionId: plan.data.entry.id,
11081
+ binding: plan.data.entry.data.binding,
11082
+ expectedRevision: plan.data.task.revision,
11083
+ expectedWorkspaceRevision: workspace.data.revision,
11084
+ step: message
11085
+ };
11086
+ };
10785
11087
  var approvedExternalAction = (store, host, actor, operation) => {
10786
11088
  const selected = externalActionState(store, host, actor, operation);
10787
- if (!selected.ok)
11089
+ if (selected.ok) {
11090
+ if (selected.data.entry.data.consumption !== null)
11091
+ 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");
10788
11092
  return selected;
10789
- if (selected.data.entry.data.consumption !== null)
10790
- 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");
11093
+ }
11094
+ const intent = approvedBranchSetupIntent(store, host, actor, operation);
11095
+ if (intent.ok) {
11096
+ if (intent.data.entry.data.consumption !== null)
11097
+ return failure(intent.data.entry.data.consumption.state === "uncertain" ? "external_outcome_unknown" : "permission_denied", intent.data.entry.data.consumption.state === "uncertain" ? "previous external action outcome is unknown" : "external action was already settled");
11098
+ return intent;
11099
+ }
10791
11100
  return selected;
10792
11101
  };
11102
+ var branchSetupIntent = (descriptor) => {
11103
+ try {
11104
+ const value = JSON.parse(descriptor);
11105
+ if (value.operation !== "git.branch_setup" || !value.payload)
11106
+ return null;
11107
+ const { target_branch, resolved } = value.payload;
11108
+ if (typeof target_branch !== "string" || !target_branch || !resolved)
11109
+ return null;
11110
+ if (typeof resolved.base_branch !== "string" || typeof resolved.target_exists !== "boolean")
11111
+ return null;
11112
+ return {
11113
+ target: target_branch,
11114
+ base: resolved.base_branch,
11115
+ remoteBase: typeof resolved.remote_base === "string" ? resolved.remote_base : null,
11116
+ targetExists: resolved.target_exists
11117
+ };
11118
+ } catch {
11119
+ return null;
11120
+ }
11121
+ };
11122
+ var approvedBranchSetupIntent = (store, host, actor, operation) => {
11123
+ const current = branchSetupIntent(operation);
11124
+ if (!current)
11125
+ return failure("permission_denied", "no approved action is bound to this session");
11126
+ const listed = store.listTasks();
11127
+ const workspace = store.readWorkspace();
11128
+ if (!listed.ok || !workspace.ok)
11129
+ return failure("storage_error", "external action state is unavailable");
11130
+ if (!workspace.data)
11131
+ return failure("not_found", "workspace not found");
11132
+ const candidates = listed.data.flatMap((task) => {
11133
+ if (task.status !== "active" || task.workspaceId !== workspace.data.id || task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
11134
+ return [];
11135
+ return task.decisions.filter((entry) => entry.data.purpose === "action" && entry.data.response === "approved" && entry.data.revoked === null).map((entry) => ({ task, entry }));
11136
+ });
11137
+ const matches = candidates.filter(({ entry }) => {
11138
+ const intent = branchSetupIntent(entry.data.binding.approvedContent);
11139
+ return intent !== null && intent.target === current.target && intent.base === current.base && intent.remoteBase === current.remoteBase && intent.targetExists === current.targetExists;
11140
+ });
11141
+ if (matches.length !== 1)
11142
+ return failure("permission_denied", matches.length === 0 ? "no approved action is bound to this session" : "multiple approved actions match this session");
11143
+ return success(workspace.data.revision, null, { ...matches[0], workspace: workspace.data });
11144
+ };
11145
+ var priorResolvedDrift = (store, host, actor, operation, payload, currentResolved) => {
11146
+ const prior = priorExternalAction(store, host, actor, operation, payload);
11147
+ if (!prior.ok)
11148
+ return success(null, null, null);
11149
+ let baseline;
11150
+ try {
11151
+ const parsed = JSON.parse(prior.data.entry.data.binding.approvedContent);
11152
+ baseline = parsed.payload?.resolved;
11153
+ } catch {
11154
+ return success(null, null, null);
11155
+ }
11156
+ if (baseline === undefined || baseline === null)
11157
+ return success(null, null, null);
11158
+ if (operation === "git.branch_setup") {
11159
+ const before = branchSetupIntent(prior.data.entry.data.binding.approvedContent);
11160
+ let after = null;
11161
+ try {
11162
+ after = branchSetupIntent(JSON.stringify({
11163
+ operation,
11164
+ payload: { ...payload, resolved: currentResolved }
11165
+ }));
11166
+ } catch {
11167
+ after = null;
11168
+ }
11169
+ if (before && after) {
11170
+ const moved = before.target !== after.target ? "target_branch" : before.base !== after.base ? "base_branch" : before.remoteBase !== after.remoteBase ? "remote_base" : before.targetExists !== after.targetExists ? "target_exists" : null;
11171
+ if (moved === null)
11172
+ return success(null, null, null);
11173
+ return failure("invalid_input", `repository state changed since approval (${moved} moved); show the current state and approve the action again`, { outcome: "not_started", operation });
11174
+ }
11175
+ }
11176
+ if (canonicalJson(baseline) === canonicalJson(currentResolved))
11177
+ return success(null, null, null);
11178
+ return failure("invalid_input", "repository state changed since approval; show the current state and approve the action again", { outcome: "not_started", operation });
11179
+ };
10793
11180
  var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
10794
11181
  let steps = [];
10795
11182
  try {
@@ -11067,7 +11454,7 @@ function resolveRequirements(input) {
11067
11454
  ].join(", ")}`,
11068
11455
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
11069
11456
  before: "dependent_action",
11070
- dependentAction: "dependent-action",
11457
+ dependentAction: null,
11071
11458
  acceptanceAllowed: false
11072
11459
  }));
11073
11460
  if (signals.productChoiceOpen.value === true)
@@ -11078,7 +11465,7 @@ function resolveRequirements(input) {
11078
11465
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
11079
11466
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
11080
11467
  before: "dependent_action",
11081
- dependentAction: "dependent-action",
11468
+ dependentAction: null,
11082
11469
  acceptanceAllowed: false
11083
11470
  }));
11084
11471
  if (signals.behaviorChange.value === true)
@@ -11236,59 +11623,6 @@ function diffPolicy(previous, next, reason, now) {
11236
11623
  return parsed.data;
11237
11624
  }
11238
11625
 
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
11626
  // packages/workit-core/src/core/task-engine.ts
11293
11627
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
11294
11628
  kind,
@@ -11419,15 +11753,20 @@ var applyWorkerLifecycle = (input) => {
11419
11753
  return failure("invalid_transition", "worker has not started");
11420
11754
  if (input.state === "running" && entry.data.state === "stopped")
11421
11755
  return failure("invalid_transition", "stopped worker cannot run again");
11756
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
11422
11757
  const nextEntry = {
11423
11758
  ...entry,
11424
11759
  recordedAt: input.now ?? entry.recordedAt,
11425
11760
  provenance: authority.provenance,
11426
- data: { ...entry.data, state: input.state, session: input.session }
11761
+ data: {
11762
+ ...entry.data,
11763
+ state: terminalCancel ? "cancelling" : input.state,
11764
+ session: input.session
11765
+ }
11427
11766
  };
11428
11767
  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
11768
  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)
11769
+ if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
11431
11770
  return success(task.data.revision, workspace.data.revision, entry);
11432
11771
  const changed = input.store.mutateTaskAndWorkspace({
11433
11772
  taskId: task.data.id,
@@ -11797,7 +12136,7 @@ class WorkitCore {
11797
12136
  activeWorkerBlocker(task, workspace) {
11798
12137
  if (workspace.writer)
11799
12138
  return failure("recovery_required", "writer ownership must be released first");
11800
- if (task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
12139
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
11801
12140
  return failure("recovery_required", "worker state requires reconciliation");
11802
12141
  return null;
11803
12142
  }
@@ -12034,11 +12373,6 @@ class WorkitCore {
12034
12373
  if (helper && !helper.ok)
12035
12374
  return helper;
12036
12375
  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
12376
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
12043
12377
  return failure("invalid_input", "missing or skipped evidence requires a reason");
12044
12378
  if (evidence.kind === "review") {
@@ -12055,6 +12389,11 @@ class WorkitCore {
12055
12389
  evidence.beforeCandidateId ??= null;
12056
12390
  evidence.candidateId ??= null;
12057
12391
  }
12392
+ if (helper?.ok) {
12393
+ const assignment = helper.data.data.assignment;
12394
+ 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))
12395
+ return failure("permission_denied", "evidence is outside the worker assignment");
12396
+ }
12058
12397
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
12059
12398
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
12060
12399
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -12108,7 +12447,12 @@ class WorkitCore {
12108
12447
  const input = parsed.data;
12109
12448
  if (nativeRequired && input.action !== "record")
12110
12449
  return failure("permission_denied", "native observation is only valid for recording decisions");
12111
- if (nativeRequired && nativeObservation === undefined)
12450
+ const stated = input.action === "record" && input.response === "stated";
12451
+ if (stated && input.purpose === "action")
12452
+ return failure("permission_denied", "stated choices cannot authorize mutating actions");
12453
+ if (stated && !input.binding?.statedChoice)
12454
+ return failure("invalid_input", "stated choices require binding.statedChoice");
12455
+ if (nativeRequired && nativeObservation === undefined && !stated)
12112
12456
  return failure("permission_denied", "native decision observation is required");
12113
12457
  const task = this.store.readTask(input.taskId);
12114
12458
  if (!task.ok)
@@ -12141,7 +12485,7 @@ class WorkitCore {
12141
12485
  consumption: null
12142
12486
  };
12143
12487
  const data = { ...base, digest: decisionDigest(base) };
12144
- const native = nativeRequired ? verifyNativeDecision(this.context.nativeAuthority, {
12488
+ const native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
12145
12489
  observation: nativeObservation,
12146
12490
  expected: {
12147
12491
  taskId: task.data.id,
@@ -12950,6 +13294,9 @@ class WorkitCore {
12950
13294
  id: task.id,
12951
13295
  revision: task.revision,
12952
13296
  workspaceRevision: workspace.data.revision,
13297
+ createdAt: task.createdAt,
13298
+ updatedAt: task.updatedAt,
13299
+ runtime: task.runtime ?? null,
12953
13300
  objective: task.intent.data.objective,
12954
13301
  status: task.status,
12955
13302
  closure: task.closure,
@@ -13035,7 +13382,7 @@ class WorkitCore {
13035
13382
  const base = reconcileResume(effectiveView);
13036
13383
  if (!base.ok)
13037
13384
  return base;
13038
- const needsReconciliation = view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
13385
+ const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
13039
13386
  const blockers = [...base.data.blockers];
13040
13387
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
13041
13388
  blockers.push({
@@ -13102,7 +13449,8 @@ class WorkitCore {
13102
13449
  ...current,
13103
13450
  status,
13104
13451
  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
13452
+ progress: current.progress,
13453
+ pauseReason: status === "paused" ? input.reason ?? null : null
13106
13454
  })
13107
13455
  });
13108
13456
  if (!changed.ok)
@@ -13138,7 +13486,7 @@ class WorkitCore {
13138
13486
  return workspace;
13139
13487
  if (!workspace.data)
13140
13488
  return failure("not_found", "workspace not found");
13141
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
13489
+ if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
13142
13490
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
13143
13491
  this.fillRevisions(input, task.data, workspace.data);
13144
13492
  const changed = this.store.mutateTaskAndWorkspace({
@@ -13256,15 +13604,15 @@ function resolveGitRevision(root, value) {
13256
13604
  }
13257
13605
  function resolveInside(root, candidate) {
13258
13606
  const base = realpathSync5(root);
13259
- const target = path6.resolve(base, candidate);
13260
- if (target !== base && !target.startsWith(base + path6.sep)) {
13607
+ const target = path7.resolve(base, candidate);
13608
+ if (target !== base && !target.startsWith(base + path7.sep)) {
13261
13609
  throw new Error("path must stay inside repository root");
13262
13610
  }
13263
13611
  let ancestor = target;
13264
- while (!existsSync3(ancestor))
13265
- ancestor = path6.dirname(ancestor);
13612
+ while (!existsSync4(ancestor))
13613
+ ancestor = path7.dirname(ancestor);
13266
13614
  const canonicalAncestor = realpathSync5(ancestor);
13267
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path6.sep)) {
13615
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
13268
13616
  throw new Error("path must stay inside repository root");
13269
13617
  }
13270
13618
  return target;
@@ -13306,7 +13654,7 @@ if (false) {}
13306
13654
  // packages/workit-core/src/core/logger.ts
13307
13655
  import { appendFileSync, mkdirSync as mkdirSync2, readdirSync as readdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
13308
13656
  import os from "node:os";
13309
- import path7 from "node:path";
13657
+ import path8 from "node:path";
13310
13658
  var REDACTED = "[REDACTED]";
13311
13659
  var DEFAULT_MAX_RATE = 20;
13312
13660
  var DEFAULT_RATE_WINDOW_MS = 1000;
@@ -13362,14 +13710,14 @@ var resolveStateDir = () => {
13362
13710
  return override;
13363
13711
  const home = os.homedir();
13364
13712
  if (process.env.XDG_STATE_HOME)
13365
- return path7.join(process.env.XDG_STATE_HOME, "workit");
13713
+ return path8.join(process.env.XDG_STATE_HOME, "workit");
13366
13714
  switch (os.platform()) {
13367
13715
  case "darwin":
13368
- return path7.join(home, "Library", "Application Support", "workit");
13716
+ return path8.join(home, "Library", "Application Support", "workit");
13369
13717
  case "win32":
13370
- return path7.join(process.env.LOCALAPPDATA ?? path7.join(home, "AppData", "Local"), "workit");
13718
+ return path8.join(process.env.LOCALAPPDATA ?? path8.join(home, "AppData", "Local"), "workit");
13371
13719
  default:
13372
- return path7.join(process.env.HOME ?? home, ".local", "state", "workit");
13720
+ return path8.join(process.env.HOME ?? home, ".local", "state", "workit");
13373
13721
  }
13374
13722
  };
13375
13723
  var homePattern = new RegExp(`${escapeRegExp(os.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
@@ -13442,14 +13790,14 @@ var pruneOldFiles = (dir) => {
13442
13790
  const files = readdirSync3(dir).filter((name) => DAY_FILE.test(name)).sort().reverse();
13443
13791
  for (const file of files.slice(RETAINED_DAYS)) {
13444
13792
  try {
13445
- unlinkSync2(path7.join(dir, file));
13793
+ unlinkSync2(path8.join(dir, file));
13446
13794
  } catch {}
13447
13795
  }
13448
13796
  } catch {}
13449
13797
  };
13450
13798
  var createLogger = (options = {}) => {
13451
13799
  const stateDir = options.stateDir ?? resolveStateDir();
13452
- const logDir = path7.join(stateDir, "logs");
13800
+ const logDir = path8.join(stateDir, "logs");
13453
13801
  const now = options.now ?? (() => new Date);
13454
13802
  const maxRate = options.maxRate ?? DEFAULT_MAX_RATE;
13455
13803
  const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
@@ -13483,7 +13831,7 @@ var createLogger = (options = {}) => {
13483
13831
  const line = `${JSON.stringify(event)}
13484
13832
  `;
13485
13833
  mkdirSync2(logDir, { recursive: true, mode: 448 });
13486
- appendFileSync(path7.join(logDir, dailyFileName(date)), line, { mode: 384 });
13834
+ appendFileSync(path8.join(logDir, dailyFileName(date)), line, { mode: 384 });
13487
13835
  pruneOldFiles(logDir);
13488
13836
  } catch {
13489
13837
  try {
@@ -13494,7 +13842,7 @@ var createLogger = (options = {}) => {
13494
13842
  context: { redaction_failed: true }
13495
13843
  };
13496
13844
  mkdirSync2(logDir, { recursive: true, mode: 448 });
13497
- appendFileSync(path7.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
13845
+ appendFileSync(path8.join(logDir, dailyFileName(date)), `${JSON.stringify(event)}
13498
13846
  `, {
13499
13847
  mode: 384
13500
13848
  });
@@ -13577,6 +13925,24 @@ OpenCode Workit decisions use the native question header "Workit decision: <purp
13577
13925
  return cached2;
13578
13926
  };
13579
13927
 
13928
+ // packages/workit-opencode/src/stale-sources.ts
13929
+ import { existsSync as existsSync5 } from "node:fs";
13930
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
13931
+ var pluginSourceFiles = [
13932
+ fileURLToPath2(import.meta.url),
13933
+ fileURLToPath2(new URL("./plugin.ts", import.meta.url)),
13934
+ fileURLToPath2(new URL("./tools/workit.ts", import.meta.url)),
13935
+ ...[
13936
+ "task-contract.ts",
13937
+ "task-engine.ts",
13938
+ "task-evaluation.ts",
13939
+ "task-store.ts",
13940
+ "workers.ts",
13941
+ "authority.ts",
13942
+ "methods.ts"
13943
+ ].map((file) => fileURLToPath2(new URL(`../../workit-core/src/core/${file}`, import.meta.url)))
13944
+ ].filter((file) => existsSync5(file));
13945
+
13580
13946
  // packages/workit-opencode/src/tools/repo.ts
13581
13947
  import path22 from "node:path";
13582
13948
  import { realpathSync as realpathSync8 } from "node:fs";
@@ -25906,25 +26272,6 @@ import fs7 from "node:fs";
25906
26272
  import path9 from "node:path";
25907
26273
  import { createHash as createHash5 } from "node:crypto";
25908
26274
 
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
26275
  // packages/workit-core/src/core/scripts.ts
25929
26276
  var PLUGIN_ROOT = packageRoot();
25930
26277
  var ASSET_ROOT = assetRoot();
@@ -26257,10 +26604,10 @@ function changelogApply({
26257
26604
  }
26258
26605
 
26259
26606
  // packages/workit-core/src/core/git.ts
26260
- import { execFileSync } from "node:child_process";
26607
+ import { execFileSync as execFileSync2 } from "node:child_process";
26261
26608
  var run2 = (cwd, args) => {
26262
26609
  try {
26263
- const stdout = execFileSync("git", args, {
26610
+ const stdout = execFileSync2("git", args, {
26264
26611
  cwd,
26265
26612
  encoding: "utf8",
26266
26613
  stdio: ["pipe", "pipe", "pipe"]
@@ -26378,15 +26725,15 @@ function parseVerifyOutput(stdout) {
26378
26725
  }
26379
26726
 
26380
26727
  // 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";
26728
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
26729
+ import { execFileSync as execFileSync3 } from "node:child_process";
26383
26730
  import path13 from "node:path";
26384
26731
 
26385
26732
  // packages/workit-core/src/core/config.ts
26386
26733
  import {
26387
26734
  copyFileSync,
26388
26735
  cpSync,
26389
- existsSync as existsSync5,
26736
+ existsSync as existsSync6,
26390
26737
  mkdirSync as mkdirSync3,
26391
26738
  readFileSync as readFileSync4,
26392
26739
  readdirSync as readdirSync4,
@@ -26426,11 +26773,11 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
26426
26773
  return dir;
26427
26774
  }
26428
26775
  const legacy = path10.join(process.env.XDG_CONFIG_HOME || path10.join(os2.homedir(), ".config"), "workflow-toolkit");
26429
- if (!existsSync5(legacy)) {
26776
+ if (!existsSync6(legacy)) {
26430
26777
  migratedDir = dir;
26431
26778
  return dir;
26432
26779
  }
26433
- if (!migrationFailed && existsSync5(dir)) {
26780
+ if (!migrationFailed && existsSync6(dir)) {
26434
26781
  migratedDir = dir;
26435
26782
  return dir;
26436
26783
  }
@@ -26440,7 +26787,7 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
26440
26787
  for (const entry of readdirSync4(legacy, { withFileTypes: true })) {
26441
26788
  const src = path10.join(legacy, entry.name);
26442
26789
  const dest = path10.join(dir, entry.name);
26443
- if (existsSync5(dest))
26790
+ if (existsSync6(dest))
26444
26791
  continue;
26445
26792
  try {
26446
26793
  if (entry.isDirectory())
@@ -27018,7 +27365,7 @@ var baseBranch = (cwd) => {
27018
27365
  return { base: String(resolved.defaultTargetBranch ?? "develop") };
27019
27366
  };
27020
27367
  var originBaseReady = (cwd, base) => {
27021
- const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
27368
+ const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
27022
27369
  try {
27023
27370
  try {
27024
27371
  run(["fetch", "origin", base, "--prune"]);
@@ -27026,7 +27373,7 @@ var originBaseReady = (cwd, base) => {
27026
27373
  run(["fetch", "origin", "--prune"]);
27027
27374
  }
27028
27375
  try {
27029
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
27376
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
27030
27377
  cwd,
27031
27378
  stdio: "pipe"
27032
27379
  });
@@ -27045,11 +27392,11 @@ var originBaseReady = (cwd, base) => {
27045
27392
  }
27046
27393
  };
27047
27394
  var fastForwardBase = (cwd, base) => {
27048
- const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
27395
+ const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
27049
27396
  try {
27050
27397
  let hasLocalBase = true;
27051
27398
  try {
27052
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
27399
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
27053
27400
  cwd,
27054
27401
  stdio: "pipe"
27055
27402
  });
@@ -27090,18 +27437,17 @@ var branchSetup = ({
27090
27437
  log
27091
27438
  }) => {
27092
27439
  const cwd = path13.resolve(workspace_root);
27093
- const exec = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
27440
+ const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
27094
27441
  const current = gitContext(cwd).branch;
27095
27442
  if (!current || current === "unknown")
27096
- return { error: "not in a git repository" };
27443
+ return { error: "not in a git repository", phase: "preflight" };
27097
27444
  const sdd = sdd_dir ?? "docs";
27098
27445
  const manifestPath = path13.isAbsolute(sdd) ? path13.join(sdd, "manifest.json") : path13.join(cwd, sdd, "manifest.json");
27099
27446
  const ensureManifestDir = () => {
27100
27447
  const dir = path13.dirname(manifestPath);
27101
- if (!existsSync6(dir))
27448
+ if (!existsSync7(dir))
27102
27449
  mkdirSync4(dir, { recursive: true, mode: 493 });
27103
27450
  };
27104
- ensureManifestDir();
27105
27451
  const readManifest = () => {
27106
27452
  try {
27107
27453
  return JSON.parse(readFileSync6(manifestPath, "utf8"));
@@ -27119,13 +27465,16 @@ var branchSetup = ({
27119
27465
  const manifest = readManifest();
27120
27466
  const ref = manifest.stash_ref;
27121
27467
  if (!ref)
27122
- return { error: "no stash_ref in manifest" };
27468
+ return { error: "no stash_ref in manifest", phase: "preflight" };
27123
27469
  journal(`pre-pop: ${String(ref)}`);
27124
27470
  try {
27125
27471
  exec(["stash", "pop", String(ref)]);
27126
27472
  } catch (error) {
27127
27473
  journal("pop: failed");
27128
- return { error: error instanceof Error ? error.message : "stash pop failed" };
27474
+ return {
27475
+ error: error instanceof Error ? error.message : "stash pop failed",
27476
+ phase: "post_mutation"
27477
+ };
27129
27478
  }
27130
27479
  journal("pop: ok");
27131
27480
  delete manifest.stash_ref;
@@ -27135,11 +27484,17 @@ var branchSetup = ({
27135
27484
  }
27136
27485
  const target = target_branch ?? "";
27137
27486
  if (!target)
27138
- return { error: "target branch required" };
27487
+ return {
27488
+ error: "target_branch (the working branch to create or switch to) is required for setup",
27489
+ phase: "preflight"
27490
+ };
27139
27491
  if (isProtected(cwd, target))
27140
- return { error: `protected branch ${target}` };
27492
+ return { error: `protected branch ${target}`, phase: "preflight" };
27141
27493
  if (!allowedBranch(cwd, target))
27142
- return { error: `target branch ${target} is not allowed by the branch policy` };
27494
+ return {
27495
+ error: `target branch ${target} is not allowed by the branch policy`,
27496
+ phase: "preflight"
27497
+ };
27143
27498
  let base;
27144
27499
  let targetExists = true;
27145
27500
  try {
@@ -27150,12 +27505,12 @@ var branchSetup = ({
27150
27505
  if (!targetExists) {
27151
27506
  const baseResolved = baseBranch(cwd);
27152
27507
  if ("error" in baseResolved)
27153
- return { error: baseResolved.error };
27508
+ return { error: baseResolved.error, phase: "preflight" };
27154
27509
  base = baseResolved.base;
27155
27510
  }
27156
27511
  journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
27157
27512
  let stash_ref;
27158
- const failAfterStash = (message) => {
27513
+ const failAfterStash = (message, phase = "post_mutation") => {
27159
27514
  let suffix = "";
27160
27515
  if (stash_ref) {
27161
27516
  journal(`pre-pop: ${stash_ref}`);
@@ -27168,27 +27523,31 @@ var branchSetup = ({
27168
27523
  suffix = " (changes preserved in stash)";
27169
27524
  }
27170
27525
  }
27171
- return { error: `${message}${suffix}` };
27526
+ return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
27172
27527
  };
27173
27528
  if (current !== target) {
27174
27529
  const dirty = Boolean(gitContext(cwd).status_short.trim());
27175
27530
  if (dirty && stash !== "yes") {
27176
27531
  return {
27177
- error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes"
27532
+ error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
27533
+ phase: "preflight"
27178
27534
  };
27179
27535
  }
27180
27536
  let validatedBase;
27181
27537
  if (!targetExists && base !== undefined) {
27182
27538
  const ready = originBaseReady(cwd, base);
27183
27539
  if (!ready.ok)
27184
- return { error: ready.error ?? "ensure-base-branch failed" };
27540
+ return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
27185
27541
  validatedBase = base;
27186
27542
  }
27187
27543
  if (dirty) {
27188
27544
  try {
27189
27545
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
27190
27546
  } catch (error) {
27191
- return { error: error instanceof Error ? error.message : "stash push failed" };
27547
+ return {
27548
+ error: error instanceof Error ? error.message : "stash push failed",
27549
+ phase: "post_mutation"
27550
+ };
27192
27551
  }
27193
27552
  stash_ref = "stash@{0}";
27194
27553
  journal(`stash push: ${stash_ref}`);
@@ -27199,14 +27558,14 @@ var branchSetup = ({
27199
27558
  } catch (error) {
27200
27559
  const message = error instanceof Error ? error.message : "checkout failed";
27201
27560
  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)`);
27561
+ return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
27203
27562
  }
27204
27563
  try {
27205
27564
  let effectiveBase = base;
27206
27565
  if (effectiveBase === undefined) {
27207
27566
  const lateResolved = baseBranch(cwd);
27208
27567
  if ("error" in lateResolved)
27209
- return failAfterStash(lateResolved.error);
27568
+ return failAfterStash(lateResolved.error, "preflight");
27210
27569
  effectiveBase = lateResolved.base;
27211
27570
  }
27212
27571
  const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
@@ -27252,7 +27611,7 @@ var branchSetup = ({
27252
27611
  };
27253
27612
 
27254
27613
  // packages/workit-core/src/core/gitignore.ts
27255
- import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
27614
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
27256
27615
  import path14 from "node:path";
27257
27616
  var GITIGNORE_ENTRIES = [
27258
27617
  "# workit: SDD working state (never commit)",
@@ -27274,7 +27633,7 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
27274
27633
  if (!confirmed)
27275
27634
  return { ok: false, error: "confirmed: true required" };
27276
27635
  const file = path14.join(workspaceRoot, ".gitignore");
27277
- const existing = existsSync7(file) ? readFileSync7(file, "utf8") : "";
27636
+ const existing = existsSync8(file) ? readFileSync7(file, "utf8") : "";
27278
27637
  const existingLines = new Set(existing.split(`
27279
27638
  `).map((l) => l.trim()).filter(Boolean));
27280
27639
  const added = [];
@@ -27293,19 +27652,19 @@ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
27293
27652
  ` : "") + append.join(`
27294
27653
  `) + `
27295
27654
  `, "utf8");
27296
- } else if (!existsSync7(file)) {
27655
+ } else if (!existsSync8(file)) {
27297
27656
  writeFileSync4(file, "", "utf8");
27298
27657
  }
27299
27658
  return { ok: true, path: file, added };
27300
27659
  };
27301
27660
 
27302
27661
  // packages/workit-core/src/core/hygiene.ts
27303
- import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
27662
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
27304
27663
  import path15 from "node:path";
27305
27664
  var repoRoot = assetRoot();
27306
27665
  var templatesDir = () => path15.join(repoRoot, "templates", "hygiene");
27307
27666
  var packageJson = (root) => {
27308
- if (!existsSync8(path15.join(root, "package.json")))
27667
+ if (!existsSync9(path15.join(root, "package.json")))
27309
27668
  return null;
27310
27669
  try {
27311
27670
  return JSON.parse(readFileSync8(path15.join(root, "package.json"), "utf8"));
@@ -27314,7 +27673,7 @@ var packageJson = (root) => {
27314
27673
  }
27315
27674
  };
27316
27675
  var isOpenSource = (root) => {
27317
- if (existsSync8(path15.join(root, "LICENSE")))
27676
+ if (existsSync9(path15.join(root, "LICENSE")))
27318
27677
  return true;
27319
27678
  const pkg = packageJson(root);
27320
27679
  if (pkg && !pkg.private)
@@ -27342,10 +27701,10 @@ var planHygieneFiles = (root, opts = {}) => {
27342
27701
  files.push("LICENSE", "CONTRIBUTING.md");
27343
27702
  const planned = [];
27344
27703
  for (const file of files) {
27345
- if (existsSync8(path15.join(root, file)))
27704
+ if (existsSync9(path15.join(root, file)))
27346
27705
  continue;
27347
27706
  const tpl = path15.join(templatesDir(), file);
27348
- if (!existsSync8(tpl))
27707
+ if (!existsSync9(tpl))
27349
27708
  continue;
27350
27709
  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
27710
  planned.push({ path: path15.join(root, file), content });
@@ -27365,7 +27724,7 @@ var ensureHygieneFiles = (root, opts) => {
27365
27724
 
27366
27725
  // packages/workit-core/src/core/repo-context.ts
27367
27726
  import { spawnSync as spawnSync4 } from "node:child_process";
27368
- import { existsSync as existsSync9, readFileSync as readFileSync9, readdirSync as readdirSync5 } from "node:fs";
27727
+ import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync5 } from "node:fs";
27369
27728
  import path16 from "node:path";
27370
27729
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
27371
27730
  try {
@@ -27532,7 +27891,7 @@ var diffStatForRange = (cwd, range) => {
27532
27891
  };
27533
27892
  var packageScripts = (cwd, keys) => {
27534
27893
  const pkgPath = path16.join(cwd, "package.json");
27535
- if (!existsSync9(pkgPath))
27894
+ if (!existsSync10(pkgPath))
27536
27895
  return [];
27537
27896
  try {
27538
27897
  const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
@@ -27567,7 +27926,7 @@ var documentationFiles = (cwd) => {
27567
27926
  return matches.sort().slice(0, 200);
27568
27927
  };
27569
27928
  var readTrimmed = (file, maxLines) => {
27570
- if (!existsSync9(file))
27929
+ if (!existsSync10(file))
27571
27930
  return "";
27572
27931
  const lines = readFileSync9(file, "utf8").split(`
27573
27932
  `);
@@ -27643,7 +28002,7 @@ ${status.stderr}` : "");
27643
28002
  stdout += `
27644
28003
  `;
27645
28004
  stdout += printSection("Recent Validation Signals");
27646
- if (existsSync9(path16.join(cwd, "package.json"))) {
28005
+ if (existsSync10(path16.join(cwd, "package.json"))) {
27647
28006
  stdout += `package.json detected. Common scripts:
27648
28007
  `;
27649
28008
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -27652,7 +28011,7 @@ ${status.stderr}` : "");
27652
28011
  `) + `
27653
28012
  `;
27654
28013
  }
27655
- if (existsSync9(path16.join(cwd, "Cargo.toml")) || existsSync9(path16.join(cwd, "src-tauri/Cargo.toml"))) {
28014
+ if (existsSync10(path16.join(cwd, "Cargo.toml")) || existsSync10(path16.join(cwd, "src-tauri/Cargo.toml"))) {
27656
28015
  stdout += `Rust project detected.
27657
28016
  `;
27658
28017
  }
@@ -27704,7 +28063,7 @@ function changelogContext(root, range) {
27704
28063
  `;
27705
28064
  stdout += printSection("Existing CHANGELOG.md");
27706
28065
  const changelogPath = path16.join(cwd, "CHANGELOG.md");
27707
- if (existsSync9(changelogPath)) {
28066
+ if (existsSync10(changelogPath)) {
27708
28067
  stdout += readTrimmed(changelogPath, 260) + `
27709
28068
  `;
27710
28069
  } else {
@@ -27746,7 +28105,7 @@ function docsRefreshContext(root, range) {
27746
28105
  `;
27747
28106
  stdout += printSection("README Preview");
27748
28107
  const readme = path16.join(cwd, "README.md");
27749
- if (existsSync9(readme)) {
28108
+ if (existsSync10(readme)) {
27750
28109
  stdout += readTrimmed(readme, 220) + `
27751
28110
  `;
27752
28111
  } else {
@@ -27755,7 +28114,7 @@ function docsRefreshContext(root, range) {
27755
28114
  }
27756
28115
  stdout += printSection("Package Scripts");
27757
28116
  const pkgPath = path16.join(cwd, "package.json");
27758
- if (existsSync9(pkgPath)) {
28117
+ if (existsSync10(pkgPath)) {
27759
28118
  try {
27760
28119
  const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
27761
28120
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
@@ -27806,7 +28165,7 @@ function releaseNotesContext(root, rangeOrTag) {
27806
28165
  `;
27807
28166
  stdout += printSection("Existing Release Files");
27808
28167
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
27809
- if (existsSync9(path16.join(cwd, rel)))
28168
+ if (existsSync10(path16.join(cwd, rel)))
27810
28169
  stdout += rel + `
27811
28170
  `;
27812
28171
  }
@@ -27835,7 +28194,7 @@ function releaseNotesContext(root, rangeOrTag) {
27835
28194
 
27836
28195
  // packages/workit-core/src/core/verify-project.ts
27837
28196
  import { spawnSync as spawnSync5 } from "node:child_process";
27838
- import { existsSync as existsSync10, readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
28197
+ import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
27839
28198
  import path17 from "node:path";
27840
28199
  var commandOnPath = (name) => {
27841
28200
  const dirs = (process.env.PATH ?? "").split(path17.delimiter);
@@ -27860,7 +28219,7 @@ var commandOnPath = (name) => {
27860
28219
  };
27861
28220
  var hasScript = (cwd, name) => {
27862
28221
  const pkgPath = path17.join(cwd, "package.json");
27863
- if (!existsSync10(pkgPath))
28222
+ if (!existsSync11(pkgPath))
27864
28223
  return false;
27865
28224
  try {
27866
28225
  const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
@@ -27870,10 +28229,10 @@ var hasScript = (cwd, name) => {
27870
28229
  }
27871
28230
  };
27872
28231
  var packageRunner = (cwd) => {
27873
- if (existsSync10(path17.join(cwd, "pnpm-lock.yaml")))
28232
+ if (existsSync11(path17.join(cwd, "pnpm-lock.yaml")))
27874
28233
  return "pnpm";
27875
28234
  const pkgPath = path17.join(cwd, "package.json");
27876
- if (existsSync10(pkgPath)) {
28235
+ if (existsSync11(pkgPath)) {
27877
28236
  try {
27878
28237
  const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
27879
28238
  if (typeof pkg.packageManager === "string" && pkg.packageManager.startsWith("pnpm")) {
@@ -27881,7 +28240,7 @@ var packageRunner = (cwd) => {
27881
28240
  }
27882
28241
  } catch {}
27883
28242
  }
27884
- if (existsSync10(path17.join(cwd, "yarn.lock")))
28243
+ if (existsSync11(path17.join(cwd, "yarn.lock")))
27885
28244
  return "yarn";
27886
28245
  return "npm";
27887
28246
  };
@@ -27942,7 +28301,7 @@ root: ${cwd}
27942
28301
  lines.push(`dry_run: ${String(dryRun)}
27943
28302
  `);
27944
28303
  const pkgPath = path17.join(cwd, "package.json");
27945
- if (existsSync10(pkgPath)) {
28304
+ if (existsSync11(pkgPath)) {
27946
28305
  const runner = packageRunner(cwd);
27947
28306
  for (const script of ["lint", "format:check", "test", "build"]) {
27948
28307
  if (hasScript(cwd, script)) {
@@ -27957,7 +28316,7 @@ root: ${cwd}
27957
28316
  } else {
27958
28317
  skipCheck("javascript", "package.json not found");
27959
28318
  }
27960
- const cargoManifest = existsSync10(path17.join(cwd, "Cargo.toml")) ? "Cargo.toml" : existsSync10(path17.join(cwd, "src-tauri/Cargo.toml")) ? "src-tauri/Cargo.toml" : "";
28319
+ const cargoManifest = existsSync11(path17.join(cwd, "Cargo.toml")) ? "Cargo.toml" : existsSync11(path17.join(cwd, "src-tauri/Cargo.toml")) ? "src-tauri/Cargo.toml" : "";
27961
28320
  if (cargoManifest) {
27962
28321
  runCheck("cargo fmt", "cargo", ["fmt", "--manifest-path", cargoManifest, "--", "--check"]);
27963
28322
  runCheck("cargo clippy", "cargo", [
@@ -27973,7 +28332,7 @@ root: ${cwd}
27973
28332
  } else {
27974
28333
  skipCheck("rust", "Cargo.toml not found");
27975
28334
  }
27976
- if (existsSync10(path17.join(cwd, "pyproject.toml")) || existsSync10(path17.join(cwd, "pytest.ini")) || existsSync10(path17.join(cwd, "tests"))) {
28335
+ if (existsSync11(path17.join(cwd, "pyproject.toml")) || existsSync11(path17.join(cwd, "pytest.ini")) || existsSync11(path17.join(cwd, "tests"))) {
27977
28336
  if (commandOnPath("pytest"))
27978
28337
  runCheck("pytest", "pytest", []);
27979
28338
  else
@@ -27995,7 +28354,7 @@ root: ${cwd}
27995
28354
  lines.push(`status: skipped (dry run)
27996
28355
  `);
27997
28356
  skipped += 1;
27998
- } else if (existsSync10(changelogPath)) {
28357
+ } else if (existsSync11(changelogPath)) {
27999
28358
  if (/## \[Unreleased\]/i.test(readFileSync10(changelogPath, "utf8"))) {
28000
28359
  lines.push(`status: pass
28001
28360
  `);
@@ -28330,7 +28689,7 @@ function writeFileExclusive(file, content, mode) {
28330
28689
  import {
28331
28690
  copyFileSync as copyFileSync2,
28332
28691
  cpSync as cpSync2,
28333
- existsSync as existsSync11,
28692
+ existsSync as existsSync12,
28334
28693
  lstatSync as lstatSync4,
28335
28694
  mkdirSync as mkdirSync5,
28336
28695
  mkdtempSync,
@@ -29871,7 +30230,7 @@ function createRepoTools(runtime = defaultRuntime) {
29871
30230
  return output(legacyScriptResult({
29872
30231
  stdout: JSON.stringify(result),
29873
30232
  stderr: "",
29874
- exitCode: result.error ? 1 : 0,
30233
+ exitCode: "error" in result ? 1 : 0,
29875
30234
  cwd: context.directory
29876
30235
  }));
29877
30236
  }
@@ -30519,6 +30878,101 @@ var safePath = (root, value) => {
30519
30878
  return null;
30520
30879
  }
30521
30880
  };
30881
+ var branchCreationBaseline = (root, target, base) => {
30882
+ const exists = gitValue(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]) !== null;
30883
+ if (exists)
30884
+ return { base_branch: base, target_exists: true, remote_base: null };
30885
+ const remote = run4(root, ["ls-remote", "origin", `refs/heads/${base}`]);
30886
+ const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
30887
+ return {
30888
+ base_branch: base,
30889
+ target_exists: false,
30890
+ remote_base: /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null
30891
+ };
30892
+ };
30893
+ var shortSha = (value) => typeof value === "string" && value ? value.slice(0, 8) : "unknown";
30894
+ var isProtectedBranch2 = (root, branch) => resolveBranchPolicyFor(root).protected.has(branch.toLowerCase());
30895
+ var actionProposalQuestion = (request, descriptorPayload) => {
30896
+ const payload = descriptorPayload ?? {};
30897
+ const resolved = payload.resolved ?? {};
30898
+ const payloadTarget = String(payload.target_branch ?? "");
30899
+ if (request.operation === "git.commit" && Array.isArray(payload.plan_steps)) {
30900
+ const steps = payload.plan_steps;
30901
+ return {
30902
+ presented: `Workit decision: action — Approve committing the plan's ${steps.length} listed tasks on \`${String(payload.plan_branch ?? "")}\`?`,
30903
+ approvedText: `Approve the ${steps.length} listed plan commits.`
30904
+ };
30905
+ }
30906
+ switch (request.operation) {
30907
+ case "git.branch_setup": {
30908
+ if (request.payload.action === "reapply_stash")
30909
+ return {
30910
+ presented: "Workit decision: action — Reapply the pre-checkout stash recorded in the branch manifest?",
30911
+ approvedText: "Reapply the recorded stash."
30912
+ };
30913
+ const target = String(payload.target_branch ?? "");
30914
+ const stashes = payload.stash === "yes" && resolved.dirty === true;
30915
+ if (resolved.target_exists === true)
30916
+ return stashes ? {
30917
+ presented: `Workit decision: action — Stash the dirty working tree and switch to branch \`${target}\`?`,
30918
+ approvedText: `Stash and switch to \`${target}\`.`
30919
+ } : {
30920
+ presented: `Workit decision: action — Switch to branch \`${target}\`?`,
30921
+ approvedText: `Switch to \`${target}\`.`
30922
+ };
30923
+ const base = String(resolved.base_branch ?? "the base branch");
30924
+ return stashes ? {
30925
+ presented: `Workit decision: action — Stash the dirty working tree and create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
30926
+ approvedText: `Stash and create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
30927
+ } : {
30928
+ presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
30929
+ approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
30930
+ };
30931
+ }
30932
+ case "git.commit": {
30933
+ const paths = Array.isArray(resolved.paths) ? resolved.paths : [];
30934
+ return {
30935
+ presented: `Workit decision: action — Commit ${paths.length} staged file(s) as "${String(request.payload.message)}"?`,
30936
+ approvedText: `Commit as "${String(request.payload.message)}".`
30937
+ };
30938
+ }
30939
+ case "git.push":
30940
+ return {
30941
+ presented: `Workit decision: action — Push branch \`${String(payload.branch ?? "")}\` to origin?`,
30942
+ approvedText: `Push \`${String(payload.branch ?? "")}\` to origin.`
30943
+ };
30944
+ case "hosting.pull_request":
30945
+ return {
30946
+ presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
30947
+ approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
30948
+ };
30949
+ case "changelog.apply":
30950
+ return {
30951
+ presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
30952
+ approvedText: `Apply changelog changes at ${String(payload.path ?? "CHANGELOG.md")}.`
30953
+ };
30954
+ case "youtrack.update":
30955
+ return {
30956
+ presented: `Workit decision: action — Update YouTrack issue ${String(request.payload.issueId)}${request.payload.minutes ? ` and log ${String(request.payload.minutes)}m` : ""}?`,
30957
+ approvedText: `Update YouTrack issue ${String(request.payload.issueId)}.`
30958
+ };
30959
+ case "youtrack.time":
30960
+ return {
30961
+ presented: `Workit decision: action — Log ${String(request.payload.minutes)}m to YouTrack issue ${String(request.payload.issueId)}?`,
30962
+ approvedText: `Log time to YouTrack issue ${String(request.payload.issueId)}.`
30963
+ };
30964
+ case "youtrack.meeting":
30965
+ return {
30966
+ presented: `Workit decision: action — Log meeting time to YouTrack issue ${String(request.payload.issueId)}?`,
30967
+ approvedText: `Log meeting time to YouTrack issue ${String(request.payload.issueId)}.`
30968
+ };
30969
+ default:
30970
+ return {
30971
+ presented: `Workit decision: action — Approve ${request.operation}?`,
30972
+ approvedText: `Approve ${request.operation}.`
30973
+ };
30974
+ }
30975
+ };
30522
30976
  var approvedResolvedExternalAction = (content) => {
30523
30977
  try {
30524
30978
  const value = JSON.parse(content);
@@ -30782,24 +31236,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30782
31236
  const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
30783
31237
  if (!sdd_dir)
30784
31238
  return failure("invalid_input", "sdd_dir must stay inside the workspace");
31239
+ const action = request.payload.action ?? "setup";
30785
31240
  const policy = resolveBranchPolicyFor(root);
30786
- const target_branch = request.payload.target_branch ?? policy.defaultTargetBranch;
31241
+ const target_branch = request.payload.target_branch;
31242
+ if (action !== "reapply_stash" && !target_branch)
31243
+ return failure("invalid_input", "target_branch (the working branch to create or switch to) is required for setup", {
31244
+ outcome: "not_started",
31245
+ fields: [{ path: "target_branch", reason: "required for action=setup" }]
31246
+ });
30787
31247
  const head = gitValue(root, ["rev-parse", "HEAD"]);
30788
31248
  if (!head)
30789
31249
  return failure("storage_error", "current Git commit could not be resolved");
31250
+ const dirty = Boolean((gitRaw(root, ["status", "--short"]) ?? "").trim());
31251
+ const creation = action === "reapply_stash" || !target_branch ? { base_branch: policy.defaultTargetBranch } : branchCreationBaseline(root, target_branch, policy.defaultTargetBranch);
30790
31252
  const normalized = {
30791
31253
  operation: request.operation,
30792
31254
  payload: {
30793
31255
  ...request.payload,
30794
- action: request.payload.action ?? "setup",
31256
+ action,
30795
31257
  sdd_dir,
30796
31258
  stash: request.payload.stash ?? "no",
30797
- target_branch
31259
+ ...target_branch ? { target_branch } : {}
30798
31260
  }
30799
31261
  };
30800
31262
  return success(null, null, {
30801
31263
  request: normalized,
30802
- descriptorPayload: { ...normalized.payload, resolved: { head, target_branch } }
31264
+ descriptorPayload: {
31265
+ ...normalized.payload,
31266
+ resolved: { head, dirty, ...creation }
31267
+ }
30803
31268
  });
30804
31269
  }
30805
31270
  case "git.push": {
@@ -30813,7 +31278,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30813
31278
  }) : failure("invalid_input", "git push requires a resolvable current branch");
30814
31279
  }
30815
31280
  case "git.commit": {
31281
+ const planSteps = request.payload.plan_steps;
31282
+ const planBranch = request.payload.plan_branch;
30816
31283
  const head = gitValue(root, ["rev-parse", "HEAD"]);
31284
+ if (!head)
31285
+ return failure("storage_error", "Git state could not be resolved");
31286
+ const branch = gitValue(root, ["branch", "--show-current"]);
31287
+ if (planSteps !== undefined || planBranch !== undefined) {
31288
+ if (!Array.isArray(planSteps) || planSteps.some((step) => typeof step !== "string" || !step.trim()) || new Set(planSteps).size !== planSteps.length || planSteps.length === 0 || planSteps.length > 32)
31289
+ return failure("invalid_input", "plan_steps must be 1-32 unique non-empty commit messages", {
31290
+ outcome: "not_started",
31291
+ fields: [{ path: "plan_steps", reason: "unique strings" }]
31292
+ });
31293
+ if (typeof planBranch !== "string" || !planBranch.trim() || planBranch !== branch)
31294
+ return failure("invalid_input", "plan_branch must name the current working branch", {
31295
+ outcome: "not_started",
31296
+ fields: [{ path: "plan_branch", reason: "current branch" }]
31297
+ });
31298
+ if (isProtectedBranch2(root, planBranch))
31299
+ return failure("invalid_input", "plan_branch must not be a protected branch", {
31300
+ outcome: "not_started",
31301
+ fields: [{ path: "plan_branch", reason: "protected branch" }]
31302
+ });
31303
+ return success(null, null, {
31304
+ request,
31305
+ descriptorPayload: {
31306
+ plan_steps: planSteps,
31307
+ plan_branch: planBranch,
31308
+ resolved: { head, branch, steps: planSteps }
31309
+ }
31310
+ });
31311
+ }
30817
31312
  const staged = gitRaw(root, [
30818
31313
  "diff",
30819
31314
  "--cached",
@@ -30824,7 +31319,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30824
31319
  "HEAD"
30825
31320
  ]);
30826
31321
  const paths = stagedPaths(root);
30827
- if (!head || staged === null || paths === null)
31322
+ const message = request.payload.message;
31323
+ if (typeof message !== "string" || !message)
31324
+ return failure("invalid_input", "git.commit requires a commit message", {
31325
+ outcome: "not_started",
31326
+ fields: [{ path: "message", reason: "required unless plan_steps is provided" }]
31327
+ });
31328
+ if (staged === null || paths === null)
30828
31329
  return failure("storage_error", "Git state could not be resolved");
30829
31330
  let policy;
30830
31331
  try {
@@ -30839,13 +31340,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
30839
31340
  `).filter(Boolean));
30840
31341
  flavor = detected.flavor ?? "conventional";
30841
31342
  }
30842
- if (!matchCommitFlavor(request.payload.message, flavor, policy.pattern))
31343
+ if (!matchCommitFlavor(message, flavor, policy.pattern))
30843
31344
  return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
30844
31345
  return success(null, null, {
30845
31346
  request,
30846
31347
  descriptorPayload: {
30847
31348
  ...request.payload,
30848
- resolved: { head, staged: sha256(staged), paths }
31349
+ resolved: { head, branch, staged: sha256(staged), paths }
30849
31350
  }
30850
31351
  });
30851
31352
  }
@@ -31015,10 +31516,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
31015
31516
  sdd_dir: request.payload.sdd_dir ?? "docs",
31016
31517
  workspace_root: root
31017
31518
  });
31018
- return result.error ? unknown3(request.operation) : success(null, null, result);
31519
+ if ("error" in result) {
31520
+ if (result.phase === "preflight")
31521
+ return failure("invalid_input", result.error, {
31522
+ outcome: "not_started",
31523
+ operation: request.operation
31524
+ });
31525
+ return unknown3(request.operation);
31526
+ }
31527
+ return success(null, null, result);
31019
31528
  }
31020
31529
  case "git.commit": {
31021
- const result = run4(root, ["commit", "-m", request.payload.message]);
31530
+ const message = request.payload.message;
31531
+ if (typeof message !== "string" || !message)
31532
+ return failure("invalid_input", "git.commit requires a commit message", {
31533
+ outcome: "not_started",
31534
+ fields: [{ path: "message", reason: "required" }]
31535
+ });
31536
+ const result = run4(root, ["commit", "-m", message]);
31022
31537
  return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown3(request.operation);
31023
31538
  }
31024
31539
  case "git.push": {
@@ -31128,8 +31643,15 @@ var sessionParent = (session) => {
31128
31643
  const parentID = session.parentID;
31129
31644
  return typeof parentID === "string" && parentID.length > 0 ? parentID : null;
31130
31645
  };
31131
- var freshMs = 5 * 60 * 1000;
31132
31646
  var rejectedDescription = "Reject this decision";
31647
+ var isSelfAuthorizingActionContent = (content) => {
31648
+ try {
31649
+ const value = JSON.parse(content);
31650
+ return typeof value.operation === "string" && value.operation.length > 0;
31651
+ } catch {
31652
+ return /^[a-z][a-z_]*\.[a-z_]+$/.test(content);
31653
+ }
31654
+ };
31133
31655
  var decisionContent = (purpose, question, approvedContent) => ({
31134
31656
  header: `Workit decision: ${purpose}`,
31135
31657
  question,
@@ -31235,15 +31757,31 @@ class NativeReceiptStore {
31235
31757
  queue.splice(i, 1);
31236
31758
  if (!queue.length)
31237
31759
  this.#receipts.delete(sessionID);
31238
- if (this.#now() - receipt.recordedAt > freshMs)
31239
- return { ok: false, error: "permission_denied: native question receipt is stale" };
31240
31760
  const observation = { receipt };
31241
31761
  this.#observations.add(observation);
31242
31762
  return { ok: true, receipt, observation };
31243
31763
  }
31244
31764
  return {
31245
31765
  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"
31766
+ error: (() => {
31767
+ const samePurpose = queue.filter((receipt) => receipt.purpose === purpose);
31768
+ const base = "permission_denied: no matching native question receipt for this purpose";
31769
+ if (samePurpose.length) {
31770
+ const closest = samePurpose[samePurpose.length - 1];
31771
+ const reasons = [];
31772
+ if (expected.selectedLabel !== undefined && normalizeReceiptLabel(closest.selectedLabel) !== normalizeReceiptLabel(expected.selectedLabel))
31773
+ reasons.push(`selected answer was "${closest.selectedLabel}"`);
31774
+ if (expected.selectedDescription !== undefined && closest.selectedDescription !== expected.selectedDescription)
31775
+ reasons.push("approved option description did not match");
31776
+ if (expected.decisionPurpose !== undefined && closest.decisionPurpose !== expected.decisionPurpose)
31777
+ reasons.push(`purpose was "${closest.decisionPurpose}"`);
31778
+ if (expected.contentDigest !== undefined && closest.contentDigest !== expected.contentDigest)
31779
+ reasons.push("question or approved content differed from the asked receipt");
31780
+ if (reasons.length)
31781
+ return `${base} (closest ${purpose} receipt: ${reasons.join("; ")})`;
31782
+ }
31783
+ 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`;
31784
+ })()
31247
31785
  };
31248
31786
  }
31249
31787
  verify(observation, sessionID, purpose) {
@@ -31345,6 +31883,13 @@ var opencodeCapabilities = () => [
31345
31883
  reason: "nested native task launches are denied and observed child sessions are parent-bound",
31346
31884
  refs: [hostRef("task")]
31347
31885
  },
31886
+ {
31887
+ name: "fresh-context-review",
31888
+ surface: "task",
31889
+ assurance: "agent_guided",
31890
+ reason: "independent review runs as a native direct-child session; evidence evaluation enforces creator and duplicate-reviewer exclusion",
31891
+ refs: [hostRef("task")]
31892
+ },
31348
31893
  {
31349
31894
  name: "known_product_writes",
31350
31895
  surface: "write/edit/bash",
@@ -31363,7 +31908,8 @@ var opencodeCapabilities = () => [
31363
31908
  var nativeAuthority = (receipts, actor, reconciliationTokens = new WeakSet) => ({
31364
31909
  verifyDecision: ({ observation, expected, caller }) => {
31365
31910
  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)
31911
+ const expectedApproved = typeof expected.binding.displayed === "string" ? expected.binding.displayed : expected.binding.approvedContent;
31912
+ 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
31913
  return failure("permission_denied", "native decision receipt is not bound to this session");
31368
31914
  return success(null, null, {
31369
31915
  kind: "host_observed",
@@ -31438,8 +31984,34 @@ var nativeDispatchFor = (directChildren, actor, generation) => ({
31438
31984
  var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
31439
31985
  const store = new TaskStore(root);
31440
31986
  const selected = approvedExternalAction(store, "opencode", actor, operation);
31441
- if (!selected.ok)
31987
+ if (!selected.ok) {
31988
+ const plan = planCommitBinding(store, "opencode", actor, operation);
31989
+ if (plan) {
31990
+ const actionRef = externalActionRef("opencode", actor, operation);
31991
+ return {
31992
+ taskId: plan.taskId,
31993
+ decisionId: plan.decisionId,
31994
+ actionRef,
31995
+ expectedRevision: plan.expectedRevision,
31996
+ expectedWorkspaceRevision: plan.expectedWorkspaceRevision,
31997
+ binding: plan.binding,
31998
+ step: plan.step,
31999
+ refresh: () => {
32000
+ const task = store.readTask(plan.taskId);
32001
+ const freshWorkspace = store.readWorkspace();
32002
+ if (!task.ok || !freshWorkspace.ok || !freshWorkspace.data)
32003
+ throw new Error("external action state changed");
32004
+ return {
32005
+ expectedRevision: task.data.revision,
32006
+ expectedWorkspaceRevision: freshWorkspace.data.revision
32007
+ };
32008
+ },
32009
+ reserveObservation: nativeExternalActionObservation(actor, actionRef, "reserve"),
32010
+ settleObservation: (outcome, revisions) => nativeExternalActionObservation(actor, actionRef, outcome, actionRef.kind === "host" ? actionRef.handle : "external-action", revisions)
32011
+ };
32012
+ }
31442
32013
  return selected;
32014
+ }
31443
32015
  const actionRef = externalActionRef("opencode", actor, operation);
31444
32016
  return {
31445
32017
  taskId: selected.data.task.id,
@@ -31485,6 +32057,7 @@ var createWorkitTools = ({
31485
32057
  receipts = new NativeReceiptStore,
31486
32058
  directChildren = new Map
31487
32059
  } = {}) => {
32060
+ const actionProposals = new Map;
31488
32061
  const make = (family) => tool({
31489
32062
  description: `Workit ${family} operations backed by the shared task contract.`,
31490
32063
  args: operationShapeFor(family),
@@ -31518,6 +32091,15 @@ var createWorkitTools = ({
31518
32091
  let result;
31519
32092
  if (family === "decision" && args.action === "record") {
31520
32093
  const decision = parsed.data;
32094
+ if (decision.response === "stated") {
32095
+ result = core.observeDecision(parsed.data, undefined);
32096
+ return output2(result);
32097
+ }
32098
+ const budgetIssue = workitBindingQuestionIssue([
32099
+ decisionContent(decision.purpose, decision.binding.presented, decision.binding.approvedContent)
32100
+ ]);
32101
+ if (budgetIssue)
32102
+ return output2(failure("invalid_input", budgetIssue));
31521
32103
  const expectation = {
31522
32104
  selectedLabel: decision.response,
31523
32105
  decisionPurpose: decision.purpose,
@@ -31529,7 +32111,37 @@ var createWorkitTools = ({
31529
32111
  const observed = receipts.consume(context.sessionID, "decision", expectation);
31530
32112
  if (!observed.ok)
31531
32113
  return output2(failure("permission_denied", observed.error));
31532
- result = core.observeDecision(parsed.data, observed.observation);
32114
+ let recordInput = parsed.data;
32115
+ if (decision.purpose === "action" && decision.response === "approved") {
32116
+ const queue = actionProposals.get(context.sessionID) ?? [];
32117
+ const textMatches = queue.filter((pending) => pending.presented === decision.binding.presented && pending.approvedText === decision.binding.approvedContent);
32118
+ if (textMatches.length > 1 && new Set(textMatches.map((pending) => pending.descriptor)).size > 1)
32119
+ return output2(failure("invalid_input", "multiple action proposals match this approval; resolve the action again"));
32120
+ let bound = false;
32121
+ const pending = textMatches[0];
32122
+ if (pending) {
32123
+ const fresh = resolveExternalActionRequest(context.directory, pending.request);
32124
+ const freshDescriptor = fresh.ok ? externalActionDescriptor(pending.request.operation, fresh.data.descriptorPayload) : null;
32125
+ if (freshDescriptor === pending.descriptor) {
32126
+ actionProposals.set(context.sessionID, queue.filter((candidate) => candidate !== pending));
32127
+ recordInput = {
32128
+ ...recordInput,
32129
+ binding: {
32130
+ ...recordInput.binding,
32131
+ approvedContent: pending.descriptor,
32132
+ displayed: pending.approvedText
32133
+ }
32134
+ };
32135
+ bound = true;
32136
+ } else if (fresh.ok) {
32137
+ actionProposals.set(context.sessionID, queue.filter((candidate) => candidate !== pending));
32138
+ }
32139
+ }
32140
+ if (!bound && !isSelfAuthorizingActionContent(decision.binding.approvedContent)) {
32141
+ return output2(failure("invalid_input", textMatches.length > 0 ? "repository state changed since the proposal; resolve the action again for a fresh proposal" : "no matching action proposal; resolve the action through the action tool first"));
32142
+ }
32143
+ }
32144
+ result = core.observeDecision(recordInput, observed.observation);
31533
32145
  } else {
31534
32146
  const run = core[family];
31535
32147
  result = run.call(core, parsed.data);
@@ -31565,9 +32177,17 @@ var createWorkitTools = ({
31565
32177
  if (earlySession !== null && sessionParent(earlySession) !== undefined)
31566
32178
  return output2(failure("permission_denied", "child sessions cannot run external actions"));
31567
32179
  }
31568
- const resolved = resolveExternalActionRequest(context.directory, parsed.data);
32180
+ let resolved = resolveExternalActionRequest(context.directory, parsed.data);
31569
32181
  if (!resolved.ok)
31570
32182
  return output2(resolved);
32183
+ if (resolved.ok && resolved.data.request.operation === "git.branch_setup" && resolved.data.descriptorPayload.resolved?.dirty === true && resolved.data.descriptorPayload.stash !== "yes") {
32184
+ const upgraded = resolveExternalActionRequest(context.directory, {
32185
+ ...parsed.data,
32186
+ payload: { ...parsed.data.payload, stash: "yes" }
32187
+ });
32188
+ if (upgraded.ok)
32189
+ resolved = upgraded;
32190
+ }
31571
32191
  if (resolved.data.request.operation === "context.read")
31572
32192
  return output2(await executeResolvedExternalAction(resolved.data, context.directory));
31573
32193
  if (!client)
@@ -31630,6 +32250,68 @@ var createWorkitTools = ({
31630
32250
  return output2(failure("external_outcome_unknown", "previous external action outcome is unknown"));
31631
32251
  }
31632
32252
  const descriptor = externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload);
32253
+ 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);
32254
+ if (!drift.ok)
32255
+ return output2(drift);
32256
+ const localOperation = [
32257
+ "git.branch_setup",
32258
+ "git.commit",
32259
+ "git.push",
32260
+ "hosting.pull_request",
32261
+ "changelog.apply"
32262
+ ].includes(resolved.data.request.operation);
32263
+ if (localOperation) {
32264
+ const writer = assertLocalExternalActionWriter(context.directory, {
32265
+ host: "opencode",
32266
+ actor: context.sessionID
32267
+ });
32268
+ if (!writer.ok)
32269
+ return output2(failure("needs_input", "writer ownership is required before this action", {
32270
+ outcome: "not_started",
32271
+ operation: resolved.data.request.operation,
32272
+ guidance: "Acquire checkout writer ownership first (writer.acquire) and retry the action."
32273
+ }));
32274
+ }
32275
+ const selected = approvedExternalAction(store, "opencode", context.sessionID, descriptor);
32276
+ let planAuthorized = false;
32277
+ if (!selected.ok) {
32278
+ const commitMessage = resolved.data.request.operation === "git.commit" ? resolved.data.request.payload.message : undefined;
32279
+ const currentBranch = resolved.data.descriptorPayload.resolved?.branch;
32280
+ if (typeof commitMessage === "string" && commitMessage) {
32281
+ const plan = approvedPlanCommit(store, "opencode", context.sessionID, commitMessage);
32282
+ planAuthorized = plan.ok && plan.data.branch === currentBranch;
32283
+ }
32284
+ }
32285
+ if (!selected.ok && !planAuthorized) {
32286
+ if (selected.error.startsWith("no approved action")) {
32287
+ const proposal = actionProposalQuestion(resolved.data.request, resolved.data.descriptorPayload);
32288
+ const pending = actionProposals.get(context.sessionID) ?? [];
32289
+ const existing = pending.find((candidate) => candidate.descriptor === descriptor);
32290
+ const open = existing ?? {
32291
+ descriptor,
32292
+ presented: proposal.presented,
32293
+ approvedText: proposal.approvedText,
32294
+ request: resolved.data.request
32295
+ };
32296
+ if (!existing) {
32297
+ pending.push(open);
32298
+ if (pending.length > 8)
32299
+ pending.shift();
32300
+ actionProposals.set(context.sessionID, pending);
32301
+ }
32302
+ return output2(failure("needs_input", proposal.presented, {
32303
+ outcome: "not_started",
32304
+ operation: resolved.data.request.operation,
32305
+ proposal: {
32306
+ presented: proposal.presented,
32307
+ approvedContent: proposal.approvedText,
32308
+ descriptorDigest: sha256(descriptor)
32309
+ },
32310
+ 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."
32311
+ }));
32312
+ }
32313
+ return output2(selected);
32314
+ }
31633
32315
  const result = await nativeExternalActionRunner(context.directory, context.sessionID, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, context.directory, step, {
31634
32316
  host: "opencode",
31635
32317
  actor: context.sessionID
@@ -31729,26 +32411,14 @@ var loadProvenance = (logger, pkgUrl) => {
31729
32411
  };
31730
32412
 
31731
32413
  // packages/workit-opencode/src/plugin.ts
31732
- var root = fileURLToPath2(new URL("../assets/", import.meta.url));
32414
+ var root = fileURLToPath3(new URL("../assets/", import.meta.url));
31733
32415
  var skillsPath = path25.join(root, "skills");
31734
32416
  var logger = createLogger({
31735
32417
  appLog: () => {
31736
32418
  return;
31737
32419
  }
31738
32420
  });
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)));
32421
+ var sourceMarker = markSourcesLoaded(pluginSourceFiles);
31752
32422
  var staleSourcesWarned = false;
31753
32423
  var warnStaleSources = () => {
31754
32424
  if (staleSourcesWarned)
@@ -32062,11 +32732,17 @@ var plugin = async ({ client, directory }) => {
32062
32732
  warnStaleSources();
32063
32733
  if (input.tool === "bash") {
32064
32734
  const command = output?.args?.command;
32065
- const route = typeof command === "string" ? shellRouteIntent(command) : null;
32735
+ const route = typeof command === "string" ? shouldDenyShellRoute(directory, command) : null;
32066
32736
  if (route)
32067
32737
  throw new Error(`recovery_required: direct branch or PR creation bypasses the Workit route; ${route.guidance}`);
32068
32738
  return;
32069
32739
  }
32740
+ if (input.tool === "question") {
32741
+ const issue = workitBindingQuestionIssue(output?.args?.questions);
32742
+ if (issue)
32743
+ throw new Error(issue);
32744
+ return;
32745
+ }
32070
32746
  if (input.tool === "task") {
32071
32747
  const listed = new TaskStore(directory).listTasks();
32072
32748
  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 +32762,7 @@ var plugin = async ({ client, directory }) => {
32086
32762
  config: async (config) => {
32087
32763
  const mutable = config;
32088
32764
  const paths = [...mutable.skills?.paths ?? []];
32089
- if (existsSync12(skillsPath) && !paths.includes(skillsPath))
32765
+ if (existsSync13(skillsPath) && !paths.includes(skillsPath))
32090
32766
  paths.push(skillsPath);
32091
32767
  mutable.skills ??= {};
32092
32768
  mutable.skills.paths = paths;