@brainervirus/workit-pi 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/workit.js CHANGED
@@ -3,8 +3,8 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
3
3
 
4
4
  // packages/workit-core/src/core.ts
5
5
  import { spawnSync as spawnSync2 } from "node:child_process";
6
- import { existsSync as existsSync3, realpathSync as realpathSync5 } from "node:fs";
7
- import path6 from "node:path";
6
+ import { existsSync as existsSync4, realpathSync as realpathSync5 } from "node:fs";
7
+ import path7 from "node:path";
8
8
 
9
9
  // packages/workit-core/src/core/task-contract.ts
10
10
  import { createHash, randomUUID } from "node:crypto";
@@ -7796,10 +7796,12 @@ var decisionSchema = object({
7796
7796
  scope: scopeSchema,
7797
7797
  presented: text,
7798
7798
  approvedContent: text,
7799
- contentRefs: array(refSchema)
7799
+ displayed: text.optional(),
7800
+ contentRefs: array(refSchema),
7801
+ statedChoice: object({ ref: text, text }).strict().optional()
7800
7802
  }).strict(),
7801
7803
  digest,
7802
- response: _enum(["approved", "rejected"]),
7804
+ response: _enum(["approved", "rejected", "stated"]),
7803
7805
  requirementIds: array(digest),
7804
7806
  revoked: object({ at: utc, reason: text }).strict().nullable(),
7805
7807
  consumption: object({
@@ -7808,6 +7810,27 @@ var decisionSchema = object({
7808
7810
  actionRef: refSchema
7809
7811
  }).strict().nullable()
7810
7812
  }).strict();
7813
+ var BINDING_QUESTION_BUDGET = 300;
7814
+ var bindingQuestionHeader = /^Workit decision: (design|action|limitation|preference)$/;
7815
+ var workitBindingQuestionIssue = (questions) => {
7816
+ if (!Array.isArray(questions) || questions.length !== 1)
7817
+ return null;
7818
+ const question = questions[0];
7819
+ if (!question || typeof question !== "object")
7820
+ return null;
7821
+ if (typeof question.header !== "string" || !bindingQuestionHeader.test(question.header.trim()))
7822
+ return null;
7823
+ 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.`;
7824
+ if (typeof question.question === "string" && question.question.length > BINDING_QUESTION_BUDGET)
7825
+ return over("the question text", question.question);
7826
+ if (Array.isArray(question.options))
7827
+ for (const option of question.options) {
7828
+ const description = option.description;
7829
+ if (typeof description === "string" && description.length > BINDING_QUESTION_BUDGET)
7830
+ return over("an option description", description);
7831
+ }
7832
+ return null;
7833
+ };
7811
7834
  var findingSchema = object({
7812
7835
  claim: text,
7813
7836
  consequence: text,
@@ -7874,6 +7897,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
7874
7897
  path: ["decisionId"]
7875
7898
  });
7876
7899
  });
7900
+ var runtimeSchema = object({
7901
+ createdWith: text.nullable(),
7902
+ updatedWith: text
7903
+ }).strict();
7877
7904
  var taskRecordSchema = object({
7878
7905
  schemaVersion: literal(1),
7879
7906
  id,
@@ -7887,6 +7914,8 @@ var taskRecordSchema = object({
7887
7914
  status: _enum(["active", "paused", "closed"]),
7888
7915
  closure: closureSchema.nullable(),
7889
7916
  progress: progressSchema,
7917
+ pauseReason: text.nullable().optional(),
7918
+ runtime: runtimeSchema.optional(),
7890
7919
  assessments: array(entrySchema(assessmentSchema)),
7891
7920
  policy: policySchema.nullable(),
7892
7921
  policyChanges: array(policyChangeSchema),
@@ -7902,6 +7931,7 @@ var workspaceRecordSchema = object({
7902
7931
  id,
7903
7932
  revision,
7904
7933
  root: nonEmpty,
7934
+ runtime: runtimeSchema.optional(),
7905
7935
  writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
7906
7936
  }).strict();
7907
7937
  var capabilitySchema = object({
@@ -7929,6 +7959,9 @@ var taskSummarySchema = object({
7929
7959
  id,
7930
7960
  revision,
7931
7961
  workspaceRevision: revision,
7962
+ createdAt: utc,
7963
+ updatedAt: utc,
7964
+ runtime: runtimeSchema.nullable(),
7932
7965
  objective: text,
7933
7966
  status: _enum(["active", "paused", "closed"]),
7934
7967
  closure: closureSchema.nullable(),
@@ -7999,7 +8032,7 @@ var taskOperations = {
7999
8032
  expectedWorkspaceRevision: revision.optional(),
8000
8033
  outcome: outcomeSchema,
8001
8034
  summary: text,
8002
- decisionIds: array(id)
8035
+ decisionIds: array(id).optional()
8003
8036
  })
8004
8037
  };
8005
8038
  var policyOperations = {
@@ -8091,14 +8124,15 @@ var writerOperations = {
8091
8124
  ...taskId,
8092
8125
  expectedRevision: revision.optional(),
8093
8126
  expectedWorkspaceRevision: revision.optional(),
8094
- workerId: nullableId.optional()
8127
+ workerId: nullableId.optional(),
8128
+ reason: text.optional()
8095
8129
  }),
8096
8130
  release: operation({
8097
8131
  action: literal("release"),
8098
8132
  ...taskId,
8099
8133
  expectedRevision: revision.optional(),
8100
8134
  expectedWorkspaceRevision: revision.optional(),
8101
- reason: text
8135
+ reason: text.optional()
8102
8136
  })
8103
8137
  };
8104
8138
  var stateOperations = {
@@ -8305,7 +8339,26 @@ function requirementId(input) {
8305
8339
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
8306
8340
  import * as fs4 from "node:fs";
8307
8341
  import { hostname } from "node:os";
8308
- import path3 from "node:path";
8342
+ import path4 from "node:path";
8343
+
8344
+ // packages/workit-core/src/core/package-root.ts
8345
+ import { existsSync } from "node:fs";
8346
+ import path from "node:path";
8347
+ import { fileURLToPath } from "node:url";
8348
+ var packageRoot = () => {
8349
+ let dir = path.dirname(fileURLToPath(import.meta.url));
8350
+ while (true) {
8351
+ const parent = path.dirname(dir);
8352
+ if (existsSync(path.join(dir, "package.json")) || parent === dir)
8353
+ return dir;
8354
+ dir = parent;
8355
+ }
8356
+ };
8357
+ var assetRoot = () => {
8358
+ const root = packageRoot();
8359
+ const assets = path.join(root, "assets");
8360
+ return existsSync(assets) ? assets : root;
8361
+ };
8309
8362
 
8310
8363
  // node_modules/@openclaw/fs-safe/dist/errors.js
8311
8364
  var OPERATIONAL_CODES = new Set([
@@ -8356,7 +8409,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
8356
8409
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
8357
8410
  import { randomBytes } from "node:crypto";
8358
8411
  import fsSync from "node:fs";
8359
- import path from "node:path";
8412
+ import path2 from "node:path";
8360
8413
 
8361
8414
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
8362
8415
  import fs from "node:fs";
@@ -8446,13 +8499,13 @@ ${ownershipToken}
8446
8499
  return { raw, ownershipToken };
8447
8500
  }
8448
8501
  function relativeSidecarLockPath(lockRoot, lockPath) {
8449
- const resolved = path.resolve(lockPath);
8450
- const lexicalRelative = path.relative(lockRoot.rootDir, resolved);
8451
- const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path.sep}`) && !path.isAbsolute(lexicalRelative) ? lexicalRelative : path.relative(lockRoot.rootReal, resolved);
8452
- if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
8502
+ const resolved = path2.resolve(lockPath);
8503
+ const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
8504
+ const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
8505
+ if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
8453
8506
  throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
8454
8507
  }
8455
- return relative.split(path.sep).join(path.posix.sep);
8508
+ return relative.split(path2.sep).join(path2.posix.sep);
8456
8509
  }
8457
8510
  function parseSidecarLockPayload(raw, parser) {
8458
8511
  if (parser) {
@@ -8599,7 +8652,7 @@ function getFsSafeLockConfig() {
8599
8652
 
8600
8653
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
8601
8654
  import fs3 from "node:fs";
8602
- import path2 from "node:path";
8655
+ import path3 from "node:path";
8603
8656
 
8604
8657
  // node_modules/@openclaw/fs-safe/dist/timing.js
8605
8658
  function sleepSync(ms) {
@@ -8690,26 +8743,26 @@ function canonicalLockParentSync(parent) {
8690
8743
  return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
8691
8744
  }
8692
8745
  function normalizeTargetPath(targetPath) {
8693
- const resolved = path2.resolve(targetPath);
8694
- fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
8746
+ const resolved = path3.resolve(targetPath);
8747
+ fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
8695
8748
  try {
8696
- return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
8749
+ return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
8697
8750
  } catch {
8698
8751
  return resolved;
8699
8752
  }
8700
8753
  }
8701
8754
  function boundedLockPath(lockPath, lockRoot) {
8702
- const resolved = path2.resolve(lockPath);
8755
+ const resolved = path3.resolve(lockPath);
8703
8756
  if (!lockRoot)
8704
8757
  return resolved;
8705
8758
  relativeSidecarLockPath(lockRoot, resolved);
8706
- const parent = path2.dirname(resolved);
8759
+ const parent = path3.dirname(resolved);
8707
8760
  const parentReal = canonicalLockParentSync(parent);
8708
- const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
8709
- if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
8761
+ const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
8762
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
8710
8763
  throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
8711
8764
  }
8712
- return path2.join(parentReal, path2.basename(resolved));
8765
+ return path3.join(parentReal, path3.basename(resolved));
8713
8766
  }
8714
8767
  function defaultShouldReclaim(snapshot, staleMs, nowMs) {
8715
8768
  const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
@@ -8922,6 +8975,33 @@ var metadataLockSchema = object({
8922
8975
  nonce: string2().min(1)
8923
8976
  }).strict();
8924
8977
  var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
8978
+ var cachedRuntimeVersion = null;
8979
+ var runtimeVersion = () => {
8980
+ if (cachedRuntimeVersion === null) {
8981
+ try {
8982
+ const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
8983
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
8984
+ } catch {
8985
+ cachedRuntimeVersion = "0.0.0";
8986
+ }
8987
+ }
8988
+ return cachedRuntimeVersion;
8989
+ };
8990
+ var stampNew = (record) => ({
8991
+ createdWith: record.runtime?.createdWith ?? null,
8992
+ updatedWith: runtimeVersion()
8993
+ });
8994
+ var isNewerVersion = (candidate, current) => {
8995
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
8996
+ const [next, now] = [parts(candidate), parts(current)];
8997
+ for (let index = 0;index < 3; index += 1) {
8998
+ const left = next[index] ?? 0;
8999
+ const right = now[index] ?? 0;
9000
+ if (left !== right)
9001
+ return left > right;
9002
+ }
9003
+ return false;
9004
+ };
8925
9005
  var jsonBytes = (value) => `${canonicalJson(value)}
8926
9006
  `;
8927
9007
  var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
@@ -8948,12 +9028,12 @@ var sameMetadataLock = (left, right) => {
8948
9028
  class TaskStore {
8949
9029
  root;
8950
9030
  constructor(root) {
8951
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path3.resolve(root);
9031
+ this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
8952
9032
  }
8953
9033
  readTask(taskId) {
8954
9034
  if (!validId(taskId))
8955
9035
  return failure("invalid_input", "task ID is invalid", { taskId });
8956
- const result = this.readRecord(path3.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
9036
+ const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
8957
9037
  if (!result.exists)
8958
9038
  return failure("not_found", "task not found", { taskId });
8959
9039
  if (result.result.ok && result.result.data.id !== taskId)
@@ -8987,7 +9067,7 @@ class TaskStore {
8987
9067
  });
8988
9068
  const tasks = [];
8989
9069
  for (const name of names.sort()) {
8990
- const item = this.readRecord(path3.join(this.tasksDir, name), taskRecordSchema);
9070
+ const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
8991
9071
  if (!item.exists)
8992
9072
  continue;
8993
9073
  if (!item.result.ok)
@@ -9021,7 +9101,7 @@ class TaskStore {
9021
9101
  if (!current.ok)
9022
9102
  return current;
9023
9103
  if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
9024
- return failure("revision_conflict", "workspace revision does not match", {
9104
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
9025
9105
  expectedWorkspaceRevision: value.expectedWorkspaceRevision,
9026
9106
  actualWorkspaceRevision: current.data?.revision ?? null
9027
9107
  });
@@ -9033,11 +9113,17 @@ class TaskStore {
9033
9113
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
9034
9114
  if (current.data && !previousWorkspaceBytes)
9035
9115
  return failure("storage_error", "workspace snapshot disappeared during creation");
9036
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
9116
+ const workspace = current.data ? {
9117
+ ...current.data,
9118
+ revision: newRevision(),
9119
+ root: this.root,
9120
+ runtime: stampNew(current.data)
9121
+ } : {
9037
9122
  schemaVersion: SCHEMA_VERSION,
9038
9123
  id: newId(),
9039
9124
  revision: newRevision(),
9040
9125
  root: this.root,
9126
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
9041
9127
  writer: null
9042
9128
  };
9043
9129
  const timestamp = value.now ?? now();
@@ -9059,6 +9145,7 @@ class TaskStore {
9059
9145
  status: "active",
9060
9146
  closure: null,
9061
9147
  progress: { summary: "", nextAction: null, blockers: [] },
9148
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
9062
9149
  assessments: [],
9063
9150
  policy: null,
9064
9151
  policyChanges: [],
@@ -9089,7 +9176,7 @@ class TaskStore {
9089
9176
  if (!current.ok)
9090
9177
  return current;
9091
9178
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
9092
- return failure("revision_conflict", "workspace revision does not match", {
9179
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
9093
9180
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
9094
9181
  actualWorkspaceRevision: current.data?.revision ?? null
9095
9182
  });
@@ -9097,11 +9184,17 @@ class TaskStore {
9097
9184
  if (current.data && !previousWorkspaceBytes)
9098
9185
  return failure("storage_error", "workspace snapshot disappeared during import");
9099
9186
  const timestamp = input.now ?? now();
9100
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
9187
+ const workspace = current.data ? {
9188
+ ...current.data,
9189
+ revision: newRevision(),
9190
+ root: this.root,
9191
+ runtime: stampNew(current.data)
9192
+ } : {
9101
9193
  schemaVersion: SCHEMA_VERSION,
9102
9194
  id: input.workspaceId ?? newId(),
9103
9195
  revision: newRevision(),
9104
9196
  root: this.root,
9197
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
9105
9198
  writer: null
9106
9199
  };
9107
9200
  const task = {
@@ -9109,7 +9202,11 @@ class TaskStore {
9109
9202
  workspaceId: workspace.id,
9110
9203
  revision: newRevision(),
9111
9204
  createdAt: timestamp,
9112
- updatedAt: timestamp
9205
+ updatedAt: timestamp,
9206
+ runtime: {
9207
+ createdWith: input.task.runtime?.createdWith ?? null,
9208
+ updatedWith: runtimeVersion()
9209
+ }
9113
9210
  };
9114
9211
  const validTask = taskRecordSchema.safeParse(task);
9115
9212
  if (!validTask.success)
@@ -9158,7 +9255,8 @@ class TaskStore {
9158
9255
  workspaceId: current.data.workspaceId,
9159
9256
  createdAt: current.data.createdAt,
9160
9257
  revision: context.revision,
9161
- updatedAt: context.now
9258
+ updatedAt: context.now,
9259
+ runtime: stampNew(current.data)
9162
9260
  };
9163
9261
  const valid = taskRecordSchema.safeParse(record);
9164
9262
  if (!valid.success)
@@ -9192,7 +9290,8 @@ class TaskStore {
9192
9290
  ...changed.data,
9193
9291
  id: current.data.id,
9194
9292
  root: this.root,
9195
- revision: context.revision
9293
+ revision: context.revision,
9294
+ runtime: stampNew(current.data)
9196
9295
  };
9197
9296
  const valid = workspaceRecordSchema.safeParse(record);
9198
9297
  if (!valid.success)
@@ -9235,7 +9334,8 @@ class TaskStore {
9235
9334
  ...changedWorkspace.data,
9236
9335
  id: workspace.data.id,
9237
9336
  root: this.root,
9238
- revision: workspaceContext.revision
9337
+ revision: workspaceContext.revision,
9338
+ runtime: stampNew(workspace.data)
9239
9339
  });
9240
9340
  if (!nextWorkspace.success)
9241
9341
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -9260,7 +9360,8 @@ class TaskStore {
9260
9360
  workspaceId: task.data.workspaceId,
9261
9361
  createdAt: task.data.createdAt,
9262
9362
  revision: taskContext.revision,
9263
- updatedAt: taskContext.now
9363
+ updatedAt: taskContext.now,
9364
+ runtime: stampNew(task.data)
9264
9365
  });
9265
9366
  if (!nextTask.success) {
9266
9367
  this.markUncertain(nextWorkspace.data);
@@ -9287,7 +9388,7 @@ class TaskStore {
9287
9388
  if (match)
9288
9389
  candidates.push({
9289
9390
  target: match[1],
9290
- path: path3.join(this.recoveryDir, name),
9391
+ path: path4.join(this.recoveryDir, name),
9291
9392
  digest: match[3]
9292
9393
  });
9293
9394
  }
@@ -9480,7 +9581,8 @@ class TaskStore {
9480
9581
  const value = {
9481
9582
  ...workspace,
9482
9583
  writer: { ...workspace.writer, state: "uncertain" },
9483
- revision: newRevision()
9584
+ revision: newRevision(),
9585
+ runtime: stampNew(workspace)
9484
9586
  };
9485
9587
  this.replaceSnapshot(this.workspacePath, value, workspace);
9486
9588
  }
@@ -9558,7 +9660,7 @@ class TaskStore {
9558
9660
  }
9559
9661
  fs4.renameSync(temporary, file);
9560
9662
  temporary = undefined;
9561
- this.fsyncDirectory(path3.dirname(file));
9663
+ this.fsyncDirectory(path4.dirname(file));
9562
9664
  return success(null, null, value);
9563
9665
  } catch (error) {
9564
9666
  return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
@@ -9572,9 +9674,9 @@ class TaskStore {
9572
9674
  }
9573
9675
  }
9574
9676
  saveRecovery(file, bytes) {
9575
- const target = path3.basename(file) === "workspace.json" ? "workspace" : "task";
9576
- const id = target === "task" ? path3.basename(file, ".json") : "workspace";
9577
- const destination = path3.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
9677
+ const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
9678
+ const id = target === "task" ? path4.basename(file, ".json") : "workspace";
9679
+ const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
9578
9680
  if (fs4.existsSync(destination)) {
9579
9681
  if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
9580
9682
  return;
@@ -9652,7 +9754,7 @@ class TaskStore {
9652
9754
  for (const name of fs4.readdirSync(this.tasksDir)) {
9653
9755
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
9654
9756
  return null;
9655
- const parsed = this.parseBytes(fs4.readFileSync(path3.join(this.tasksDir, name)), taskRecordSchema);
9757
+ const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
9656
9758
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
9657
9759
  return null;
9658
9760
  ids.add(parsed.data.workspaceId);
@@ -9694,7 +9796,12 @@ class TaskStore {
9694
9796
  if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
9695
9797
  return failure("unsupported_version", "unsupported snapshot schema version");
9696
9798
  const parsed = schema.safeParse(value);
9697
- return parsed.success ? success(null, null, parsed.data) : failure("recovery_required", "snapshot does not satisfy its schema");
9799
+ if (parsed.success)
9800
+ return success(null, null, parsed.data);
9801
+ const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
9802
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
9803
+ return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
9804
+ return failure("recovery_required", "snapshot does not satisfy its schema");
9698
9805
  }
9699
9806
  processStart(pid) {
9700
9807
  try {
@@ -9704,43 +9811,98 @@ class TaskStore {
9704
9811
  }
9705
9812
  }
9706
9813
  conflict(expected, actual) {
9707
- return failure("revision_conflict", "snapshot revision does not match", {
9814
+ return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
9708
9815
  expectedRevision: expected,
9709
9816
  actualRevision: actual
9710
9817
  });
9711
9818
  }
9712
9819
  taskPath(taskId) {
9713
- return path3.join(this.tasksDir, `${taskId}.json`);
9820
+ return path4.join(this.tasksDir, `${taskId}.json`);
9714
9821
  }
9715
9822
  get workitDir() {
9716
- return path3.join(this.root, ".workit");
9823
+ return path4.join(this.root, ".workit");
9717
9824
  }
9718
9825
  get tasksDir() {
9719
- return path3.join(this.workitDir, "tasks");
9826
+ return path4.join(this.workitDir, "tasks");
9720
9827
  }
9721
9828
  get recoveryDir() {
9722
- return path3.join(this.workitDir, "recovery");
9829
+ return path4.join(this.workitDir, "recovery");
9723
9830
  }
9724
9831
  get workspacePath() {
9725
- return path3.join(this.workitDir, "workspace.json");
9832
+ return path4.join(this.workitDir, "workspace.json");
9726
9833
  }
9727
9834
  get lockPath() {
9728
- return path3.join(this.workitDir, "metadata.lock");
9835
+ return path4.join(this.workitDir, "metadata.lock");
9729
9836
  }
9730
9837
  get gitignorePath() {
9731
- return path3.join(this.workitDir, ".gitignore");
9838
+ return path4.join(this.workitDir, ".gitignore");
9732
9839
  }
9733
9840
  }
9841
+ // packages/workit-core/src/core/workers.ts
9842
+ var same = (left, right) => {
9843
+ try {
9844
+ return canonicalJson(left) === canonicalJson(right);
9845
+ } catch {
9846
+ return false;
9847
+ }
9848
+ };
9849
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
9850
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
9851
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
9852
+ var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
9853
+ const code = char.charCodeAt(0);
9854
+ return code < 32 || code === 127;
9855
+ }) && !value.split(/[\\/]/).includes("..");
9856
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
9857
+ var workerIdOf = (value) => value.workerId ?? null;
9858
+ function assertProductWriteAllowed(input) {
9859
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
9860
+ return failure("invalid_input", "invalid product write path");
9861
+ if (!input.store)
9862
+ return failure("permission_denied", "write authorization requires core state");
9863
+ const task = input.store.readTask(input.task.id);
9864
+ if (!task.ok)
9865
+ return task;
9866
+ const workspace = input.store.readWorkspace();
9867
+ if (!workspace.ok)
9868
+ return workspace;
9869
+ if (!workspace.data)
9870
+ return failure("not_found", "workspace not found");
9871
+ const workspaceRecord = workspace.data;
9872
+ if (task.data.status !== "active")
9873
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
9874
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
9875
+ return failure("recovery_required", "task and workspace bindings are invalid");
9876
+ const writer = workspaceRecord.writer;
9877
+ if (!writer)
9878
+ return failure("permission_denied", "checkout has no writer owner");
9879
+ if (writer.state === "uncertain")
9880
+ return failure("recovery_required", "checkout writer requires recovery", {
9881
+ owner: writer.owner
9882
+ });
9883
+ const caller = callerValue(input.caller);
9884
+ const workerId = workerIdOf(input.caller);
9885
+ const owner = writer.owner;
9886
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
9887
+ return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
9888
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
9889
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
9890
+ return failure("permission_denied", "only implementers can own product writes");
9891
+ 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))))
9892
+ return failure("recovery_required", "worker session is not an active writer");
9893
+ return success(null, workspaceRecord.revision, owner);
9894
+ }
9895
+
9734
9896
  // packages/workit-core/src/core/task-evaluation.ts
9735
9897
  import { createHash as createHash4 } from "node:crypto";
9736
9898
  import * as fs6 from "node:fs";
9737
9899
  import { spawnSync } from "node:child_process";
9738
- import path5 from "node:path";
9900
+ import path6 from "node:path";
9739
9901
 
9740
9902
  // packages/workit-core/src/core/authority.ts
9741
9903
  import { createHash as createHash3 } from "node:crypto";
9742
9904
  import * as fs5 from "node:fs";
9743
- import path4 from "node:path";
9905
+ import path5 from "node:path";
9744
9906
  var verifiedAuthorities = new WeakMap;
9745
9907
  var trustedAuthority = (kind, provenance, expected, binding, caller) => {
9746
9908
  const token = {};
@@ -9867,20 +10029,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
9867
10029
  continue;
9868
10030
  if (!reference.digest)
9869
10031
  return failure("invalid_input", "document references require a byte digest");
9870
- const target = path4.resolve(root, reference.path);
9871
- if (target !== root && !target.startsWith(`${root}${path4.sep}`))
9872
- return failure("invalid_input", "document reference escapes checkout");
10032
+ const target = path5.resolve(root, reference.path);
10033
+ if (target !== root && !target.startsWith(`${root}${path5.sep}`))
10034
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
10035
+ fields: [
10036
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
10037
+ ]
10038
+ });
9873
10039
  try {
9874
- const relative = path4.relative(root, target);
10040
+ const relative = path5.relative(root, target);
9875
10041
  let current = root;
9876
- for (const segment of relative.split(path4.sep)) {
10042
+ for (const segment of relative.split(path5.sep)) {
9877
10043
  if (!segment)
9878
10044
  continue;
9879
- current = path4.join(current, segment);
10045
+ current = path5.join(current, segment);
9880
10046
  const stat = fs5.lstatSync(current);
9881
10047
  const real = fs5.realpathSync(current);
9882
- if (real !== root && !real.startsWith(`${root}${path4.sep}`))
9883
- return failure("invalid_input", "document reference escapes checkout");
10048
+ if (real !== root && !real.startsWith(`${root}${path5.sep}`))
10049
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
10050
+ fields: [
10051
+ {
10052
+ path: "contentRefs.path",
10053
+ reason: `${reference.path} resolves outside the checkout`
10054
+ }
10055
+ ]
10056
+ });
9884
10057
  if (stat.isSymbolicLink())
9885
10058
  return failure("invalid_input", "document reference uses a symlink");
9886
10059
  if (current === target && !stat.isFile())
@@ -10227,14 +10400,14 @@ var verifyDecisionContent = verifyContentRefs;
10227
10400
 
10228
10401
  // packages/workit-core/src/core/task-evaluation.ts
10229
10402
  var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
10230
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path5.sep}`);
10231
- var relative = (root, candidate) => path5.relative(root, candidate).split(path5.sep).join("/") || ".";
10403
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
10404
+ var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
10232
10405
  var canonicalPath = (value) => {
10233
10406
  if (typeof value !== "string" || !value)
10234
10407
  return null;
10235
10408
  if (value.includes("\\") || value.split("/").includes(".."))
10236
10409
  return null;
10237
- const normalized = path5.posix.normalize(value);
10410
+ const normalized = path6.posix.normalize(value);
10238
10411
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
10239
10412
  return null;
10240
10413
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -10282,14 +10455,14 @@ var gitPaths = (root) => {
10282
10455
  git: /not a git repository/i.test(stderr) ? false : true
10283
10456
  };
10284
10457
  }
10285
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")) : [];
10458
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
10286
10459
  const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
10287
10460
  if (stagedDeleted.status !== 0) {
10288
10461
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
10289
10462
  if (!/not a git repository/i.test(stderr))
10290
10463
  return { paths, uncertain: true, git: true };
10291
10464
  } else if (stagedDeleted.stdout) {
10292
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")));
10465
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
10293
10466
  }
10294
10467
  return { paths: [...new Set(paths)], uncertain: false, git: true };
10295
10468
  };
@@ -10304,7 +10477,7 @@ var walk = (root, directory, output) => {
10304
10477
  for (const name of names) {
10305
10478
  if (name === ".git" || name === ".workit")
10306
10479
  continue;
10307
- const target = path5.join(directory, name);
10480
+ const target = path6.join(directory, name);
10308
10481
  let stat;
10309
10482
  try {
10310
10483
  stat = fs6.lstatSync(target);
@@ -10322,17 +10495,17 @@ var walk = (root, directory, output) => {
10322
10495
  var scopeRoots = (root, scope) => {
10323
10496
  const roots = [];
10324
10497
  for (const item of scope.paths.length ? scope.paths : ["."]) {
10325
- const target = path5.resolve(root, item);
10498
+ const target = path6.resolve(root, item);
10326
10499
  if (!inside(root, target))
10327
- return failure("invalid_input", "candidate scope escapes checkout");
10500
+ 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` }] });
10328
10501
  let ancestor = target;
10329
10502
  while (!fs6.existsSync(ancestor) && ancestor !== root)
10330
- ancestor = path5.dirname(ancestor);
10503
+ ancestor = path6.dirname(ancestor);
10331
10504
  try {
10332
10505
  if (!inside(root, fs6.realpathSync(ancestor)))
10333
- return failure("invalid_input", "candidate scope escapes checkout");
10506
+ 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` }] });
10334
10507
  } catch {
10335
- return failure("invalid_input", "candidate scope cannot be inspected");
10508
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
10336
10509
  }
10337
10510
  roots.push(target);
10338
10511
  }
@@ -10347,7 +10520,7 @@ function captureCandidate(root, scope, environment = []) {
10347
10520
  }
10348
10521
  const normalizedScope = canonicalScope(scope);
10349
10522
  if (!normalizedScope)
10350
- return failure("invalid_input", "candidate scope is invalid");
10523
+ 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" }] });
10351
10524
  const roots = scopeRoots(checkout, normalizedScope);
10352
10525
  if (!roots.ok)
10353
10526
  return roots;
@@ -10379,7 +10552,7 @@ function captureCandidate(root, scope, environment = []) {
10379
10552
  for (const item of [...names].sort()) {
10380
10553
  if (!scopeMatches(item, normalizedScope))
10381
10554
  continue;
10382
- const target = path5.join(checkout, item);
10555
+ const target = path6.join(checkout, item);
10383
10556
  try {
10384
10557
  const stat = fs6.lstatSync(target);
10385
10558
  if (stat.isSymbolicLink()) {
@@ -10501,7 +10674,7 @@ function evaluateEvidence(task, candidate) {
10501
10674
  }
10502
10675
  var scopeCovers2 = scopeCovers;
10503
10676
  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")));
10504
- 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 }));
10677
+ 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 }));
10505
10678
  var evidenceMatchesRequirement = (kind, evidenceKind, dimension) => {
10506
10679
  if (kind !== "passed")
10507
10680
  return false;
@@ -10536,7 +10709,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10536
10709
  return true;
10537
10710
  const reviewSession = sessionFromRef(entry.data.reviewContext);
10538
10711
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
10539
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
10712
+ const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
10540
10713
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
10541
10714
  });
10542
10715
  if (passed.length) {
@@ -10577,7 +10750,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10577
10750
  status: "satisfied",
10578
10751
  evidenceIds: [],
10579
10752
  decisionIds: decisions.map((decision) => decision.id),
10580
- reason: "an applicable approved decision satisfies the requirement"
10753
+ reason: "an applicable approved or stated decision satisfies the requirement"
10581
10754
  };
10582
10755
  const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
10583
10756
  if (limitations.length)
@@ -10632,7 +10805,17 @@ function evaluateClosure(requestedOutcome, view) {
10632
10805
  const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
10633
10806
  if (requestedOutcome !== "stopped" && blocking.length)
10634
10807
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
10635
- requirementIds: blocking.map((item) => item.requirementId)
10808
+ requirementIds: blocking.map((item) => item.requirementId),
10809
+ requirements: blocking.map((item) => {
10810
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
10811
+ return {
10812
+ requirementId: item.requirementId,
10813
+ ruleId: requirement?.ruleId ?? "unknown",
10814
+ reason: item.reason,
10815
+ satisfaction: requirement?.satisfaction ?? "",
10816
+ dependentAction: requirement?.dependentAction ?? null
10817
+ };
10818
+ })
10636
10819
  });
10637
10820
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
10638
10821
  if (requestedOutcome === "verified" && accepted.length)
@@ -10663,7 +10846,7 @@ function evaluateClosure(requestedOutcome, view) {
10663
10846
  var METHODS = {
10664
10847
  "workit-challenge": { dimensions: ["challenge", "decisions"] },
10665
10848
  "workit-behavioral-tdd": { dimensions: ["testing"] },
10666
- "workit-review": { dimensions: ["review"] },
10849
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
10667
10850
  "workit-plan": { dimensions: ["artifacts", "continuity"] },
10668
10851
  "workit-implement": { dimensions: ["delegation"] },
10669
10852
  "workit-debug": { ruleIds: ["root-cause-investigation"] },
@@ -10725,7 +10908,18 @@ decision.record {binding with taskId/workspaceId from inspect plus presented and
10725
10908
  approvedContent} only through a native approval question asked receipt-shaped:
10726
10909
  header Workit decision: <purpose>, the same label repeated in the question
10727
10910
  text (some host UIs do not render headers), exactly two options approved and
10728
- rejected; ask once, never re-ask to mint a receipt. task.close {outcome,
10911
+ rejected; ask once, never re-ask to mint a receipt. Before asking, present the
10912
+ item in the conversation (durable artifacts: complete digest plus exact path;
10913
+ inline plans: the content) and keep the question to one short scoped sentence;
10914
+ never ask to approve something unseen. An answer with custom text is steering,
10915
+ not approval: adjust, re-present, then ask a new scoped question. After the user
10916
+ approves a spec or plan, execute the remaining tasks continuously with one
10917
+ atomic commit per task and do not ask whether to continue; stop only for a new
10918
+ product decision, a failed safety or verification gate, a conflicting edit, or
10919
+ missing authority. Record the plan's commit list once through the plan-scoped
10920
+ action approval (git.commit with plan_steps and plan_branch) so each listed
10921
+ commit executes without a new question; an unlisted message needs a fresh
10922
+ approval. task.close {outcome,
10729
10923
  summary, decisionIds}. A completed native subagent run stops its bound worker
10730
10924
  by itself; if a worker strands in cancelling with its run verifiably over,
10731
10925
  repeat worker.cancel {reason} to confirm the stop. Check and review evidence
@@ -10757,7 +10951,7 @@ function reconcileResume(view, observations = []) {
10757
10951
  return candidate;
10758
10952
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
10759
10953
  const blockers = [...view.task.progress.blockers];
10760
- if (view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
10954
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
10761
10955
  blockers.push({
10762
10956
  reason: "worker state requires reconciliation",
10763
10957
  dependentAction: "resume",
@@ -10889,7 +11083,25 @@ function shellRouteIntent(command) {
10889
11083
  }
10890
11084
  return null;
10891
11085
  }
11086
+ function hasLiveWorkitTask(dir) {
11087
+ try {
11088
+ if (typeof dir !== "string" || dir === "")
11089
+ return false;
11090
+ const listed = new TaskStore(dir).listTasks();
11091
+ if (!listed.ok)
11092
+ return false;
11093
+ return listed.data.some((task) => task.status === "active" || task.status === "paused");
11094
+ } catch {
11095
+ return false;
11096
+ }
11097
+ }
11098
+ function shouldDenyShellRoute(dir, command) {
11099
+ if (!hasLiveWorkitTask(dir))
11100
+ return null;
11101
+ return shellRouteIntent(command);
11102
+ }
10892
11103
  // packages/workit-core/src/core/external-action.ts
11104
+ import { execFileSync } from "node:child_process";
10893
11105
  var externalActionSchema = discriminatedUnion("operation", [
10894
11106
  object({
10895
11107
  operation: literal("git.branch_setup"),
@@ -10902,7 +11114,11 @@ var externalActionSchema = discriminatedUnion("operation", [
10902
11114
  }).strict(),
10903
11115
  object({
10904
11116
  operation: literal("git.commit"),
10905
- payload: object({ message: string2().min(1) }).strict()
11117
+ payload: object({
11118
+ message: string2().min(1).optional(),
11119
+ plan_steps: array(string2().min(1)).optional(),
11120
+ plan_branch: string2().min(1).optional()
11121
+ }).strict()
10906
11122
  }).strict(),
10907
11123
  object({
10908
11124
  operation: literal("git.push"),
@@ -10991,7 +11207,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
10991
11207
  payload,
10992
11208
  ...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 } : {}
10993
11209
  });
10994
- 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).";
11210
+ 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).";
10995
11211
  var externalActionRef = (host, actor, descriptor) => ({
10996
11212
  kind: "host",
10997
11213
  host,
@@ -11042,14 +11258,185 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
11042
11258
  return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
11043
11259
  return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
11044
11260
  };
11261
+ var planCommitDescriptor = (content) => {
11262
+ try {
11263
+ const value = JSON.parse(content);
11264
+ if (value.operation !== "git.commit" || !value.payload)
11265
+ return null;
11266
+ const steps = value.payload.plan_steps;
11267
+ const branch = value.payload.plan_branch;
11268
+ if (!Array.isArray(steps) || steps.some((step) => typeof step !== "string" || !step) || typeof branch !== "string" || !branch)
11269
+ return null;
11270
+ return { steps, branch };
11271
+ } catch {
11272
+ return null;
11273
+ }
11274
+ };
11275
+ var approvedPlanCommit = (store, host, actor, message) => {
11276
+ if (typeof message !== "string" || !message)
11277
+ return failure("invalid_input", "commit message is required");
11278
+ const listed = store.listTasks();
11279
+ const workspace = store.readWorkspace();
11280
+ if (!listed.ok || !workspace.ok)
11281
+ return failure("storage_error", "external action state is unavailable");
11282
+ if (!workspace.data)
11283
+ return failure("not_found", "workspace not found");
11284
+ const matches = listed.data.flatMap((task) => {
11285
+ if (task.status !== "active" || task.workspaceId !== workspace.data.id)
11286
+ return [];
11287
+ if (task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
11288
+ return [];
11289
+ return task.decisions.flatMap((entry) => {
11290
+ const decision = entry.data;
11291
+ if (decision.purpose !== "action" || decision.response !== "approved" || decision.revoked !== null)
11292
+ return [];
11293
+ if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
11294
+ return [];
11295
+ const plan = planCommitDescriptor(decision.binding.approvedContent);
11296
+ if (!plan)
11297
+ return [];
11298
+ const completed = task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ?? [];
11299
+ if (plan.steps[completed.length] !== message)
11300
+ return [];
11301
+ return [{ task, entry, steps: plan.steps, branch: plan.branch, completed: [...completed] }];
11302
+ });
11303
+ });
11304
+ if (matches.length !== 1)
11305
+ return failure("permission_denied", matches.length === 0 ? "no plan commit authorization matches this message" : "multiple plan authorizations match this message");
11306
+ return success(workspace.data.revision, null, matches[0]);
11307
+ };
11308
+ var commitMessageFromDescriptor = (operation) => {
11309
+ try {
11310
+ const descriptor = JSON.parse(operation);
11311
+ return descriptor.operation === "git.commit" && typeof descriptor.payload?.message === "string" ? descriptor.payload.message : undefined;
11312
+ } catch {
11313
+ return;
11314
+ }
11315
+ };
11316
+ var planCommitBinding = (store, host, actor, operation) => {
11317
+ const message = commitMessageFromDescriptor(operation);
11318
+ if (!message)
11319
+ return null;
11320
+ const plan = approvedPlanCommit(store, host, actor, message);
11321
+ if (!plan.ok)
11322
+ return null;
11323
+ let branch;
11324
+ try {
11325
+ branch = execFileSync("git", ["branch", "--show-current"], {
11326
+ cwd: store.root,
11327
+ encoding: "utf8",
11328
+ stdio: ["pipe", "pipe", "pipe"]
11329
+ }).trim();
11330
+ } catch {
11331
+ return null;
11332
+ }
11333
+ if (!branch || branch !== plan.data.branch)
11334
+ return null;
11335
+ const workspace = store.readWorkspace();
11336
+ if (!workspace.ok || !workspace.data)
11337
+ return null;
11338
+ return {
11339
+ taskId: plan.data.task.id,
11340
+ decisionId: plan.data.entry.id,
11341
+ binding: plan.data.entry.data.binding,
11342
+ expectedRevision: plan.data.task.revision,
11343
+ expectedWorkspaceRevision: workspace.data.revision,
11344
+ step: message
11345
+ };
11346
+ };
11045
11347
  var approvedExternalAction = (store, host, actor, operation) => {
11046
11348
  const selected = externalActionState(store, host, actor, operation);
11047
- if (!selected.ok)
11349
+ if (selected.ok) {
11350
+ if (selected.data.entry.data.consumption !== null)
11351
+ 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");
11048
11352
  return selected;
11049
- if (selected.data.entry.data.consumption !== null)
11050
- 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");
11353
+ }
11354
+ const intent = approvedBranchSetupIntent(store, host, actor, operation);
11355
+ if (intent.ok) {
11356
+ if (intent.data.entry.data.consumption !== null)
11357
+ 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");
11358
+ return intent;
11359
+ }
11051
11360
  return selected;
11052
11361
  };
11362
+ var branchSetupIntent = (descriptor) => {
11363
+ try {
11364
+ const value = JSON.parse(descriptor);
11365
+ if (value.operation !== "git.branch_setup" || !value.payload)
11366
+ return null;
11367
+ const { target_branch, resolved } = value.payload;
11368
+ if (typeof target_branch !== "string" || !target_branch || !resolved)
11369
+ return null;
11370
+ if (typeof resolved.base_branch !== "string" || typeof resolved.target_exists !== "boolean")
11371
+ return null;
11372
+ return {
11373
+ target: target_branch,
11374
+ base: resolved.base_branch,
11375
+ remoteBase: typeof resolved.remote_base === "string" ? resolved.remote_base : null,
11376
+ targetExists: resolved.target_exists
11377
+ };
11378
+ } catch {
11379
+ return null;
11380
+ }
11381
+ };
11382
+ var approvedBranchSetupIntent = (store, host, actor, operation) => {
11383
+ const current = branchSetupIntent(operation);
11384
+ if (!current)
11385
+ return failure("permission_denied", "no approved action is bound to this session");
11386
+ const listed = store.listTasks();
11387
+ const workspace = store.readWorkspace();
11388
+ if (!listed.ok || !workspace.ok)
11389
+ return failure("storage_error", "external action state is unavailable");
11390
+ if (!workspace.data)
11391
+ return failure("not_found", "workspace not found");
11392
+ const candidates = listed.data.flatMap((task) => {
11393
+ 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)
11394
+ return [];
11395
+ return task.decisions.filter((entry) => entry.data.purpose === "action" && entry.data.response === "approved" && entry.data.revoked === null).map((entry) => ({ task, entry }));
11396
+ });
11397
+ const matches = candidates.filter(({ entry }) => {
11398
+ const intent = branchSetupIntent(entry.data.binding.approvedContent);
11399
+ return intent !== null && intent.target === current.target && intent.base === current.base && intent.remoteBase === current.remoteBase && intent.targetExists === current.targetExists;
11400
+ });
11401
+ if (matches.length !== 1)
11402
+ return failure("permission_denied", matches.length === 0 ? "no approved action is bound to this session" : "multiple approved actions match this session");
11403
+ return success(workspace.data.revision, null, { ...matches[0], workspace: workspace.data });
11404
+ };
11405
+ var priorResolvedDrift = (store, host, actor, operation, payload, currentResolved) => {
11406
+ const prior = priorExternalAction(store, host, actor, operation, payload);
11407
+ if (!prior.ok)
11408
+ return success(null, null, null);
11409
+ let baseline;
11410
+ try {
11411
+ const parsed = JSON.parse(prior.data.entry.data.binding.approvedContent);
11412
+ baseline = parsed.payload?.resolved;
11413
+ } catch {
11414
+ return success(null, null, null);
11415
+ }
11416
+ if (baseline === undefined || baseline === null)
11417
+ return success(null, null, null);
11418
+ if (operation === "git.branch_setup") {
11419
+ const before = branchSetupIntent(prior.data.entry.data.binding.approvedContent);
11420
+ let after = null;
11421
+ try {
11422
+ after = branchSetupIntent(JSON.stringify({
11423
+ operation,
11424
+ payload: { ...payload, resolved: currentResolved }
11425
+ }));
11426
+ } catch {
11427
+ after = null;
11428
+ }
11429
+ if (before && after) {
11430
+ 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;
11431
+ if (moved === null)
11432
+ return success(null, null, null);
11433
+ return failure("invalid_input", `repository state changed since approval (${moved} moved); show the current state and approve the action again`, { outcome: "not_started", operation });
11434
+ }
11435
+ }
11436
+ if (canonicalJson(baseline) === canonicalJson(currentResolved))
11437
+ return success(null, null, null);
11438
+ return failure("invalid_input", "repository state changed since approval; show the current state and approve the action again", { outcome: "not_started", operation });
11439
+ };
11053
11440
  var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
11054
11441
  let steps = [];
11055
11442
  try {
@@ -11327,7 +11714,7 @@ function resolveRequirements(input) {
11327
11714
  ].join(", ")}`,
11328
11715
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
11329
11716
  before: "dependent_action",
11330
- dependentAction: "dependent-action",
11717
+ dependentAction: null,
11331
11718
  acceptanceAllowed: false
11332
11719
  }));
11333
11720
  if (signals.productChoiceOpen.value === true)
@@ -11338,7 +11725,7 @@ function resolveRequirements(input) {
11338
11725
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
11339
11726
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
11340
11727
  before: "dependent_action",
11341
- dependentAction: "dependent-action",
11728
+ dependentAction: null,
11342
11729
  acceptanceAllowed: false
11343
11730
  }));
11344
11731
  if (signals.behaviorChange.value === true)
@@ -11496,59 +11883,6 @@ function diffPolicy(previous, next, reason, now) {
11496
11883
  return parsed.data;
11497
11884
  }
11498
11885
 
11499
- // packages/workit-core/src/core/workers.ts
11500
- var same = (left, right) => {
11501
- try {
11502
- return canonicalJson(left) === canonicalJson(right);
11503
- } catch {
11504
- return false;
11505
- }
11506
- };
11507
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
11508
- var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
11509
- const code = char.charCodeAt(0);
11510
- return code < 32 || code === 127;
11511
- }) && !value.split(/[\\/]/).includes("..");
11512
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
11513
- var workerIdOf = (value) => value.workerId ?? null;
11514
- function assertProductWriteAllowed(input) {
11515
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
11516
- return failure("invalid_input", "invalid product write path");
11517
- if (!input.store)
11518
- return failure("permission_denied", "write authorization requires core state");
11519
- const task = input.store.readTask(input.task.id);
11520
- if (!task.ok)
11521
- return task;
11522
- const workspace = input.store.readWorkspace();
11523
- if (!workspace.ok)
11524
- return workspace;
11525
- if (!workspace.data)
11526
- return failure("not_found", "workspace not found");
11527
- const workspaceRecord = workspace.data;
11528
- if (task.data.status !== "active")
11529
- return failure("invalid_transition", "paused or closed tasks cannot own product writes");
11530
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
11531
- return failure("recovery_required", "task and workspace bindings are invalid");
11532
- const writer = workspaceRecord.writer;
11533
- if (!writer)
11534
- return failure("permission_denied", "checkout has no writer owner");
11535
- if (writer.state === "uncertain")
11536
- return failure("recovery_required", "checkout writer requires recovery", {
11537
- owner: writer.owner
11538
- });
11539
- const caller = callerValue(input.caller);
11540
- const workerId = workerIdOf(input.caller);
11541
- const owner = writer.owner;
11542
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
11543
- return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
11544
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
11545
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
11546
- return failure("permission_denied", "only implementers can own product writes");
11547
- 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))))
11548
- return failure("recovery_required", "worker session is not an active writer");
11549
- return success(null, workspaceRecord.revision, owner);
11550
- }
11551
-
11552
11886
  // packages/workit-core/src/core/task-engine.ts
11553
11887
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
11554
11888
  kind,
@@ -11679,15 +12013,20 @@ var applyWorkerLifecycle = (input) => {
11679
12013
  return failure("invalid_transition", "worker has not started");
11680
12014
  if (input.state === "running" && entry.data.state === "stopped")
11681
12015
  return failure("invalid_transition", "stopped worker cannot run again");
12016
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
11682
12017
  const nextEntry = {
11683
12018
  ...entry,
11684
12019
  recordedAt: input.now ?? entry.recordedAt,
11685
12020
  provenance: authority.provenance,
11686
- data: { ...entry.data, state: input.state, session: input.session }
12021
+ data: {
12022
+ ...entry.data,
12023
+ state: terminalCancel ? "cancelling" : input.state,
12024
+ session: input.session
12025
+ }
11687
12026
  };
11688
12027
  const shouldClear = input.state === "stopped" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
11689
12028
  const shouldUncertain = input.state === "unknown" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
11690
- if (entry.data.state === input.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
12029
+ if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
11691
12030
  return success(task.data.revision, workspace.data.revision, entry);
11692
12031
  const changed = input.store.mutateTaskAndWorkspace({
11693
12032
  taskId: task.data.id,
@@ -12057,7 +12396,7 @@ class WorkitCore {
12057
12396
  activeWorkerBlocker(task, workspace) {
12058
12397
  if (workspace.writer)
12059
12398
  return failure("recovery_required", "writer ownership must be released first");
12060
- if (task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
12399
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
12061
12400
  return failure("recovery_required", "worker state requires reconciliation");
12062
12401
  return null;
12063
12402
  }
@@ -12294,11 +12633,6 @@ class WorkitCore {
12294
12633
  if (helper && !helper.ok)
12295
12634
  return helper;
12296
12635
  const evidence = input.evidence;
12297
- if (helper?.ok) {
12298
- const assignment = helper.data.data.assignment;
12299
- 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))
12300
- return failure("permission_denied", "evidence is outside the worker assignment");
12301
- }
12302
12636
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
12303
12637
  return failure("invalid_input", "missing or skipped evidence requires a reason");
12304
12638
  if (evidence.kind === "review") {
@@ -12315,6 +12649,11 @@ class WorkitCore {
12315
12649
  evidence.beforeCandidateId ??= null;
12316
12650
  evidence.candidateId ??= null;
12317
12651
  }
12652
+ if (helper?.ok) {
12653
+ const assignment = helper.data.data.assignment;
12654
+ 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))
12655
+ return failure("permission_denied", "evidence is outside the worker assignment");
12656
+ }
12318
12657
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
12319
12658
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
12320
12659
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -12368,7 +12707,12 @@ class WorkitCore {
12368
12707
  const input = parsed.data;
12369
12708
  if (nativeRequired && input.action !== "record")
12370
12709
  return failure("permission_denied", "native observation is only valid for recording decisions");
12371
- if (nativeRequired && nativeObservation === undefined)
12710
+ const stated = input.action === "record" && input.response === "stated";
12711
+ if (stated && input.purpose === "action")
12712
+ return failure("permission_denied", "stated choices cannot authorize mutating actions");
12713
+ if (stated && !input.binding?.statedChoice)
12714
+ return failure("invalid_input", "stated choices require binding.statedChoice");
12715
+ if (nativeRequired && nativeObservation === undefined && !stated)
12372
12716
  return failure("permission_denied", "native decision observation is required");
12373
12717
  const task = this.store.readTask(input.taskId);
12374
12718
  if (!task.ok)
@@ -12401,7 +12745,7 @@ class WorkitCore {
12401
12745
  consumption: null
12402
12746
  };
12403
12747
  const data = { ...base, digest: decisionDigest(base) };
12404
- const native = nativeRequired ? verifyNativeDecision(this.context.nativeAuthority, {
12748
+ const native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
12405
12749
  observation: nativeObservation,
12406
12750
  expected: {
12407
12751
  taskId: task.data.id,
@@ -13210,6 +13554,9 @@ class WorkitCore {
13210
13554
  id: task.id,
13211
13555
  revision: task.revision,
13212
13556
  workspaceRevision: workspace.data.revision,
13557
+ createdAt: task.createdAt,
13558
+ updatedAt: task.updatedAt,
13559
+ runtime: task.runtime ?? null,
13213
13560
  objective: task.intent.data.objective,
13214
13561
  status: task.status,
13215
13562
  closure: task.closure,
@@ -13295,7 +13642,7 @@ class WorkitCore {
13295
13642
  const base = reconcileResume(effectiveView);
13296
13643
  if (!base.ok)
13297
13644
  return base;
13298
- const needsReconciliation = view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
13645
+ const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
13299
13646
  const blockers = [...base.data.blockers];
13300
13647
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
13301
13648
  blockers.push({
@@ -13362,7 +13709,8 @@ class WorkitCore {
13362
13709
  ...current,
13363
13710
  status,
13364
13711
  candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
13365
- progress: status === "paused" ? { ...current.progress, summary: input.reason } : current.progress
13712
+ progress: current.progress,
13713
+ pauseReason: status === "paused" ? input.reason ?? null : null
13366
13714
  })
13367
13715
  });
13368
13716
  if (!changed.ok)
@@ -13398,7 +13746,7 @@ class WorkitCore {
13398
13746
  return workspace;
13399
13747
  if (!workspace.data)
13400
13748
  return failure("not_found", "workspace not found");
13401
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
13749
+ if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
13402
13750
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
13403
13751
  this.fillRevisions(input, task.data, workspace.data);
13404
13752
  const changed = this.store.mutateTaskAndWorkspace({
@@ -13516,15 +13864,15 @@ function resolveGitRevision(root, value) {
13516
13864
  }
13517
13865
  function resolveInside(root, candidate) {
13518
13866
  const base = realpathSync5(root);
13519
- const target = path6.resolve(base, candidate);
13520
- if (target !== base && !target.startsWith(base + path6.sep)) {
13867
+ const target = path7.resolve(base, candidate);
13868
+ if (target !== base && !target.startsWith(base + path7.sep)) {
13521
13869
  throw new Error("path must stay inside repository root");
13522
13870
  }
13523
13871
  let ancestor = target;
13524
- while (!existsSync3(ancestor))
13525
- ancestor = path6.dirname(ancestor);
13872
+ while (!existsSync4(ancestor))
13873
+ ancestor = path7.dirname(ancestor);
13526
13874
  const canonicalAncestor = realpathSync5(ancestor);
13527
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path6.sep)) {
13875
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
13528
13876
  throw new Error("path must stay inside repository root");
13529
13877
  }
13530
13878
  return target;
@@ -13567,6 +13915,13 @@ var piCapabilities = (ctx) => [
13567
13915
  assurance: "agent_guided",
13568
13916
  reason: "Pi extensions do not sandbox arbitrary shell commands.",
13569
13917
  refs: [hostRef("tool_call")]
13918
+ },
13919
+ {
13920
+ name: "fresh-context-review",
13921
+ surface: "supervised_worker",
13922
+ assurance: "agent_guided",
13923
+ reason: "independent review runs as a supervised stock-Pi process; evidence evaluation enforces reviewer exclusivity",
13924
+ refs: [hostRef("worker")]
13570
13925
  }
13571
13926
  ];
13572
13927
  var piContext = (ctx) => ({
@@ -13634,15 +13989,15 @@ var WORKIT_SKILL_ALIASES = {
13634
13989
  if (false) {}
13635
13990
 
13636
13991
  // packages/workit-core/src/core/branch.ts
13637
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
13638
- import { execFileSync as execFileSync2 } from "node:child_process";
13639
- import path10 from "node:path";
13992
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
13993
+ import { execFileSync as execFileSync3 } from "node:child_process";
13994
+ import path11 from "node:path";
13640
13995
 
13641
13996
  // packages/workit-core/src/core/git.ts
13642
- import { execFileSync } from "node:child_process";
13997
+ import { execFileSync as execFileSync2 } from "node:child_process";
13643
13998
  var run2 = (cwd, args) => {
13644
13999
  try {
13645
- const stdout = execFileSync("git", args, {
14000
+ const stdout = execFileSync2("git", args, {
13646
14001
  cwd,
13647
14002
  encoding: "utf8",
13648
14003
  stdio: ["pipe", "pipe", "pipe"]
@@ -13710,14 +14065,14 @@ var gitContext = (workspaceRoot, paths = []) => {
13710
14065
  import {
13711
14066
  copyFileSync,
13712
14067
  cpSync,
13713
- existsSync as existsSync4,
14068
+ existsSync as existsSync5,
13714
14069
  mkdirSync as mkdirSync2,
13715
14070
  readFileSync as readFileSync4,
13716
14071
  readdirSync as readdirSync3,
13717
14072
  writeFileSync as writeFileSync2
13718
14073
  } from "node:fs";
13719
14074
  import os from "node:os";
13720
- import path7 from "node:path";
14075
+ import path8 from "node:path";
13721
14076
 
13722
14077
  // packages/workit-core/src/core/boundary.ts
13723
14078
  var EVENT = {
@@ -13761,7 +14116,7 @@ var mergePreset = (preset, input = {}, current = {
13761
14116
  protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
13762
14117
  };
13763
14118
  };
13764
- var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workit");
14119
+ var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path8.join(process.env.XDG_CONFIG_HOME || path8.join(os.homedir(), ".config"), "workit");
13765
14120
  var migratedDir = null;
13766
14121
  var migrationFailed = false;
13767
14122
  var ensureConfigDir = (dir = resolveConfigDir()) => {
@@ -13771,12 +14126,12 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
13771
14126
  migratedDir = dir;
13772
14127
  return dir;
13773
14128
  }
13774
- const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workflow-toolkit");
13775
- if (!existsSync4(legacy)) {
14129
+ const legacy = path8.join(process.env.XDG_CONFIG_HOME || path8.join(os.homedir(), ".config"), "workflow-toolkit");
14130
+ if (!existsSync5(legacy)) {
13776
14131
  migratedDir = dir;
13777
14132
  return dir;
13778
14133
  }
13779
- if (!migrationFailed && existsSync4(dir)) {
14134
+ if (!migrationFailed && existsSync5(dir)) {
13780
14135
  migratedDir = dir;
13781
14136
  return dir;
13782
14137
  }
@@ -13784,9 +14139,9 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
13784
14139
  mkdirSync2(dir, { recursive: true });
13785
14140
  diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
13786
14141
  for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
13787
- const src = path7.join(legacy, entry.name);
13788
- const dest = path7.join(dir, entry.name);
13789
- if (existsSync4(dest))
14142
+ const src = path8.join(legacy, entry.name);
14143
+ const dest = path8.join(dir, entry.name);
14144
+ if (existsSync5(dest))
13790
14145
  continue;
13791
14146
  try {
13792
14147
  if (entry.isDirectory())
@@ -13859,7 +14214,7 @@ var parseConfigResult = (raw, file) => {
13859
14214
  };
13860
14215
  };
13861
14216
  var readConfigTyped = (dir) => {
13862
- const file = path7.join(dir ?? configDir(), "config.json");
14217
+ const file = path8.join(dir ?? configDir(), "config.json");
13863
14218
  return parseConfigResult(readSafe(file), file);
13864
14219
  };
13865
14220
  var readConfig = () => {
@@ -13905,9 +14260,9 @@ var resolveCommitPolicy = (config, workspace) => {
13905
14260
 
13906
14261
  // packages/workit-core/src/core/workspaces.ts
13907
14262
  import { readFileSync as readFileSync5, realpathSync as realpathSync6 } from "node:fs";
13908
- import path8 from "node:path";
14263
+ import path9 from "node:path";
13909
14264
  var readWorkspacesResult = (dir = configDir()) => {
13910
- const file = path8.join(dir, "workspaces.json");
14265
+ const file = path9.join(dir, "workspaces.json");
13911
14266
  let raw;
13912
14267
  try {
13913
14268
  raw = readFileSync5(file, "utf8");
@@ -14005,10 +14360,10 @@ var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
14005
14360
 
14006
14361
  // packages/workit-core/src/core/vcs-config.ts
14007
14362
  import fs7 from "node:fs";
14008
- import path9 from "node:path";
14363
+ import path10 from "node:path";
14009
14364
  import { spawnSync as spawnSync3 } from "node:child_process";
14010
14365
  var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
14011
- var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
14366
+ var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path10.join(configDir(), "vcs.json");
14012
14367
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
14013
14368
  var remoteProvider = (cwd) => {
14014
14369
  const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
@@ -14091,8 +14446,8 @@ function vcsConfig(mode, cwd) {
14091
14446
  }
14092
14447
  const prov = cfg[provider] ?? {};
14093
14448
  const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
14094
- const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path9.join(configDir(), `${provider}.token`));
14095
- const tokenPath = path9.resolve(tokenFile);
14449
+ const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path10.join(configDir(), `${provider}.token`));
14450
+ const tokenPath = path10.resolve(tokenFile);
14096
14451
  let tokenOk = false;
14097
14452
  if (fs7.existsSync(tokenPath)) {
14098
14453
  const token = fs7.readFileSync(tokenPath, "utf8").trim();
@@ -14101,7 +14456,7 @@ function vcsConfig(mode, cwd) {
14101
14456
  }
14102
14457
  const out = {
14103
14458
  ok: true,
14104
- configPath: path9.resolve(cfgPath),
14459
+ configPath: path10.resolve(cfgPath),
14105
14460
  provider,
14106
14461
  defaultTargetBranch: defaultTarget,
14107
14462
  pr: cfg.pr ?? {},
@@ -14202,7 +14557,7 @@ var baseBranch = (cwd) => {
14202
14557
  return { base: String(resolved.defaultTargetBranch ?? "develop") };
14203
14558
  };
14204
14559
  var originBaseReady = (cwd, base) => {
14205
- const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14560
+ const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14206
14561
  try {
14207
14562
  try {
14208
14563
  run(["fetch", "origin", base, "--prune"]);
@@ -14210,7 +14565,7 @@ var originBaseReady = (cwd, base) => {
14210
14565
  run(["fetch", "origin", "--prune"]);
14211
14566
  }
14212
14567
  try {
14213
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
14568
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
14214
14569
  cwd,
14215
14570
  stdio: "pipe"
14216
14571
  });
@@ -14229,11 +14584,11 @@ var originBaseReady = (cwd, base) => {
14229
14584
  }
14230
14585
  };
14231
14586
  var fastForwardBase = (cwd, base) => {
14232
- const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14587
+ const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14233
14588
  try {
14234
14589
  let hasLocalBase = true;
14235
14590
  try {
14236
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
14591
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
14237
14592
  cwd,
14238
14593
  stdio: "pipe"
14239
14594
  });
@@ -14273,19 +14628,18 @@ var branchSetup = ({
14273
14628
  workspace_root,
14274
14629
  log
14275
14630
  }) => {
14276
- const cwd = path10.resolve(workspace_root);
14277
- const exec = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14631
+ const cwd = path11.resolve(workspace_root);
14632
+ const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14278
14633
  const current = gitContext(cwd).branch;
14279
14634
  if (!current || current === "unknown")
14280
- return { error: "not in a git repository" };
14635
+ return { error: "not in a git repository", phase: "preflight" };
14281
14636
  const sdd = sdd_dir ?? "docs";
14282
- const manifestPath = path10.isAbsolute(sdd) ? path10.join(sdd, "manifest.json") : path10.join(cwd, sdd, "manifest.json");
14637
+ const manifestPath = path11.isAbsolute(sdd) ? path11.join(sdd, "manifest.json") : path11.join(cwd, sdd, "manifest.json");
14283
14638
  const ensureManifestDir = () => {
14284
- const dir = path10.dirname(manifestPath);
14285
- if (!existsSync5(dir))
14639
+ const dir = path11.dirname(manifestPath);
14640
+ if (!existsSync6(dir))
14286
14641
  mkdirSync3(dir, { recursive: true, mode: 493 });
14287
14642
  };
14288
- ensureManifestDir();
14289
14643
  const readManifest = () => {
14290
14644
  try {
14291
14645
  return JSON.parse(readFileSync6(manifestPath, "utf8"));
@@ -14303,13 +14657,16 @@ var branchSetup = ({
14303
14657
  const manifest = readManifest();
14304
14658
  const ref = manifest.stash_ref;
14305
14659
  if (!ref)
14306
- return { error: "no stash_ref in manifest" };
14660
+ return { error: "no stash_ref in manifest", phase: "preflight" };
14307
14661
  journal(`pre-pop: ${String(ref)}`);
14308
14662
  try {
14309
14663
  exec(["stash", "pop", String(ref)]);
14310
14664
  } catch (error) {
14311
14665
  journal("pop: failed");
14312
- return { error: error instanceof Error ? error.message : "stash pop failed" };
14666
+ return {
14667
+ error: error instanceof Error ? error.message : "stash pop failed",
14668
+ phase: "post_mutation"
14669
+ };
14313
14670
  }
14314
14671
  journal("pop: ok");
14315
14672
  delete manifest.stash_ref;
@@ -14319,11 +14676,17 @@ var branchSetup = ({
14319
14676
  }
14320
14677
  const target = target_branch ?? "";
14321
14678
  if (!target)
14322
- return { error: "target branch required" };
14679
+ return {
14680
+ error: "target_branch (the working branch to create or switch to) is required for setup",
14681
+ phase: "preflight"
14682
+ };
14323
14683
  if (isProtected(cwd, target))
14324
- return { error: `protected branch ${target}` };
14684
+ return { error: `protected branch ${target}`, phase: "preflight" };
14325
14685
  if (!allowedBranch(cwd, target))
14326
- return { error: `target branch ${target} is not allowed by the branch policy` };
14686
+ return {
14687
+ error: `target branch ${target} is not allowed by the branch policy`,
14688
+ phase: "preflight"
14689
+ };
14327
14690
  let base;
14328
14691
  let targetExists = true;
14329
14692
  try {
@@ -14334,12 +14697,12 @@ var branchSetup = ({
14334
14697
  if (!targetExists) {
14335
14698
  const baseResolved = baseBranch(cwd);
14336
14699
  if ("error" in baseResolved)
14337
- return { error: baseResolved.error };
14700
+ return { error: baseResolved.error, phase: "preflight" };
14338
14701
  base = baseResolved.base;
14339
14702
  }
14340
14703
  journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
14341
14704
  let stash_ref;
14342
- const failAfterStash = (message) => {
14705
+ const failAfterStash = (message, phase = "post_mutation") => {
14343
14706
  let suffix = "";
14344
14707
  if (stash_ref) {
14345
14708
  journal(`pre-pop: ${stash_ref}`);
@@ -14352,27 +14715,31 @@ var branchSetup = ({
14352
14715
  suffix = " (changes preserved in stash)";
14353
14716
  }
14354
14717
  }
14355
- return { error: `${message}${suffix}` };
14718
+ return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
14356
14719
  };
14357
14720
  if (current !== target) {
14358
14721
  const dirty = Boolean(gitContext(cwd).status_short.trim());
14359
14722
  if (dirty && stash !== "yes") {
14360
14723
  return {
14361
- error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes"
14724
+ error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
14725
+ phase: "preflight"
14362
14726
  };
14363
14727
  }
14364
14728
  let validatedBase;
14365
14729
  if (!targetExists && base !== undefined) {
14366
14730
  const ready = originBaseReady(cwd, base);
14367
14731
  if (!ready.ok)
14368
- return { error: ready.error ?? "ensure-base-branch failed" };
14732
+ return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
14369
14733
  validatedBase = base;
14370
14734
  }
14371
14735
  if (dirty) {
14372
14736
  try {
14373
14737
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
14374
14738
  } catch (error) {
14375
- return { error: error instanceof Error ? error.message : "stash push failed" };
14739
+ return {
14740
+ error: error instanceof Error ? error.message : "stash push failed",
14741
+ phase: "post_mutation"
14742
+ };
14376
14743
  }
14377
14744
  stash_ref = "stash@{0}";
14378
14745
  journal(`stash push: ${stash_ref}`);
@@ -14383,14 +14750,14 @@ var branchSetup = ({
14383
14750
  } catch (error) {
14384
14751
  const message = error instanceof Error ? error.message : "checkout failed";
14385
14752
  if (/worktree/i.test(message)) {
14386
- return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`);
14753
+ return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
14387
14754
  }
14388
14755
  try {
14389
14756
  let effectiveBase = base;
14390
14757
  if (effectiveBase === undefined) {
14391
14758
  const lateResolved = baseBranch(cwd);
14392
14759
  if ("error" in lateResolved)
14393
- return failAfterStash(lateResolved.error);
14760
+ return failAfterStash(lateResolved.error, "preflight");
14394
14761
  effectiveBase = lateResolved.base;
14395
14762
  }
14396
14763
  const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
@@ -14437,7 +14804,7 @@ var branchSetup = ({
14437
14804
 
14438
14805
  // packages/workit-core/src/core/pr-create.ts
14439
14806
  import fs8 from "node:fs";
14440
- import path11 from "node:path";
14807
+ import path12 from "node:path";
14441
14808
  import { spawnSync as spawnSync4 } from "node:child_process";
14442
14809
  function parseGhRepo(remote) {
14443
14810
  remote = (remote || "").trim().replace(/\/+$/, "");
@@ -14502,11 +14869,11 @@ ${line}` : line;
14502
14869
  }
14503
14870
  function whichOnPath(tool) {
14504
14871
  const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
14505
- for (const dir of (process.env.PATH ?? "").split(path11.delimiter)) {
14872
+ for (const dir of (process.env.PATH ?? "").split(path12.delimiter)) {
14506
14873
  if (!dir)
14507
14874
  continue;
14508
14875
  for (const name of names) {
14509
- const candidate = path11.join(dir, name);
14876
+ const candidate = path12.join(dir, name);
14510
14877
  try {
14511
14878
  fs8.accessSync(candidate, fs8.constants.X_OK);
14512
14879
  return candidate;
@@ -14609,7 +14976,7 @@ function prCreate(env, cwd) {
14609
14976
  const draft = String(env.WF_PR_DRAFT ?? "false").toLowerCase() === "true";
14610
14977
  let baseUrl = cfg.youtrack_base_url;
14611
14978
  if (!baseUrl) {
14612
- const ytCfg = process.env.WORKFLOW_YOUTRACK_CONFIG ?? path11.join(path11.dirname(String(cfg.configPath)), "youtrack.json");
14979
+ const ytCfg = process.env.WORKFLOW_YOUTRACK_CONFIG ?? path12.join(path12.dirname(String(cfg.configPath)), "youtrack.json");
14613
14980
  try {
14614
14981
  const yt = JSON.parse(fs8.readFileSync(ytCfg, "utf8"));
14615
14982
  if (yt && typeof yt === "object")
@@ -14716,8 +15083,8 @@ function prCreate(env, cwd) {
14716
15083
 
14717
15084
  // packages/workit-core/src/core/repo-context.ts
14718
15085
  import { spawnSync as spawnSync5 } from "node:child_process";
14719
- import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
14720
- import path12 from "node:path";
15086
+ import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
15087
+ import path13 from "node:path";
14721
15088
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
14722
15089
  try {
14723
15090
  gitRevisionParts(value);
@@ -14851,7 +15218,7 @@ var findPrTemplate = (cwd) => {
14851
15218
  ]) {
14852
15219
  const parts = rel.split("/");
14853
15220
  const base = parts[parts.length - 1];
14854
- const dir = parts.length > 1 ? path12.join(cwd, ...parts.slice(0, -1)) : cwd;
15221
+ const dir = parts.length > 1 ? path13.join(cwd, ...parts.slice(0, -1)) : cwd;
14855
15222
  const actual = probeCaseInsensitive(dir, base);
14856
15223
  if (actual)
14857
15224
  return [...parts.slice(0, -1), actual].join("/");
@@ -14882,8 +15249,8 @@ var diffStatForRange = (cwd, range) => {
14882
15249
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
14883
15250
  };
14884
15251
  var packageScripts = (cwd, keys) => {
14885
- const pkgPath = path12.join(cwd, "package.json");
14886
- if (!existsSync6(pkgPath))
15252
+ const pkgPath = path13.join(cwd, "package.json");
15253
+ if (!existsSync7(pkgPath))
14887
15254
  return [];
14888
15255
  try {
14889
15256
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
@@ -14908,9 +15275,9 @@ var documentationFiles = (cwd) => {
14908
15275
  if (entry.isDirectory()) {
14909
15276
  if (excluded.has(entry.name))
14910
15277
  continue;
14911
- walk(path12.join(dir, entry.name), depth + 1);
15278
+ walk(path13.join(dir, entry.name), depth + 1);
14912
15279
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
14913
- matches.push("./" + path12.relative(cwd, path12.join(dir, entry.name)).replaceAll("\\", "/"));
15280
+ matches.push("./" + path13.relative(cwd, path13.join(dir, entry.name)).replaceAll("\\", "/"));
14914
15281
  }
14915
15282
  }
14916
15283
  };
@@ -14918,7 +15285,7 @@ var documentationFiles = (cwd) => {
14918
15285
  return matches.sort().slice(0, 200);
14919
15286
  };
14920
15287
  var readTrimmed = (file, maxLines) => {
14921
- if (!existsSync6(file))
15288
+ if (!existsSync7(file))
14922
15289
  return "";
14923
15290
  const lines = readFileSync7(file, "utf8").split(`
14924
15291
  `);
@@ -14926,7 +15293,7 @@ var readTrimmed = (file, maxLines) => {
14926
15293
  `);
14927
15294
  };
14928
15295
  function prReadyContext(root, range) {
14929
- const cwd = path12.resolve(repoRoot2(root));
15296
+ const cwd = path13.resolve(repoRoot2(root));
14930
15297
  if (range !== undefined && !isResolvableContextRange(cwd, range))
14931
15298
  return invalidRange(cwd);
14932
15299
  let stdout = "";
@@ -14984,7 +15351,7 @@ ${status.stderr}` : "");
14984
15351
  stdout += `template_path: ${template}
14985
15352
 
14986
15353
  `;
14987
- stdout += readTrimmed(path12.join(cwd, template), 220);
15354
+ stdout += readTrimmed(path13.join(cwd, template), 220);
14988
15355
  } else {
14989
15356
  stdout += `template_path: none
14990
15357
 
@@ -14994,7 +15361,7 @@ ${status.stderr}` : "");
14994
15361
  stdout += `
14995
15362
  `;
14996
15363
  stdout += printSection("Recent Validation Signals");
14997
- if (existsSync6(path12.join(cwd, "package.json"))) {
15364
+ if (existsSync7(path13.join(cwd, "package.json"))) {
14998
15365
  stdout += `package.json detected. Common scripts:
14999
15366
  `;
15000
15367
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -15003,7 +15370,7 @@ ${status.stderr}` : "");
15003
15370
  `) + `
15004
15371
  `;
15005
15372
  }
15006
- if (existsSync6(path12.join(cwd, "Cargo.toml")) || existsSync6(path12.join(cwd, "src-tauri/Cargo.toml"))) {
15373
+ if (existsSync7(path13.join(cwd, "Cargo.toml")) || existsSync7(path13.join(cwd, "src-tauri/Cargo.toml"))) {
15007
15374
  stdout += `Rust project detected.
15008
15375
  `;
15009
15376
  }
@@ -15036,7 +15403,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
15036
15403
  - Apply with the native workit_changelog_apply tool only (not hand-edits under Unreleased).
15037
15404
  - If Unreleased already has duplicate category headings, normalize_only first.`;
15038
15405
  function changelogContext(root, range) {
15039
- const cwd = path12.resolve(repoRoot2(root));
15406
+ const cwd = path13.resolve(repoRoot2(root));
15040
15407
  if (range !== undefined && !isResolvableContextRange(cwd, range))
15041
15408
  return invalidRange(cwd);
15042
15409
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -15054,8 +15421,8 @@ function changelogContext(root, range) {
15054
15421
  stdout += CHANGELOG_RULES + `
15055
15422
  `;
15056
15423
  stdout += printSection("Existing CHANGELOG.md");
15057
- const changelogPath = path12.join(cwd, "CHANGELOG.md");
15058
- if (existsSync6(changelogPath)) {
15424
+ const changelogPath = path13.join(cwd, "CHANGELOG.md");
15425
+ if (existsSync7(changelogPath)) {
15059
15426
  stdout += readTrimmed(changelogPath, 260) + `
15060
15427
  `;
15061
15428
  } else {
@@ -15074,7 +15441,7 @@ function changelogContext(root, range) {
15074
15441
  return { stdout, stderr: "", exitCode: 0, cwd };
15075
15442
  }
15076
15443
  function docsRefreshContext(root, range) {
15077
- const cwd = path12.resolve(repoRoot2(root));
15444
+ const cwd = path13.resolve(repoRoot2(root));
15078
15445
  if (range !== undefined && !isResolvableContextRange(cwd, range))
15079
15446
  return invalidRange(cwd);
15080
15447
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -15096,8 +15463,8 @@ function docsRefreshContext(root, range) {
15096
15463
  `) + `
15097
15464
  `;
15098
15465
  stdout += printSection("README Preview");
15099
- const readme = path12.join(cwd, "README.md");
15100
- if (existsSync6(readme)) {
15466
+ const readme = path13.join(cwd, "README.md");
15467
+ if (existsSync7(readme)) {
15101
15468
  stdout += readTrimmed(readme, 220) + `
15102
15469
  `;
15103
15470
  } else {
@@ -15105,8 +15472,8 @@ function docsRefreshContext(root, range) {
15105
15472
  `;
15106
15473
  }
15107
15474
  stdout += printSection("Package Scripts");
15108
- const pkgPath = path12.join(cwd, "package.json");
15109
- if (existsSync6(pkgPath)) {
15475
+ const pkgPath = path13.join(cwd, "package.json");
15476
+ if (existsSync7(pkgPath)) {
15110
15477
  try {
15111
15478
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
15112
15479
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
@@ -15119,7 +15486,7 @@ function docsRefreshContext(root, range) {
15119
15486
  return { stdout, stderr: "", exitCode: 0, cwd };
15120
15487
  }
15121
15488
  function releaseNotesContext(root, rangeOrTag) {
15122
- const cwd = path12.resolve(repoRoot2(root));
15489
+ const cwd = path13.resolve(repoRoot2(root));
15123
15490
  if (!rangeOrTag) {
15124
15491
  return { stdout: "", stderr: `ERROR: release tag or range required
15125
15492
  `, exitCode: 1, cwd };
@@ -15157,7 +15524,7 @@ function releaseNotesContext(root, rangeOrTag) {
15157
15524
  `;
15158
15525
  stdout += printSection("Existing Release Files");
15159
15526
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
15160
- if (existsSync6(path12.join(cwd, rel)))
15527
+ if (existsSync7(path13.join(cwd, rel)))
15161
15528
  stdout += rel + `
15162
15529
  `;
15163
15530
  }
@@ -15348,25 +15715,6 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
15348
15715
  import fs10 from "node:fs";
15349
15716
  import path14 from "node:path";
15350
15717
 
15351
- // packages/workit-core/src/core/package-root.ts
15352
- import { existsSync as existsSync7 } from "node:fs";
15353
- import path13 from "node:path";
15354
- import { fileURLToPath } from "node:url";
15355
- var packageRoot = () => {
15356
- let dir = path13.dirname(fileURLToPath(import.meta.url));
15357
- while (true) {
15358
- const parent = path13.dirname(dir);
15359
- if (existsSync7(path13.join(dir, "package.json")) || parent === dir)
15360
- return dir;
15361
- dir = parent;
15362
- }
15363
- };
15364
- var assetRoot = () => {
15365
- const root = packageRoot();
15366
- const assets = path13.join(root, "assets");
15367
- return existsSync7(assets) ? assets : root;
15368
- };
15369
-
15370
15718
  // packages/workit-core/src/core/templates.ts
15371
15719
  var repoRoot3 = assetRoot();
15372
15720
 
@@ -16572,6 +16920,101 @@ var safePath = (root, value) => {
16572
16920
  return null;
16573
16921
  }
16574
16922
  };
16923
+ var branchCreationBaseline = (root, target, base) => {
16924
+ const exists = gitValue(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]) !== null;
16925
+ if (exists)
16926
+ return { base_branch: base, target_exists: true, remote_base: null };
16927
+ const remote = run3(root, ["ls-remote", "origin", `refs/heads/${base}`]);
16928
+ const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
16929
+ return {
16930
+ base_branch: base,
16931
+ target_exists: false,
16932
+ remote_base: /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null
16933
+ };
16934
+ };
16935
+ var shortSha = (value) => typeof value === "string" && value ? value.slice(0, 8) : "unknown";
16936
+ var isProtectedBranch2 = (root, branch) => resolveBranchPolicyFor(root).protected.has(branch.toLowerCase());
16937
+ var actionProposalQuestion = (request, descriptorPayload) => {
16938
+ const payload = descriptorPayload ?? {};
16939
+ const resolved = payload.resolved ?? {};
16940
+ const payloadTarget = String(payload.target_branch ?? "");
16941
+ if (request.operation === "git.commit" && Array.isArray(payload.plan_steps)) {
16942
+ const steps = payload.plan_steps;
16943
+ return {
16944
+ presented: `Workit decision: action — Approve committing the plan's ${steps.length} listed tasks on \`${String(payload.plan_branch ?? "")}\`?`,
16945
+ approvedText: `Approve the ${steps.length} listed plan commits.`
16946
+ };
16947
+ }
16948
+ switch (request.operation) {
16949
+ case "git.branch_setup": {
16950
+ if (request.payload.action === "reapply_stash")
16951
+ return {
16952
+ presented: "Workit decision: action — Reapply the pre-checkout stash recorded in the branch manifest?",
16953
+ approvedText: "Reapply the recorded stash."
16954
+ };
16955
+ const target = String(payload.target_branch ?? "");
16956
+ const stashes = payload.stash === "yes" && resolved.dirty === true;
16957
+ if (resolved.target_exists === true)
16958
+ return stashes ? {
16959
+ presented: `Workit decision: action — Stash the dirty working tree and switch to branch \`${target}\`?`,
16960
+ approvedText: `Stash and switch to \`${target}\`.`
16961
+ } : {
16962
+ presented: `Workit decision: action — Switch to branch \`${target}\`?`,
16963
+ approvedText: `Switch to \`${target}\`.`
16964
+ };
16965
+ const base = String(resolved.base_branch ?? "the base branch");
16966
+ return stashes ? {
16967
+ presented: `Workit decision: action — Stash the dirty working tree and create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
16968
+ approvedText: `Stash and create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
16969
+ } : {
16970
+ presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
16971
+ approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
16972
+ };
16973
+ }
16974
+ case "git.commit": {
16975
+ const paths = Array.isArray(resolved.paths) ? resolved.paths : [];
16976
+ return {
16977
+ presented: `Workit decision: action — Commit ${paths.length} staged file(s) as "${String(request.payload.message)}"?`,
16978
+ approvedText: `Commit as "${String(request.payload.message)}".`
16979
+ };
16980
+ }
16981
+ case "git.push":
16982
+ return {
16983
+ presented: `Workit decision: action — Push branch \`${String(payload.branch ?? "")}\` to origin?`,
16984
+ approvedText: `Push \`${String(payload.branch ?? "")}\` to origin.`
16985
+ };
16986
+ case "hosting.pull_request":
16987
+ return {
16988
+ presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
16989
+ approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
16990
+ };
16991
+ case "changelog.apply":
16992
+ return {
16993
+ presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
16994
+ approvedText: `Apply changelog changes at ${String(payload.path ?? "CHANGELOG.md")}.`
16995
+ };
16996
+ case "youtrack.update":
16997
+ return {
16998
+ presented: `Workit decision: action — Update YouTrack issue ${String(request.payload.issueId)}${request.payload.minutes ? ` and log ${String(request.payload.minutes)}m` : ""}?`,
16999
+ approvedText: `Update YouTrack issue ${String(request.payload.issueId)}.`
17000
+ };
17001
+ case "youtrack.time":
17002
+ return {
17003
+ presented: `Workit decision: action — Log ${String(request.payload.minutes)}m to YouTrack issue ${String(request.payload.issueId)}?`,
17004
+ approvedText: `Log time to YouTrack issue ${String(request.payload.issueId)}.`
17005
+ };
17006
+ case "youtrack.meeting":
17007
+ return {
17008
+ presented: `Workit decision: action — Log meeting time to YouTrack issue ${String(request.payload.issueId)}?`,
17009
+ approvedText: `Log meeting time to YouTrack issue ${String(request.payload.issueId)}.`
17010
+ };
17011
+ default:
17012
+ return {
17013
+ presented: `Workit decision: action — Approve ${request.operation}?`,
17014
+ approvedText: `Approve ${request.operation}.`
17015
+ };
17016
+ }
17017
+ };
16575
17018
  var approvedResolvedExternalAction = (content) => {
16576
17019
  try {
16577
17020
  const value = JSON.parse(content);
@@ -16835,24 +17278,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16835
17278
  const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
16836
17279
  if (!sdd_dir)
16837
17280
  return failure("invalid_input", "sdd_dir must stay inside the workspace");
17281
+ const action = request.payload.action ?? "setup";
16838
17282
  const policy = resolveBranchPolicyFor(root);
16839
- const target_branch = request.payload.target_branch ?? policy.defaultTargetBranch;
17283
+ const target_branch = request.payload.target_branch;
17284
+ if (action !== "reapply_stash" && !target_branch)
17285
+ return failure("invalid_input", "target_branch (the working branch to create or switch to) is required for setup", {
17286
+ outcome: "not_started",
17287
+ fields: [{ path: "target_branch", reason: "required for action=setup" }]
17288
+ });
16840
17289
  const head = gitValue(root, ["rev-parse", "HEAD"]);
16841
17290
  if (!head)
16842
17291
  return failure("storage_error", "current Git commit could not be resolved");
17292
+ const dirty = Boolean((gitRaw(root, ["status", "--short"]) ?? "").trim());
17293
+ const creation = action === "reapply_stash" || !target_branch ? { base_branch: policy.defaultTargetBranch } : branchCreationBaseline(root, target_branch, policy.defaultTargetBranch);
16843
17294
  const normalized = {
16844
17295
  operation: request.operation,
16845
17296
  payload: {
16846
17297
  ...request.payload,
16847
- action: request.payload.action ?? "setup",
17298
+ action,
16848
17299
  sdd_dir,
16849
17300
  stash: request.payload.stash ?? "no",
16850
- target_branch
17301
+ ...target_branch ? { target_branch } : {}
16851
17302
  }
16852
17303
  };
16853
17304
  return success(null, null, {
16854
17305
  request: normalized,
16855
- descriptorPayload: { ...normalized.payload, resolved: { head, target_branch } }
17306
+ descriptorPayload: {
17307
+ ...normalized.payload,
17308
+ resolved: { head, dirty, ...creation }
17309
+ }
16856
17310
  });
16857
17311
  }
16858
17312
  case "git.push": {
@@ -16866,7 +17320,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16866
17320
  }) : failure("invalid_input", "git push requires a resolvable current branch");
16867
17321
  }
16868
17322
  case "git.commit": {
17323
+ const planSteps = request.payload.plan_steps;
17324
+ const planBranch = request.payload.plan_branch;
16869
17325
  const head = gitValue(root, ["rev-parse", "HEAD"]);
17326
+ if (!head)
17327
+ return failure("storage_error", "Git state could not be resolved");
17328
+ const branch = gitValue(root, ["branch", "--show-current"]);
17329
+ if (planSteps !== undefined || planBranch !== undefined) {
17330
+ if (!Array.isArray(planSteps) || planSteps.some((step) => typeof step !== "string" || !step.trim()) || new Set(planSteps).size !== planSteps.length || planSteps.length === 0 || planSteps.length > 32)
17331
+ return failure("invalid_input", "plan_steps must be 1-32 unique non-empty commit messages", {
17332
+ outcome: "not_started",
17333
+ fields: [{ path: "plan_steps", reason: "unique strings" }]
17334
+ });
17335
+ if (typeof planBranch !== "string" || !planBranch.trim() || planBranch !== branch)
17336
+ return failure("invalid_input", "plan_branch must name the current working branch", {
17337
+ outcome: "not_started",
17338
+ fields: [{ path: "plan_branch", reason: "current branch" }]
17339
+ });
17340
+ if (isProtectedBranch2(root, planBranch))
17341
+ return failure("invalid_input", "plan_branch must not be a protected branch", {
17342
+ outcome: "not_started",
17343
+ fields: [{ path: "plan_branch", reason: "protected branch" }]
17344
+ });
17345
+ return success(null, null, {
17346
+ request,
17347
+ descriptorPayload: {
17348
+ plan_steps: planSteps,
17349
+ plan_branch: planBranch,
17350
+ resolved: { head, branch, steps: planSteps }
17351
+ }
17352
+ });
17353
+ }
16870
17354
  const staged = gitRaw(root, [
16871
17355
  "diff",
16872
17356
  "--cached",
@@ -16877,7 +17361,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16877
17361
  "HEAD"
16878
17362
  ]);
16879
17363
  const paths = stagedPaths(root);
16880
- if (!head || staged === null || paths === null)
17364
+ const message = request.payload.message;
17365
+ if (typeof message !== "string" || !message)
17366
+ return failure("invalid_input", "git.commit requires a commit message", {
17367
+ outcome: "not_started",
17368
+ fields: [{ path: "message", reason: "required unless plan_steps is provided" }]
17369
+ });
17370
+ if (staged === null || paths === null)
16881
17371
  return failure("storage_error", "Git state could not be resolved");
16882
17372
  let policy;
16883
17373
  try {
@@ -16892,13 +17382,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16892
17382
  `).filter(Boolean));
16893
17383
  flavor = detected.flavor ?? "conventional";
16894
17384
  }
16895
- if (!matchCommitFlavor(request.payload.message, flavor, policy.pattern))
17385
+ if (!matchCommitFlavor(message, flavor, policy.pattern))
16896
17386
  return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
16897
17387
  return success(null, null, {
16898
17388
  request,
16899
17389
  descriptorPayload: {
16900
17390
  ...request.payload,
16901
- resolved: { head, staged: sha256(staged), paths }
17391
+ resolved: { head, branch, staged: sha256(staged), paths }
16902
17392
  }
16903
17393
  });
16904
17394
  }
@@ -17068,10 +17558,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
17068
17558
  sdd_dir: request.payload.sdd_dir ?? "docs",
17069
17559
  workspace_root: root
17070
17560
  });
17071
- return result.error ? unknown2(request.operation) : success(null, null, result);
17561
+ if ("error" in result) {
17562
+ if (result.phase === "preflight")
17563
+ return failure("invalid_input", result.error, {
17564
+ outcome: "not_started",
17565
+ operation: request.operation
17566
+ });
17567
+ return unknown2(request.operation);
17568
+ }
17569
+ return success(null, null, result);
17072
17570
  }
17073
17571
  case "git.commit": {
17074
- const result = run3(root, ["commit", "-m", request.payload.message]);
17572
+ const message = request.payload.message;
17573
+ if (typeof message !== "string" || !message)
17574
+ return failure("invalid_input", "git.commit requires a commit message", {
17575
+ outcome: "not_started",
17576
+ fields: [{ path: "message", reason: "required" }]
17577
+ });
17578
+ const result = run3(root, ["commit", "-m", message]);
17075
17579
  return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown2(request.operation);
17076
17580
  }
17077
17581
  case "git.push": {
@@ -17244,8 +17748,34 @@ var nativeAuthority = (actor, reconciliationTokens = new WeakSet) => ({
17244
17748
  var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
17245
17749
  const store = new TaskStore(root);
17246
17750
  const selected = approvedExternalAction(store, "pi", actor, operation);
17247
- if (!selected.ok)
17751
+ if (!selected.ok) {
17752
+ const plan = planCommitBinding(store, "pi", actor, operation);
17753
+ if (plan) {
17754
+ const actionRef = externalActionRef("pi", actor, operation);
17755
+ return {
17756
+ taskId: plan.taskId,
17757
+ decisionId: plan.decisionId,
17758
+ actionRef,
17759
+ expectedRevision: plan.expectedRevision,
17760
+ expectedWorkspaceRevision: plan.expectedWorkspaceRevision,
17761
+ binding: plan.binding,
17762
+ step: plan.step,
17763
+ refresh: () => {
17764
+ const task = store.readTask(plan.taskId);
17765
+ const freshWorkspace = store.readWorkspace();
17766
+ if (!task.ok || !freshWorkspace.ok || !freshWorkspace.data)
17767
+ throw new Error("external action state changed");
17768
+ return {
17769
+ expectedRevision: task.data.revision,
17770
+ expectedWorkspaceRevision: freshWorkspace.data.revision
17771
+ };
17772
+ },
17773
+ reserveObservation: nativeExternalActionObservation(actor, actionRef, "reserve"),
17774
+ settleObservation: (outcome, revisions) => nativeExternalActionObservation(actor, actionRef, outcome, actionRef.kind === "host" ? actionRef.handle : "external-action", revisions)
17775
+ };
17776
+ }
17248
17777
  return selected;
17778
+ }
17249
17779
  const actionRef = externalActionRef("pi", actor, operation);
17250
17780
  return {
17251
17781
  taskId: selected.data.task.id,
@@ -17311,6 +17841,21 @@ var executeFamily = async (family, toolCallId, input, ctx) => {
17311
17841
  operation: "decision.record"
17312
17842
  }));
17313
17843
  const decision = parsed.data;
17844
+ const budgetIssue = workitBindingQuestionIssue([
17845
+ {
17846
+ header: `Workit decision: ${parsed.data.purpose}`,
17847
+ question: decision.binding.presented,
17848
+ options: [
17849
+ {
17850
+ label: "approved",
17851
+ description: decision.binding.displayed ?? decision.binding.approvedContent
17852
+ },
17853
+ { label: "rejected", description: "Reject this decision" }
17854
+ ]
17855
+ }
17856
+ ]);
17857
+ if (budgetIssue)
17858
+ return output(failure("invalid_input", budgetIssue));
17314
17859
  const approved = await ctx.ui.confirm("Workit decision", decision.binding.presented);
17315
17860
  if (approved !== (decision.response === "approved"))
17316
17861
  return output(failure("permission_denied", "Pi decision answer did not match the requested response"));
@@ -17363,9 +17908,17 @@ var registerWorkitTools = (pi, options = {}) => {
17363
17908
  if (!parsed.ok)
17364
17909
  return output(parsed);
17365
17910
  const context = piContext(ctx);
17366
- const resolved = resolveExternalActionRequest(ctx.cwd, parsed.data);
17911
+ let resolved = resolveExternalActionRequest(ctx.cwd, parsed.data);
17367
17912
  if (!resolved.ok)
17368
17913
  return output(resolved);
17914
+ if (resolved.data.request.operation === "git.branch_setup" && resolved.data.descriptorPayload.resolved?.dirty === true && resolved.data.descriptorPayload.stash !== "yes") {
17915
+ const upgraded = resolveExternalActionRequest(ctx.cwd, {
17916
+ ...parsed.data,
17917
+ payload: { ...parsed.data.payload, stash: "yes" }
17918
+ });
17919
+ if (upgraded.ok)
17920
+ resolved = upgraded;
17921
+ }
17369
17922
  if (resolved.data.request.operation === "context.read")
17370
17923
  return output(await executeResolvedExternalAction(resolved.data, ctx.cwd));
17371
17924
  if (!ctx.isProjectTrusted())
@@ -17383,6 +17936,22 @@ var registerWorkitTools = (pi, options = {}) => {
17383
17936
  ...context,
17384
17937
  nativeAuthority: nativeAuthority(actor, reconciliationTokens)
17385
17938
  });
17939
+ const localOperation = [
17940
+ "git.branch_setup",
17941
+ "git.commit",
17942
+ "git.push",
17943
+ "hosting.pull_request",
17944
+ "changelog.apply"
17945
+ ].includes(resolved.data.request.operation);
17946
+ if (localOperation) {
17947
+ const writer = assertLocalExternalActionWriter(ctx.cwd, { host: "pi", actor });
17948
+ if (!writer.ok)
17949
+ return output(failure("needs_input", "writer ownership is required before this action", {
17950
+ outcome: "not_started",
17951
+ operation: resolved.data.request.operation,
17952
+ guidance: "Acquire checkout writer ownership first (writer.acquire) and retry the action."
17953
+ }));
17954
+ }
17386
17955
  const descriptor = externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload);
17387
17956
  const priorRequest = priorExternalAction(store, "pi", actor, resolved.data.request.operation, resolved.data.request.payload);
17388
17957
  if (priorRequest.ok && priorRequest.data.entry.data.consumption?.state === "uncertain") {
@@ -17421,8 +17990,18 @@ var registerWorkitTools = (pi, options = {}) => {
17421
17990
  }
17422
17991
  if (priorRequest.ok && priorRequest.data.entry.data.consumption !== null)
17423
17992
  return output(failure("permission_denied", "external action was already settled"));
17993
+ const drift = resolved.data.request.operation === "git.branch_setup" ? priorResolvedDrift(store, "pi", actor, resolved.data.request.operation, resolved.data.request.payload, resolved.data.descriptorPayload.resolved) : success(null, null, null);
17994
+ if (!drift.ok)
17995
+ return output(drift);
17424
17996
  const existing = approvedExternalAction(store, "pi", actor, descriptor);
17425
17997
  if (!existing.ok) {
17998
+ const commitMessage = resolved.data.request.operation === "git.commit" ? resolved.data.request.payload.message : undefined;
17999
+ const currentBranch = resolved.data.descriptorPayload.resolved?.branch;
18000
+ const plan = typeof commitMessage === "string" && commitMessage ? approvedPlanCommit(store, "pi", actor, commitMessage) : null;
18001
+ if (plan?.ok && plan.data.branch === currentBranch) {
18002
+ const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
18003
+ return output(result);
18004
+ }
17426
18005
  const prior = externalActionState(store, "pi", actor, descriptor);
17427
18006
  if (prior.ok && prior.data.entry.data.consumption !== null)
17428
18007
  return output(failure("permission_denied", "external action was already settled"));
@@ -17434,7 +18013,8 @@ var registerWorkitTools = (pi, options = {}) => {
17434
18013
  if (candidates.length !== 1)
17435
18014
  return output(failure("permission_denied", "external action requires exactly one active Pi task"));
17436
18015
  const task = candidates[0];
17437
- const approved = await ctx.ui.confirm("Workit external action", descriptor);
18016
+ const question = actionProposalQuestion(resolved.data.request, resolved.data.descriptorPayload);
18017
+ const approved = await ctx.ui.confirm(question.presented, question.approvedText);
17438
18018
  if (!approved)
17439
18019
  return output(failure("permission_denied", "external action was not approved"));
17440
18020
  const currentWorkspace = store.readWorkspace();
@@ -17450,8 +18030,9 @@ var registerWorkitTools = (pi, options = {}) => {
17450
18030
  taskId: task.id,
17451
18031
  workspaceId: currentWorkspace.data.id,
17452
18032
  scope: task.intent.data.scope,
17453
- presented: `Approve ${descriptor}`,
18033
+ presented: question.presented,
17454
18034
  approvedContent: descriptor,
18035
+ displayed: question.approvedText,
17455
18036
  contentRefs: []
17456
18037
  },
17457
18038
  response: "approved",
@@ -17460,6 +18041,9 @@ var registerWorkitTools = (pi, options = {}) => {
17460
18041
  if (!decision.ok)
17461
18042
  return output(decision);
17462
18043
  }
18044
+ const planSteps = resolved.data.descriptorPayload.plan_steps;
18045
+ if (resolved.data.request.operation === "git.commit" && Array.isArray(planSteps))
18046
+ return output(success(null, null, { plan_commits: planSteps.length }));
17463
18047
  const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
17464
18048
  return output(result);
17465
18049
  }
@@ -17468,7 +18052,7 @@ var registerWorkitTools = (pi, options = {}) => {
17468
18052
  var enforceNativeWriter = (event, ctx) => {
17469
18053
  if (event.toolName === "bash") {
17470
18054
  const command = event.input?.command;
17471
- const route = typeof command === "string" ? shellRouteIntent(command) : null;
18055
+ const route = typeof command === "string" ? shouldDenyShellRoute(ctx.cwd, command) : null;
17472
18056
  if (route)
17473
18057
  return {
17474
18058
  block: true,