@brainervirus/workit-pi 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,6 +7796,7 @@ var decisionSchema = object({
7796
7796
  scope: scopeSchema,
7797
7797
  presented: text,
7798
7798
  approvedContent: text,
7799
+ displayed: text.optional(),
7799
7800
  contentRefs: array(refSchema)
7800
7801
  }).strict(),
7801
7802
  digest,
@@ -7808,6 +7809,27 @@ var decisionSchema = object({
7808
7809
  actionRef: refSchema
7809
7810
  }).strict().nullable()
7810
7811
  }).strict();
7812
+ var BINDING_QUESTION_BUDGET = 300;
7813
+ var bindingQuestionHeader = /^Workit decision: (design|action|limitation|preference)$/;
7814
+ var workitBindingQuestionIssue = (questions) => {
7815
+ if (!Array.isArray(questions) || questions.length !== 1)
7816
+ return null;
7817
+ const question = questions[0];
7818
+ if (!question || typeof question !== "object")
7819
+ return null;
7820
+ if (typeof question.header !== "string" || !bindingQuestionHeader.test(question.header.trim()))
7821
+ return null;
7822
+ 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.`;
7823
+ if (typeof question.question === "string" && question.question.length > BINDING_QUESTION_BUDGET)
7824
+ return over("the question text", question.question);
7825
+ if (Array.isArray(question.options))
7826
+ for (const option of question.options) {
7827
+ const description = option.description;
7828
+ if (typeof description === "string" && description.length > BINDING_QUESTION_BUDGET)
7829
+ return over("an option description", description);
7830
+ }
7831
+ return null;
7832
+ };
7811
7833
  var findingSchema = object({
7812
7834
  claim: text,
7813
7835
  consequence: text,
@@ -7874,6 +7896,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
7874
7896
  path: ["decisionId"]
7875
7897
  });
7876
7898
  });
7899
+ var runtimeSchema = object({
7900
+ createdWith: text.nullable(),
7901
+ updatedWith: text
7902
+ }).strict();
7877
7903
  var taskRecordSchema = object({
7878
7904
  schemaVersion: literal(1),
7879
7905
  id,
@@ -7887,6 +7913,8 @@ var taskRecordSchema = object({
7887
7913
  status: _enum(["active", "paused", "closed"]),
7888
7914
  closure: closureSchema.nullable(),
7889
7915
  progress: progressSchema,
7916
+ pauseReason: text.nullable().optional(),
7917
+ runtime: runtimeSchema.optional(),
7890
7918
  assessments: array(entrySchema(assessmentSchema)),
7891
7919
  policy: policySchema.nullable(),
7892
7920
  policyChanges: array(policyChangeSchema),
@@ -7902,6 +7930,7 @@ var workspaceRecordSchema = object({
7902
7930
  id,
7903
7931
  revision,
7904
7932
  root: nonEmpty,
7933
+ runtime: runtimeSchema.optional(),
7905
7934
  writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
7906
7935
  }).strict();
7907
7936
  var capabilitySchema = object({
@@ -7929,6 +7958,9 @@ var taskSummarySchema = object({
7929
7958
  id,
7930
7959
  revision,
7931
7960
  workspaceRevision: revision,
7961
+ createdAt: utc,
7962
+ updatedAt: utc,
7963
+ runtime: runtimeSchema.nullable(),
7932
7964
  objective: text,
7933
7965
  status: _enum(["active", "paused", "closed"]),
7934
7966
  closure: closureSchema.nullable(),
@@ -7999,7 +8031,7 @@ var taskOperations = {
7999
8031
  expectedWorkspaceRevision: revision.optional(),
8000
8032
  outcome: outcomeSchema,
8001
8033
  summary: text,
8002
- decisionIds: array(id)
8034
+ decisionIds: array(id).optional()
8003
8035
  })
8004
8036
  };
8005
8037
  var policyOperations = {
@@ -8091,14 +8123,15 @@ var writerOperations = {
8091
8123
  ...taskId,
8092
8124
  expectedRevision: revision.optional(),
8093
8125
  expectedWorkspaceRevision: revision.optional(),
8094
- workerId: nullableId.optional()
8126
+ workerId: nullableId.optional(),
8127
+ reason: text.optional()
8095
8128
  }),
8096
8129
  release: operation({
8097
8130
  action: literal("release"),
8098
8131
  ...taskId,
8099
8132
  expectedRevision: revision.optional(),
8100
8133
  expectedWorkspaceRevision: revision.optional(),
8101
- reason: text
8134
+ reason: text.optional()
8102
8135
  })
8103
8136
  };
8104
8137
  var stateOperations = {
@@ -8305,7 +8338,26 @@ function requirementId(input) {
8305
8338
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
8306
8339
  import * as fs4 from "node:fs";
8307
8340
  import { hostname } from "node:os";
8308
- import path3 from "node:path";
8341
+ import path4 from "node:path";
8342
+
8343
+ // packages/workit-core/src/core/package-root.ts
8344
+ import { existsSync } from "node:fs";
8345
+ import path from "node:path";
8346
+ import { fileURLToPath } from "node:url";
8347
+ var packageRoot = () => {
8348
+ let dir = path.dirname(fileURLToPath(import.meta.url));
8349
+ while (true) {
8350
+ const parent = path.dirname(dir);
8351
+ if (existsSync(path.join(dir, "package.json")) || parent === dir)
8352
+ return dir;
8353
+ dir = parent;
8354
+ }
8355
+ };
8356
+ var assetRoot = () => {
8357
+ const root = packageRoot();
8358
+ const assets = path.join(root, "assets");
8359
+ return existsSync(assets) ? assets : root;
8360
+ };
8309
8361
 
8310
8362
  // node_modules/@openclaw/fs-safe/dist/errors.js
8311
8363
  var OPERATIONAL_CODES = new Set([
@@ -8356,7 +8408,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
8356
8408
  // node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
8357
8409
  import { randomBytes } from "node:crypto";
8358
8410
  import fsSync from "node:fs";
8359
- import path from "node:path";
8411
+ import path2 from "node:path";
8360
8412
 
8361
8413
  // node_modules/@openclaw/fs-safe/dist/bounded-read.js
8362
8414
  import fs from "node:fs";
@@ -8446,13 +8498,13 @@ ${ownershipToken}
8446
8498
  return { raw, ownershipToken };
8447
8499
  }
8448
8500
  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)) {
8501
+ const resolved = path2.resolve(lockPath);
8502
+ const lexicalRelative = path2.relative(lockRoot.rootDir, resolved);
8503
+ const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(lexicalRelative) ? lexicalRelative : path2.relative(lockRoot.rootReal, resolved);
8504
+ if (!relative || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
8453
8505
  throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
8454
8506
  }
8455
- return relative.split(path.sep).join(path.posix.sep);
8507
+ return relative.split(path2.sep).join(path2.posix.sep);
8456
8508
  }
8457
8509
  function parseSidecarLockPayload(raw, parser) {
8458
8510
  if (parser) {
@@ -8599,7 +8651,7 @@ function getFsSafeLockConfig() {
8599
8651
 
8600
8652
  // node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
8601
8653
  import fs3 from "node:fs";
8602
- import path2 from "node:path";
8654
+ import path3 from "node:path";
8603
8655
 
8604
8656
  // node_modules/@openclaw/fs-safe/dist/timing.js
8605
8657
  function sleepSync(ms) {
@@ -8690,26 +8742,26 @@ function canonicalLockParentSync(parent) {
8690
8742
  return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
8691
8743
  }
8692
8744
  function normalizeTargetPath(targetPath) {
8693
- const resolved = path2.resolve(targetPath);
8694
- fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
8745
+ const resolved = path3.resolve(targetPath);
8746
+ fs3.mkdirSync(path3.dirname(resolved), { recursive: true });
8695
8747
  try {
8696
- return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
8748
+ return path3.join(canonicalLockParentSync(path3.dirname(resolved)), path3.basename(resolved));
8697
8749
  } catch {
8698
8750
  return resolved;
8699
8751
  }
8700
8752
  }
8701
8753
  function boundedLockPath(lockPath, lockRoot) {
8702
- const resolved = path2.resolve(lockPath);
8754
+ const resolved = path3.resolve(lockPath);
8703
8755
  if (!lockRoot)
8704
8756
  return resolved;
8705
8757
  relativeSidecarLockPath(lockRoot, resolved);
8706
- const parent = path2.dirname(resolved);
8758
+ const parent = path3.dirname(resolved);
8707
8759
  const parentReal = canonicalLockParentSync(parent);
8708
- const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
8709
- if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
8760
+ const parentRelative = path3.relative(lockRoot.rootReal, parentReal);
8761
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(parentRelative)) {
8710
8762
  throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
8711
8763
  }
8712
- return path2.join(parentReal, path2.basename(resolved));
8764
+ return path3.join(parentReal, path3.basename(resolved));
8713
8765
  }
8714
8766
  function defaultShouldReclaim(snapshot, staleMs, nowMs) {
8715
8767
  const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
@@ -8922,6 +8974,33 @@ var metadataLockSchema = object({
8922
8974
  nonce: string2().min(1)
8923
8975
  }).strict();
8924
8976
  var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
8977
+ var cachedRuntimeVersion = null;
8978
+ var runtimeVersion = () => {
8979
+ if (cachedRuntimeVersion === null) {
8980
+ try {
8981
+ const pkg = JSON.parse(fs4.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
8982
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
8983
+ } catch {
8984
+ cachedRuntimeVersion = "0.0.0";
8985
+ }
8986
+ }
8987
+ return cachedRuntimeVersion;
8988
+ };
8989
+ var stampNew = (record) => ({
8990
+ createdWith: record.runtime?.createdWith ?? null,
8991
+ updatedWith: runtimeVersion()
8992
+ });
8993
+ var isNewerVersion = (candidate, current) => {
8994
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
8995
+ const [next, now] = [parts(candidate), parts(current)];
8996
+ for (let index = 0;index < 3; index += 1) {
8997
+ const left = next[index] ?? 0;
8998
+ const right = now[index] ?? 0;
8999
+ if (left !== right)
9000
+ return left > right;
9001
+ }
9002
+ return false;
9003
+ };
8925
9004
  var jsonBytes = (value) => `${canonicalJson(value)}
8926
9005
  `;
8927
9006
  var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
@@ -8948,12 +9027,12 @@ var sameMetadataLock = (left, right) => {
8948
9027
  class TaskStore {
8949
9028
  root;
8950
9029
  constructor(root) {
8951
- this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path3.resolve(root);
9030
+ this.root = fs4.existsSync(root) ? fs4.realpathSync(root) : path4.resolve(root);
8952
9031
  }
8953
9032
  readTask(taskId) {
8954
9033
  if (!validId(taskId))
8955
9034
  return failure("invalid_input", "task ID is invalid", { taskId });
8956
- const result = this.readRecord(path3.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
9035
+ const result = this.readRecord(path4.join(this.tasksDir, `${taskId}.json`), taskRecordSchema);
8957
9036
  if (!result.exists)
8958
9037
  return failure("not_found", "task not found", { taskId });
8959
9038
  if (result.result.ok && result.result.data.id !== taskId)
@@ -8987,7 +9066,7 @@ class TaskStore {
8987
9066
  });
8988
9067
  const tasks = [];
8989
9068
  for (const name of names.sort()) {
8990
- const item = this.readRecord(path3.join(this.tasksDir, name), taskRecordSchema);
9069
+ const item = this.readRecord(path4.join(this.tasksDir, name), taskRecordSchema);
8991
9070
  if (!item.exists)
8992
9071
  continue;
8993
9072
  if (!item.result.ok)
@@ -9021,7 +9100,7 @@ class TaskStore {
9021
9100
  if (!current.ok)
9022
9101
  return current;
9023
9102
  if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
9024
- return failure("revision_conflict", "workspace revision does not match", {
9103
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
9025
9104
  expectedWorkspaceRevision: value.expectedWorkspaceRevision,
9026
9105
  actualWorkspaceRevision: current.data?.revision ?? null
9027
9106
  });
@@ -9033,11 +9112,17 @@ class TaskStore {
9033
9112
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
9034
9113
  if (current.data && !previousWorkspaceBytes)
9035
9114
  return failure("storage_error", "workspace snapshot disappeared during creation");
9036
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
9115
+ const workspace = current.data ? {
9116
+ ...current.data,
9117
+ revision: newRevision(),
9118
+ root: this.root,
9119
+ runtime: stampNew(current.data)
9120
+ } : {
9037
9121
  schemaVersion: SCHEMA_VERSION,
9038
9122
  id: newId(),
9039
9123
  revision: newRevision(),
9040
9124
  root: this.root,
9125
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
9041
9126
  writer: null
9042
9127
  };
9043
9128
  const timestamp = value.now ?? now();
@@ -9059,6 +9144,7 @@ class TaskStore {
9059
9144
  status: "active",
9060
9145
  closure: null,
9061
9146
  progress: { summary: "", nextAction: null, blockers: [] },
9147
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
9062
9148
  assessments: [],
9063
9149
  policy: null,
9064
9150
  policyChanges: [],
@@ -9089,7 +9175,7 @@ class TaskStore {
9089
9175
  if (!current.ok)
9090
9176
  return current;
9091
9177
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
9092
- return failure("revision_conflict", "workspace revision does not match", {
9178
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
9093
9179
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
9094
9180
  actualWorkspaceRevision: current.data?.revision ?? null
9095
9181
  });
@@ -9097,11 +9183,17 @@ class TaskStore {
9097
9183
  if (current.data && !previousWorkspaceBytes)
9098
9184
  return failure("storage_error", "workspace snapshot disappeared during import");
9099
9185
  const timestamp = input.now ?? now();
9100
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
9186
+ const workspace = current.data ? {
9187
+ ...current.data,
9188
+ revision: newRevision(),
9189
+ root: this.root,
9190
+ runtime: stampNew(current.data)
9191
+ } : {
9101
9192
  schemaVersion: SCHEMA_VERSION,
9102
9193
  id: input.workspaceId ?? newId(),
9103
9194
  revision: newRevision(),
9104
9195
  root: this.root,
9196
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
9105
9197
  writer: null
9106
9198
  };
9107
9199
  const task = {
@@ -9109,7 +9201,11 @@ class TaskStore {
9109
9201
  workspaceId: workspace.id,
9110
9202
  revision: newRevision(),
9111
9203
  createdAt: timestamp,
9112
- updatedAt: timestamp
9204
+ updatedAt: timestamp,
9205
+ runtime: {
9206
+ createdWith: input.task.runtime?.createdWith ?? null,
9207
+ updatedWith: runtimeVersion()
9208
+ }
9113
9209
  };
9114
9210
  const validTask = taskRecordSchema.safeParse(task);
9115
9211
  if (!validTask.success)
@@ -9158,7 +9254,8 @@ class TaskStore {
9158
9254
  workspaceId: current.data.workspaceId,
9159
9255
  createdAt: current.data.createdAt,
9160
9256
  revision: context.revision,
9161
- updatedAt: context.now
9257
+ updatedAt: context.now,
9258
+ runtime: stampNew(current.data)
9162
9259
  };
9163
9260
  const valid = taskRecordSchema.safeParse(record);
9164
9261
  if (!valid.success)
@@ -9192,7 +9289,8 @@ class TaskStore {
9192
9289
  ...changed.data,
9193
9290
  id: current.data.id,
9194
9291
  root: this.root,
9195
- revision: context.revision
9292
+ revision: context.revision,
9293
+ runtime: stampNew(current.data)
9196
9294
  };
9197
9295
  const valid = workspaceRecordSchema.safeParse(record);
9198
9296
  if (!valid.success)
@@ -9235,7 +9333,8 @@ class TaskStore {
9235
9333
  ...changedWorkspace.data,
9236
9334
  id: workspace.data.id,
9237
9335
  root: this.root,
9238
- revision: workspaceContext.revision
9336
+ revision: workspaceContext.revision,
9337
+ runtime: stampNew(workspace.data)
9239
9338
  });
9240
9339
  if (!nextWorkspace.success)
9241
9340
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -9260,7 +9359,8 @@ class TaskStore {
9260
9359
  workspaceId: task.data.workspaceId,
9261
9360
  createdAt: task.data.createdAt,
9262
9361
  revision: taskContext.revision,
9263
- updatedAt: taskContext.now
9362
+ updatedAt: taskContext.now,
9363
+ runtime: stampNew(task.data)
9264
9364
  });
9265
9365
  if (!nextTask.success) {
9266
9366
  this.markUncertain(nextWorkspace.data);
@@ -9287,7 +9387,7 @@ class TaskStore {
9287
9387
  if (match)
9288
9388
  candidates.push({
9289
9389
  target: match[1],
9290
- path: path3.join(this.recoveryDir, name),
9390
+ path: path4.join(this.recoveryDir, name),
9291
9391
  digest: match[3]
9292
9392
  });
9293
9393
  }
@@ -9480,7 +9580,8 @@ class TaskStore {
9480
9580
  const value = {
9481
9581
  ...workspace,
9482
9582
  writer: { ...workspace.writer, state: "uncertain" },
9483
- revision: newRevision()
9583
+ revision: newRevision(),
9584
+ runtime: stampNew(workspace)
9484
9585
  };
9485
9586
  this.replaceSnapshot(this.workspacePath, value, workspace);
9486
9587
  }
@@ -9558,7 +9659,7 @@ class TaskStore {
9558
9659
  }
9559
9660
  fs4.renameSync(temporary, file);
9560
9661
  temporary = undefined;
9561
- this.fsyncDirectory(path3.dirname(file));
9662
+ this.fsyncDirectory(path4.dirname(file));
9562
9663
  return success(null, null, value);
9563
9664
  } catch (error) {
9564
9665
  return failure("storage_error", `snapshot replacement failed: ${String(error)}`, {
@@ -9572,9 +9673,9 @@ class TaskStore {
9572
9673
  }
9573
9674
  }
9574
9675
  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`);
9676
+ const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
9677
+ const id = target === "task" ? path4.basename(file, ".json") : "workspace";
9678
+ const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
9578
9679
  if (fs4.existsSync(destination)) {
9579
9680
  if (digestBytes(fs4.readFileSync(destination)) === digestBytes(bytes))
9580
9681
  return;
@@ -9652,7 +9753,7 @@ class TaskStore {
9652
9753
  for (const name of fs4.readdirSync(this.tasksDir)) {
9653
9754
  if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
9654
9755
  return null;
9655
- const parsed = this.parseBytes(fs4.readFileSync(path3.join(this.tasksDir, name)), taskRecordSchema);
9756
+ const parsed = this.parseBytes(fs4.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
9656
9757
  if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
9657
9758
  return null;
9658
9759
  ids.add(parsed.data.workspaceId);
@@ -9694,7 +9795,12 @@ class TaskStore {
9694
9795
  if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
9695
9796
  return failure("unsupported_version", "unsupported snapshot schema version");
9696
9797
  const parsed = schema.safeParse(value);
9697
- return parsed.success ? success(null, null, parsed.data) : failure("recovery_required", "snapshot does not satisfy its schema");
9798
+ if (parsed.success)
9799
+ return success(null, null, parsed.data);
9800
+ const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
9801
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
9802
+ return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
9803
+ return failure("recovery_required", "snapshot does not satisfy its schema");
9698
9804
  }
9699
9805
  processStart(pid) {
9700
9806
  try {
@@ -9704,43 +9810,98 @@ class TaskStore {
9704
9810
  }
9705
9811
  }
9706
9812
  conflict(expected, actual) {
9707
- return failure("revision_conflict", "snapshot revision does not match", {
9813
+ return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
9708
9814
  expectedRevision: expected,
9709
9815
  actualRevision: actual
9710
9816
  });
9711
9817
  }
9712
9818
  taskPath(taskId) {
9713
- return path3.join(this.tasksDir, `${taskId}.json`);
9819
+ return path4.join(this.tasksDir, `${taskId}.json`);
9714
9820
  }
9715
9821
  get workitDir() {
9716
- return path3.join(this.root, ".workit");
9822
+ return path4.join(this.root, ".workit");
9717
9823
  }
9718
9824
  get tasksDir() {
9719
- return path3.join(this.workitDir, "tasks");
9825
+ return path4.join(this.workitDir, "tasks");
9720
9826
  }
9721
9827
  get recoveryDir() {
9722
- return path3.join(this.workitDir, "recovery");
9828
+ return path4.join(this.workitDir, "recovery");
9723
9829
  }
9724
9830
  get workspacePath() {
9725
- return path3.join(this.workitDir, "workspace.json");
9831
+ return path4.join(this.workitDir, "workspace.json");
9726
9832
  }
9727
9833
  get lockPath() {
9728
- return path3.join(this.workitDir, "metadata.lock");
9834
+ return path4.join(this.workitDir, "metadata.lock");
9729
9835
  }
9730
9836
  get gitignorePath() {
9731
- return path3.join(this.workitDir, ".gitignore");
9837
+ return path4.join(this.workitDir, ".gitignore");
9732
9838
  }
9733
9839
  }
9840
+ // packages/workit-core/src/core/workers.ts
9841
+ var same = (left, right) => {
9842
+ try {
9843
+ return canonicalJson(left) === canonicalJson(right);
9844
+ } catch {
9845
+ return false;
9846
+ }
9847
+ };
9848
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
9849
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
9850
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
9851
+ var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
9852
+ const code = char.charCodeAt(0);
9853
+ return code < 32 || code === 127;
9854
+ }) && !value.split(/[\\/]/).includes("..");
9855
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
9856
+ var workerIdOf = (value) => value.workerId ?? null;
9857
+ function assertProductWriteAllowed(input) {
9858
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
9859
+ return failure("invalid_input", "invalid product write path");
9860
+ if (!input.store)
9861
+ return failure("permission_denied", "write authorization requires core state");
9862
+ const task = input.store.readTask(input.task.id);
9863
+ if (!task.ok)
9864
+ return task;
9865
+ const workspace = input.store.readWorkspace();
9866
+ if (!workspace.ok)
9867
+ return workspace;
9868
+ if (!workspace.data)
9869
+ return failure("not_found", "workspace not found");
9870
+ const workspaceRecord = workspace.data;
9871
+ if (task.data.status !== "active")
9872
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
9873
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
9874
+ return failure("recovery_required", "task and workspace bindings are invalid");
9875
+ const writer = workspaceRecord.writer;
9876
+ if (!writer)
9877
+ return failure("permission_denied", "checkout has no writer owner");
9878
+ if (writer.state === "uncertain")
9879
+ return failure("recovery_required", "checkout writer requires recovery", {
9880
+ owner: writer.owner
9881
+ });
9882
+ const caller = callerValue(input.caller);
9883
+ const workerId = workerIdOf(input.caller);
9884
+ const owner = writer.owner;
9885
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
9886
+ return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
9887
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
9888
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
9889
+ return failure("permission_denied", "only implementers can own product writes");
9890
+ 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))))
9891
+ return failure("recovery_required", "worker session is not an active writer");
9892
+ return success(null, workspaceRecord.revision, owner);
9893
+ }
9894
+
9734
9895
  // packages/workit-core/src/core/task-evaluation.ts
9735
9896
  import { createHash as createHash4 } from "node:crypto";
9736
9897
  import * as fs6 from "node:fs";
9737
9898
  import { spawnSync } from "node:child_process";
9738
- import path5 from "node:path";
9899
+ import path6 from "node:path";
9739
9900
 
9740
9901
  // packages/workit-core/src/core/authority.ts
9741
9902
  import { createHash as createHash3 } from "node:crypto";
9742
9903
  import * as fs5 from "node:fs";
9743
- import path4 from "node:path";
9904
+ import path5 from "node:path";
9744
9905
  var verifiedAuthorities = new WeakMap;
9745
9906
  var trustedAuthority = (kind, provenance, expected, binding, caller) => {
9746
9907
  const token = {};
@@ -9867,20 +10028,31 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
9867
10028
  continue;
9868
10029
  if (!reference.digest)
9869
10030
  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");
10031
+ const target = path5.resolve(root, reference.path);
10032
+ if (target !== root && !target.startsWith(`${root}${path5.sep}`))
10033
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
10034
+ fields: [
10035
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
10036
+ ]
10037
+ });
9873
10038
  try {
9874
- const relative = path4.relative(root, target);
10039
+ const relative = path5.relative(root, target);
9875
10040
  let current = root;
9876
- for (const segment of relative.split(path4.sep)) {
10041
+ for (const segment of relative.split(path5.sep)) {
9877
10042
  if (!segment)
9878
10043
  continue;
9879
- current = path4.join(current, segment);
10044
+ current = path5.join(current, segment);
9880
10045
  const stat = fs5.lstatSync(current);
9881
10046
  const real = fs5.realpathSync(current);
9882
- if (real !== root && !real.startsWith(`${root}${path4.sep}`))
9883
- return failure("invalid_input", "document reference escapes checkout");
10047
+ if (real !== root && !real.startsWith(`${root}${path5.sep}`))
10048
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
10049
+ fields: [
10050
+ {
10051
+ path: "contentRefs.path",
10052
+ reason: `${reference.path} resolves outside the checkout`
10053
+ }
10054
+ ]
10055
+ });
9884
10056
  if (stat.isSymbolicLink())
9885
10057
  return failure("invalid_input", "document reference uses a symlink");
9886
10058
  if (current === target && !stat.isFile())
@@ -10227,14 +10399,14 @@ var verifyDecisionContent = verifyContentRefs;
10227
10399
 
10228
10400
  // packages/workit-core/src/core/task-evaluation.ts
10229
10401
  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("/") || ".";
10402
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
10403
+ var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
10232
10404
  var canonicalPath = (value) => {
10233
10405
  if (typeof value !== "string" || !value)
10234
10406
  return null;
10235
10407
  if (value.includes("\\") || value.split("/").includes(".."))
10236
10408
  return null;
10237
- const normalized = path5.posix.normalize(value);
10409
+ const normalized = path6.posix.normalize(value);
10238
10410
  if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
10239
10411
  return null;
10240
10412
  return normalized === "." ? "." : normalized.replace(/\/$/, "");
@@ -10282,14 +10454,14 @@ var gitPaths = (root) => {
10282
10454
  git: /not a git repository/i.test(stderr) ? false : true
10283
10455
  };
10284
10456
  }
10285
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")) : [];
10457
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
10286
10458
  const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
10287
10459
  if (stagedDeleted.status !== 0) {
10288
10460
  const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
10289
10461
  if (!/not a git repository/i.test(stderr))
10290
10462
  return { paths, uncertain: true, git: true };
10291
10463
  } else if (stagedDeleted.stdout) {
10292
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path5.sep).join("/")));
10464
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
10293
10465
  }
10294
10466
  return { paths: [...new Set(paths)], uncertain: false, git: true };
10295
10467
  };
@@ -10304,7 +10476,7 @@ var walk = (root, directory, output) => {
10304
10476
  for (const name of names) {
10305
10477
  if (name === ".git" || name === ".workit")
10306
10478
  continue;
10307
- const target = path5.join(directory, name);
10479
+ const target = path6.join(directory, name);
10308
10480
  let stat;
10309
10481
  try {
10310
10482
  stat = fs6.lstatSync(target);
@@ -10322,17 +10494,17 @@ var walk = (root, directory, output) => {
10322
10494
  var scopeRoots = (root, scope) => {
10323
10495
  const roots = [];
10324
10496
  for (const item of scope.paths.length ? scope.paths : ["."]) {
10325
- const target = path5.resolve(root, item);
10497
+ const target = path6.resolve(root, item);
10326
10498
  if (!inside(root, target))
10327
- return failure("invalid_input", "candidate scope escapes checkout");
10499
+ 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
10500
  let ancestor = target;
10329
10501
  while (!fs6.existsSync(ancestor) && ancestor !== root)
10330
- ancestor = path5.dirname(ancestor);
10502
+ ancestor = path6.dirname(ancestor);
10331
10503
  try {
10332
10504
  if (!inside(root, fs6.realpathSync(ancestor)))
10333
- return failure("invalid_input", "candidate scope escapes checkout");
10505
+ 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
10506
  } catch {
10335
- return failure("invalid_input", "candidate scope cannot be inspected");
10507
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
10336
10508
  }
10337
10509
  roots.push(target);
10338
10510
  }
@@ -10347,7 +10519,7 @@ function captureCandidate(root, scope, environment = []) {
10347
10519
  }
10348
10520
  const normalizedScope = canonicalScope(scope);
10349
10521
  if (!normalizedScope)
10350
- return failure("invalid_input", "candidate scope is invalid");
10522
+ 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
10523
  const roots = scopeRoots(checkout, normalizedScope);
10352
10524
  if (!roots.ok)
10353
10525
  return roots;
@@ -10379,7 +10551,7 @@ function captureCandidate(root, scope, environment = []) {
10379
10551
  for (const item of [...names].sort()) {
10380
10552
  if (!scopeMatches(item, normalizedScope))
10381
10553
  continue;
10382
- const target = path5.join(checkout, item);
10554
+ const target = path6.join(checkout, item);
10383
10555
  try {
10384
10556
  const stat = fs6.lstatSync(target);
10385
10557
  if (stat.isSymbolicLink()) {
@@ -10536,7 +10708,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
10536
10708
  return true;
10537
10709
  const reviewSession = sessionFromRef(entry.data.reviewContext);
10538
10710
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
10539
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
10711
+ const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
10540
10712
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
10541
10713
  });
10542
10714
  if (passed.length) {
@@ -10632,7 +10804,17 @@ function evaluateClosure(requestedOutcome, view) {
10632
10804
  const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
10633
10805
  if (requestedOutcome !== "stopped" && blocking.length)
10634
10806
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
10635
- requirementIds: blocking.map((item) => item.requirementId)
10807
+ requirementIds: blocking.map((item) => item.requirementId),
10808
+ requirements: blocking.map((item) => {
10809
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
10810
+ return {
10811
+ requirementId: item.requirementId,
10812
+ ruleId: requirement?.ruleId ?? "unknown",
10813
+ reason: item.reason,
10814
+ satisfaction: requirement?.satisfaction ?? "",
10815
+ dependentAction: requirement?.dependentAction ?? null
10816
+ };
10817
+ })
10636
10818
  });
10637
10819
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
10638
10820
  if (requestedOutcome === "verified" && accepted.length)
@@ -10663,7 +10845,7 @@ function evaluateClosure(requestedOutcome, view) {
10663
10845
  var METHODS = {
10664
10846
  "workit-challenge": { dimensions: ["challenge", "decisions"] },
10665
10847
  "workit-behavioral-tdd": { dimensions: ["testing"] },
10666
- "workit-review": { dimensions: ["review"] },
10848
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
10667
10849
  "workit-plan": { dimensions: ["artifacts", "continuity"] },
10668
10850
  "workit-implement": { dimensions: ["delegation"] },
10669
10851
  "workit-debug": { ruleIds: ["root-cause-investigation"] },
@@ -10725,7 +10907,18 @@ decision.record {binding with taskId/workspaceId from inspect plus presented and
10725
10907
  approvedContent} only through a native approval question asked receipt-shaped:
10726
10908
  header Workit decision: <purpose>, the same label repeated in the question
10727
10909
  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,
10910
+ rejected; ask once, never re-ask to mint a receipt. Before asking, present the
10911
+ item in the conversation (durable artifacts: complete digest plus exact path;
10912
+ inline plans: the content) and keep the question to one short scoped sentence;
10913
+ never ask to approve something unseen. An answer with custom text is steering,
10914
+ not approval: adjust, re-present, then ask a new scoped question. After the user
10915
+ approves a spec or plan, execute the remaining tasks continuously with one
10916
+ atomic commit per task and do not ask whether to continue; stop only for a new
10917
+ product decision, a failed safety or verification gate, a conflicting edit, or
10918
+ missing authority. Record the plan's commit list once through the plan-scoped
10919
+ action approval (git.commit with plan_steps and plan_branch) so each listed
10920
+ commit executes without a new question; an unlisted message needs a fresh
10921
+ approval. task.close {outcome,
10729
10922
  summary, decisionIds}. A completed native subagent run stops its bound worker
10730
10923
  by itself; if a worker strands in cancelling with its run verifiably over,
10731
10924
  repeat worker.cancel {reason} to confirm the stop. Check and review evidence
@@ -10757,7 +10950,7 @@ function reconcileResume(view, observations = []) {
10757
10950
  return candidate;
10758
10951
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
10759
10952
  const blockers = [...view.task.progress.blockers];
10760
- if (view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
10953
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
10761
10954
  blockers.push({
10762
10955
  reason: "worker state requires reconciliation",
10763
10956
  dependentAction: "resume",
@@ -10890,6 +11083,7 @@ function shellRouteIntent(command) {
10890
11083
  return null;
10891
11084
  }
10892
11085
  // packages/workit-core/src/core/external-action.ts
11086
+ import { execFileSync } from "node:child_process";
10893
11087
  var externalActionSchema = discriminatedUnion("operation", [
10894
11088
  object({
10895
11089
  operation: literal("git.branch_setup"),
@@ -10902,7 +11096,11 @@ var externalActionSchema = discriminatedUnion("operation", [
10902
11096
  }).strict(),
10903
11097
  object({
10904
11098
  operation: literal("git.commit"),
10905
- payload: object({ message: string2().min(1) }).strict()
11099
+ payload: object({
11100
+ message: string2().min(1).optional(),
11101
+ plan_steps: array(string2().min(1)).optional(),
11102
+ plan_branch: string2().min(1).optional()
11103
+ }).strict()
10906
11104
  }).strict(),
10907
11105
  object({
10908
11106
  operation: literal("git.push"),
@@ -10991,7 +11189,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
10991
11189
  payload,
10992
11190
  ...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
11191
  });
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).";
11192
+ 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
11193
  var externalActionRef = (host, actor, descriptor) => ({
10996
11194
  kind: "host",
10997
11195
  host,
@@ -11042,6 +11240,92 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
11042
11240
  return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
11043
11241
  return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
11044
11242
  };
11243
+ var planCommitDescriptor = (content) => {
11244
+ try {
11245
+ const value = JSON.parse(content);
11246
+ if (value.operation !== "git.commit" || !value.payload)
11247
+ return null;
11248
+ const steps = value.payload.plan_steps;
11249
+ const branch = value.payload.plan_branch;
11250
+ if (!Array.isArray(steps) || steps.some((step) => typeof step !== "string" || !step) || typeof branch !== "string" || !branch)
11251
+ return null;
11252
+ return { steps, branch };
11253
+ } catch {
11254
+ return null;
11255
+ }
11256
+ };
11257
+ var approvedPlanCommit = (store, host, actor, message) => {
11258
+ if (typeof message !== "string" || !message)
11259
+ return failure("invalid_input", "commit message is required");
11260
+ const listed = store.listTasks();
11261
+ const workspace = store.readWorkspace();
11262
+ if (!listed.ok || !workspace.ok)
11263
+ return failure("storage_error", "external action state is unavailable");
11264
+ if (!workspace.data)
11265
+ return failure("not_found", "workspace not found");
11266
+ const matches = listed.data.flatMap((task) => {
11267
+ if (task.status !== "active" || task.workspaceId !== workspace.data.id)
11268
+ return [];
11269
+ if (task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
11270
+ return [];
11271
+ return task.decisions.flatMap((entry) => {
11272
+ const decision = entry.data;
11273
+ if (decision.purpose !== "action" || decision.response !== "approved" || decision.revoked !== null)
11274
+ return [];
11275
+ if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
11276
+ return [];
11277
+ const plan = planCommitDescriptor(decision.binding.approvedContent);
11278
+ if (!plan)
11279
+ return [];
11280
+ const completed = task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ?? [];
11281
+ if (plan.steps[completed.length] !== message)
11282
+ return [];
11283
+ return [{ task, entry, steps: plan.steps, branch: plan.branch, completed: [...completed] }];
11284
+ });
11285
+ });
11286
+ if (matches.length !== 1)
11287
+ return failure("permission_denied", matches.length === 0 ? "no plan commit authorization matches this message" : "multiple plan authorizations match this message");
11288
+ return success(workspace.data.revision, null, matches[0]);
11289
+ };
11290
+ var commitMessageFromDescriptor = (operation) => {
11291
+ try {
11292
+ const descriptor = JSON.parse(operation);
11293
+ return descriptor.operation === "git.commit" && typeof descriptor.payload?.message === "string" ? descriptor.payload.message : undefined;
11294
+ } catch {
11295
+ return;
11296
+ }
11297
+ };
11298
+ var planCommitBinding = (store, host, actor, operation) => {
11299
+ const message = commitMessageFromDescriptor(operation);
11300
+ if (!message)
11301
+ return null;
11302
+ const plan = approvedPlanCommit(store, host, actor, message);
11303
+ if (!plan.ok)
11304
+ return null;
11305
+ let branch;
11306
+ try {
11307
+ branch = execFileSync("git", ["branch", "--show-current"], {
11308
+ cwd: store.root,
11309
+ encoding: "utf8",
11310
+ stdio: ["pipe", "pipe", "pipe"]
11311
+ }).trim();
11312
+ } catch {
11313
+ return null;
11314
+ }
11315
+ if (!branch || branch !== plan.data.branch)
11316
+ return null;
11317
+ const workspace = store.readWorkspace();
11318
+ if (!workspace.ok || !workspace.data)
11319
+ return null;
11320
+ return {
11321
+ taskId: plan.data.task.id,
11322
+ decisionId: plan.data.entry.id,
11323
+ binding: plan.data.entry.data.binding,
11324
+ expectedRevision: plan.data.task.revision,
11325
+ expectedWorkspaceRevision: workspace.data.revision,
11326
+ step: message
11327
+ };
11328
+ };
11045
11329
  var approvedExternalAction = (store, host, actor, operation) => {
11046
11330
  const selected = externalActionState(store, host, actor, operation);
11047
11331
  if (!selected.ok)
@@ -11050,6 +11334,23 @@ var approvedExternalAction = (store, host, actor, operation) => {
11050
11334
  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");
11051
11335
  return selected;
11052
11336
  };
11337
+ var priorResolvedDrift = (store, host, actor, operation, payload, currentResolved) => {
11338
+ const prior = priorExternalAction(store, host, actor, operation, payload);
11339
+ if (!prior.ok)
11340
+ return success(null, null, null);
11341
+ let baseline;
11342
+ try {
11343
+ const parsed = JSON.parse(prior.data.entry.data.binding.approvedContent);
11344
+ baseline = parsed.payload?.resolved;
11345
+ } catch {
11346
+ return success(null, null, null);
11347
+ }
11348
+ if (baseline === undefined || baseline === null)
11349
+ return success(null, null, null);
11350
+ if (canonicalJson(baseline) === canonicalJson(currentResolved))
11351
+ return success(null, null, null);
11352
+ return failure("invalid_input", "repository state changed since approval; show the current state and approve the action again", { outcome: "not_started", operation });
11353
+ };
11053
11354
  var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
11054
11355
  let steps = [];
11055
11356
  try {
@@ -11327,7 +11628,7 @@ function resolveRequirements(input) {
11327
11628
  ].join(", ")}`,
11328
11629
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
11329
11630
  before: "dependent_action",
11330
- dependentAction: "dependent-action",
11631
+ dependentAction: null,
11331
11632
  acceptanceAllowed: false
11332
11633
  }));
11333
11634
  if (signals.productChoiceOpen.value === true)
@@ -11338,7 +11639,7 @@ function resolveRequirements(input) {
11338
11639
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
11339
11640
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
11340
11641
  before: "dependent_action",
11341
- dependentAction: "dependent-action",
11642
+ dependentAction: null,
11342
11643
  acceptanceAllowed: false
11343
11644
  }));
11344
11645
  if (signals.behaviorChange.value === true)
@@ -11496,59 +11797,6 @@ function diffPolicy(previous, next, reason, now) {
11496
11797
  return parsed.data;
11497
11798
  }
11498
11799
 
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
11800
  // packages/workit-core/src/core/task-engine.ts
11553
11801
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
11554
11802
  kind,
@@ -11679,15 +11927,20 @@ var applyWorkerLifecycle = (input) => {
11679
11927
  return failure("invalid_transition", "worker has not started");
11680
11928
  if (input.state === "running" && entry.data.state === "stopped")
11681
11929
  return failure("invalid_transition", "stopped worker cannot run again");
11930
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
11682
11931
  const nextEntry = {
11683
11932
  ...entry,
11684
11933
  recordedAt: input.now ?? entry.recordedAt,
11685
11934
  provenance: authority.provenance,
11686
- data: { ...entry.data, state: input.state, session: input.session }
11935
+ data: {
11936
+ ...entry.data,
11937
+ state: terminalCancel ? "cancelling" : input.state,
11938
+ session: input.session
11939
+ }
11687
11940
  };
11688
11941
  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
11942
  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)
11943
+ if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
11691
11944
  return success(task.data.revision, workspace.data.revision, entry);
11692
11945
  const changed = input.store.mutateTaskAndWorkspace({
11693
11946
  taskId: task.data.id,
@@ -12057,7 +12310,7 @@ class WorkitCore {
12057
12310
  activeWorkerBlocker(task, workspace) {
12058
12311
  if (workspace.writer)
12059
12312
  return failure("recovery_required", "writer ownership must be released first");
12060
- if (task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
12313
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
12061
12314
  return failure("recovery_required", "worker state requires reconciliation");
12062
12315
  return null;
12063
12316
  }
@@ -12294,11 +12547,6 @@ class WorkitCore {
12294
12547
  if (helper && !helper.ok)
12295
12548
  return helper;
12296
12549
  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
12550
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
12303
12551
  return failure("invalid_input", "missing or skipped evidence requires a reason");
12304
12552
  if (evidence.kind === "review") {
@@ -12315,6 +12563,11 @@ class WorkitCore {
12315
12563
  evidence.beforeCandidateId ??= null;
12316
12564
  evidence.candidateId ??= null;
12317
12565
  }
12566
+ if (helper?.ok) {
12567
+ const assignment = helper.data.data.assignment;
12568
+ 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))
12569
+ return failure("permission_denied", "evidence is outside the worker assignment");
12570
+ }
12318
12571
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
12319
12572
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
12320
12573
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -13210,6 +13463,9 @@ class WorkitCore {
13210
13463
  id: task.id,
13211
13464
  revision: task.revision,
13212
13465
  workspaceRevision: workspace.data.revision,
13466
+ createdAt: task.createdAt,
13467
+ updatedAt: task.updatedAt,
13468
+ runtime: task.runtime ?? null,
13213
13469
  objective: task.intent.data.objective,
13214
13470
  status: task.status,
13215
13471
  closure: task.closure,
@@ -13295,7 +13551,7 @@ class WorkitCore {
13295
13551
  const base = reconcileResume(effectiveView);
13296
13552
  if (!base.ok)
13297
13553
  return base;
13298
- const needsReconciliation = view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
13554
+ const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
13299
13555
  const blockers = [...base.data.blockers];
13300
13556
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
13301
13557
  blockers.push({
@@ -13362,7 +13618,8 @@ class WorkitCore {
13362
13618
  ...current,
13363
13619
  status,
13364
13620
  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
13621
+ progress: current.progress,
13622
+ pauseReason: status === "paused" ? input.reason ?? null : null
13366
13623
  })
13367
13624
  });
13368
13625
  if (!changed.ok)
@@ -13398,7 +13655,7 @@ class WorkitCore {
13398
13655
  return workspace;
13399
13656
  if (!workspace.data)
13400
13657
  return failure("not_found", "workspace not found");
13401
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
13658
+ if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
13402
13659
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
13403
13660
  this.fillRevisions(input, task.data, workspace.data);
13404
13661
  const changed = this.store.mutateTaskAndWorkspace({
@@ -13516,15 +13773,15 @@ function resolveGitRevision(root, value) {
13516
13773
  }
13517
13774
  function resolveInside(root, candidate) {
13518
13775
  const base = realpathSync5(root);
13519
- const target = path6.resolve(base, candidate);
13520
- if (target !== base && !target.startsWith(base + path6.sep)) {
13776
+ const target = path7.resolve(base, candidate);
13777
+ if (target !== base && !target.startsWith(base + path7.sep)) {
13521
13778
  throw new Error("path must stay inside repository root");
13522
13779
  }
13523
13780
  let ancestor = target;
13524
- while (!existsSync3(ancestor))
13525
- ancestor = path6.dirname(ancestor);
13781
+ while (!existsSync4(ancestor))
13782
+ ancestor = path7.dirname(ancestor);
13526
13783
  const canonicalAncestor = realpathSync5(ancestor);
13527
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path6.sep)) {
13784
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
13528
13785
  throw new Error("path must stay inside repository root");
13529
13786
  }
13530
13787
  return target;
@@ -13567,6 +13824,13 @@ var piCapabilities = (ctx) => [
13567
13824
  assurance: "agent_guided",
13568
13825
  reason: "Pi extensions do not sandbox arbitrary shell commands.",
13569
13826
  refs: [hostRef("tool_call")]
13827
+ },
13828
+ {
13829
+ name: "fresh-context-review",
13830
+ surface: "supervised_worker",
13831
+ assurance: "agent_guided",
13832
+ reason: "independent review runs as a supervised stock-Pi process; evidence evaluation enforces reviewer exclusivity",
13833
+ refs: [hostRef("worker")]
13570
13834
  }
13571
13835
  ];
13572
13836
  var piContext = (ctx) => ({
@@ -13634,15 +13898,15 @@ var WORKIT_SKILL_ALIASES = {
13634
13898
  if (false) {}
13635
13899
 
13636
13900
  // 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";
13901
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
13902
+ import { execFileSync as execFileSync3 } from "node:child_process";
13903
+ import path11 from "node:path";
13640
13904
 
13641
13905
  // packages/workit-core/src/core/git.ts
13642
- import { execFileSync } from "node:child_process";
13906
+ import { execFileSync as execFileSync2 } from "node:child_process";
13643
13907
  var run2 = (cwd, args) => {
13644
13908
  try {
13645
- const stdout = execFileSync("git", args, {
13909
+ const stdout = execFileSync2("git", args, {
13646
13910
  cwd,
13647
13911
  encoding: "utf8",
13648
13912
  stdio: ["pipe", "pipe", "pipe"]
@@ -13710,14 +13974,14 @@ var gitContext = (workspaceRoot, paths = []) => {
13710
13974
  import {
13711
13975
  copyFileSync,
13712
13976
  cpSync,
13713
- existsSync as existsSync4,
13977
+ existsSync as existsSync5,
13714
13978
  mkdirSync as mkdirSync2,
13715
13979
  readFileSync as readFileSync4,
13716
13980
  readdirSync as readdirSync3,
13717
13981
  writeFileSync as writeFileSync2
13718
13982
  } from "node:fs";
13719
13983
  import os from "node:os";
13720
- import path7 from "node:path";
13984
+ import path8 from "node:path";
13721
13985
 
13722
13986
  // packages/workit-core/src/core/boundary.ts
13723
13987
  var EVENT = {
@@ -13761,7 +14025,7 @@ var mergePreset = (preset, input = {}, current = {
13761
14025
  protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
13762
14026
  };
13763
14027
  };
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");
14028
+ 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
14029
  var migratedDir = null;
13766
14030
  var migrationFailed = false;
13767
14031
  var ensureConfigDir = (dir = resolveConfigDir()) => {
@@ -13771,12 +14035,12 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
13771
14035
  migratedDir = dir;
13772
14036
  return dir;
13773
14037
  }
13774
- const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workflow-toolkit");
13775
- if (!existsSync4(legacy)) {
14038
+ const legacy = path8.join(process.env.XDG_CONFIG_HOME || path8.join(os.homedir(), ".config"), "workflow-toolkit");
14039
+ if (!existsSync5(legacy)) {
13776
14040
  migratedDir = dir;
13777
14041
  return dir;
13778
14042
  }
13779
- if (!migrationFailed && existsSync4(dir)) {
14043
+ if (!migrationFailed && existsSync5(dir)) {
13780
14044
  migratedDir = dir;
13781
14045
  return dir;
13782
14046
  }
@@ -13784,9 +14048,9 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
13784
14048
  mkdirSync2(dir, { recursive: true });
13785
14049
  diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
13786
14050
  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))
14051
+ const src = path8.join(legacy, entry.name);
14052
+ const dest = path8.join(dir, entry.name);
14053
+ if (existsSync5(dest))
13790
14054
  continue;
13791
14055
  try {
13792
14056
  if (entry.isDirectory())
@@ -13859,7 +14123,7 @@ var parseConfigResult = (raw, file) => {
13859
14123
  };
13860
14124
  };
13861
14125
  var readConfigTyped = (dir) => {
13862
- const file = path7.join(dir ?? configDir(), "config.json");
14126
+ const file = path8.join(dir ?? configDir(), "config.json");
13863
14127
  return parseConfigResult(readSafe(file), file);
13864
14128
  };
13865
14129
  var readConfig = () => {
@@ -13905,9 +14169,9 @@ var resolveCommitPolicy = (config, workspace) => {
13905
14169
 
13906
14170
  // packages/workit-core/src/core/workspaces.ts
13907
14171
  import { readFileSync as readFileSync5, realpathSync as realpathSync6 } from "node:fs";
13908
- import path8 from "node:path";
14172
+ import path9 from "node:path";
13909
14173
  var readWorkspacesResult = (dir = configDir()) => {
13910
- const file = path8.join(dir, "workspaces.json");
14174
+ const file = path9.join(dir, "workspaces.json");
13911
14175
  let raw;
13912
14176
  try {
13913
14177
  raw = readFileSync5(file, "utf8");
@@ -14005,10 +14269,10 @@ var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
14005
14269
 
14006
14270
  // packages/workit-core/src/core/vcs-config.ts
14007
14271
  import fs7 from "node:fs";
14008
- import path9 from "node:path";
14272
+ import path10 from "node:path";
14009
14273
  import { spawnSync as spawnSync3 } from "node:child_process";
14010
14274
  var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
14011
- var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
14275
+ var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path10.join(configDir(), "vcs.json");
14012
14276
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
14013
14277
  var remoteProvider = (cwd) => {
14014
14278
  const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
@@ -14091,8 +14355,8 @@ function vcsConfig(mode, cwd) {
14091
14355
  }
14092
14356
  const prov = cfg[provider] ?? {};
14093
14357
  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);
14358
+ const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path10.join(configDir(), `${provider}.token`));
14359
+ const tokenPath = path10.resolve(tokenFile);
14096
14360
  let tokenOk = false;
14097
14361
  if (fs7.existsSync(tokenPath)) {
14098
14362
  const token = fs7.readFileSync(tokenPath, "utf8").trim();
@@ -14101,7 +14365,7 @@ function vcsConfig(mode, cwd) {
14101
14365
  }
14102
14366
  const out = {
14103
14367
  ok: true,
14104
- configPath: path9.resolve(cfgPath),
14368
+ configPath: path10.resolve(cfgPath),
14105
14369
  provider,
14106
14370
  defaultTargetBranch: defaultTarget,
14107
14371
  pr: cfg.pr ?? {},
@@ -14202,7 +14466,7 @@ var baseBranch = (cwd) => {
14202
14466
  return { base: String(resolved.defaultTargetBranch ?? "develop") };
14203
14467
  };
14204
14468
  var originBaseReady = (cwd, base) => {
14205
- const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14469
+ const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14206
14470
  try {
14207
14471
  try {
14208
14472
  run(["fetch", "origin", base, "--prune"]);
@@ -14210,7 +14474,7 @@ var originBaseReady = (cwd, base) => {
14210
14474
  run(["fetch", "origin", "--prune"]);
14211
14475
  }
14212
14476
  try {
14213
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
14477
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
14214
14478
  cwd,
14215
14479
  stdio: "pipe"
14216
14480
  });
@@ -14229,11 +14493,11 @@ var originBaseReady = (cwd, base) => {
14229
14493
  }
14230
14494
  };
14231
14495
  var fastForwardBase = (cwd, base) => {
14232
- const run = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14496
+ const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14233
14497
  try {
14234
14498
  let hasLocalBase = true;
14235
14499
  try {
14236
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
14500
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
14237
14501
  cwd,
14238
14502
  stdio: "pipe"
14239
14503
  });
@@ -14273,19 +14537,18 @@ var branchSetup = ({
14273
14537
  workspace_root,
14274
14538
  log
14275
14539
  }) => {
14276
- const cwd = path10.resolve(workspace_root);
14277
- const exec = (args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14540
+ const cwd = path11.resolve(workspace_root);
14541
+ const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
14278
14542
  const current = gitContext(cwd).branch;
14279
14543
  if (!current || current === "unknown")
14280
- return { error: "not in a git repository" };
14544
+ return { error: "not in a git repository", phase: "preflight" };
14281
14545
  const sdd = sdd_dir ?? "docs";
14282
- const manifestPath = path10.isAbsolute(sdd) ? path10.join(sdd, "manifest.json") : path10.join(cwd, sdd, "manifest.json");
14546
+ const manifestPath = path11.isAbsolute(sdd) ? path11.join(sdd, "manifest.json") : path11.join(cwd, sdd, "manifest.json");
14283
14547
  const ensureManifestDir = () => {
14284
- const dir = path10.dirname(manifestPath);
14285
- if (!existsSync5(dir))
14548
+ const dir = path11.dirname(manifestPath);
14549
+ if (!existsSync6(dir))
14286
14550
  mkdirSync3(dir, { recursive: true, mode: 493 });
14287
14551
  };
14288
- ensureManifestDir();
14289
14552
  const readManifest = () => {
14290
14553
  try {
14291
14554
  return JSON.parse(readFileSync6(manifestPath, "utf8"));
@@ -14303,13 +14566,16 @@ var branchSetup = ({
14303
14566
  const manifest = readManifest();
14304
14567
  const ref = manifest.stash_ref;
14305
14568
  if (!ref)
14306
- return { error: "no stash_ref in manifest" };
14569
+ return { error: "no stash_ref in manifest", phase: "preflight" };
14307
14570
  journal(`pre-pop: ${String(ref)}`);
14308
14571
  try {
14309
14572
  exec(["stash", "pop", String(ref)]);
14310
14573
  } catch (error) {
14311
14574
  journal("pop: failed");
14312
- return { error: error instanceof Error ? error.message : "stash pop failed" };
14575
+ return {
14576
+ error: error instanceof Error ? error.message : "stash pop failed",
14577
+ phase: "post_mutation"
14578
+ };
14313
14579
  }
14314
14580
  journal("pop: ok");
14315
14581
  delete manifest.stash_ref;
@@ -14319,11 +14585,17 @@ var branchSetup = ({
14319
14585
  }
14320
14586
  const target = target_branch ?? "";
14321
14587
  if (!target)
14322
- return { error: "target branch required" };
14588
+ return {
14589
+ error: "target_branch (the working branch to create or switch to) is required for setup",
14590
+ phase: "preflight"
14591
+ };
14323
14592
  if (isProtected(cwd, target))
14324
- return { error: `protected branch ${target}` };
14593
+ return { error: `protected branch ${target}`, phase: "preflight" };
14325
14594
  if (!allowedBranch(cwd, target))
14326
- return { error: `target branch ${target} is not allowed by the branch policy` };
14595
+ return {
14596
+ error: `target branch ${target} is not allowed by the branch policy`,
14597
+ phase: "preflight"
14598
+ };
14327
14599
  let base;
14328
14600
  let targetExists = true;
14329
14601
  try {
@@ -14334,12 +14606,12 @@ var branchSetup = ({
14334
14606
  if (!targetExists) {
14335
14607
  const baseResolved = baseBranch(cwd);
14336
14608
  if ("error" in baseResolved)
14337
- return { error: baseResolved.error };
14609
+ return { error: baseResolved.error, phase: "preflight" };
14338
14610
  base = baseResolved.base;
14339
14611
  }
14340
14612
  journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
14341
14613
  let stash_ref;
14342
- const failAfterStash = (message) => {
14614
+ const failAfterStash = (message, phase = "post_mutation") => {
14343
14615
  let suffix = "";
14344
14616
  if (stash_ref) {
14345
14617
  journal(`pre-pop: ${stash_ref}`);
@@ -14352,27 +14624,31 @@ var branchSetup = ({
14352
14624
  suffix = " (changes preserved in stash)";
14353
14625
  }
14354
14626
  }
14355
- return { error: `${message}${suffix}` };
14627
+ return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
14356
14628
  };
14357
14629
  if (current !== target) {
14358
14630
  const dirty = Boolean(gitContext(cwd).status_short.trim());
14359
14631
  if (dirty && stash !== "yes") {
14360
14632
  return {
14361
- error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes"
14633
+ error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
14634
+ phase: "preflight"
14362
14635
  };
14363
14636
  }
14364
14637
  let validatedBase;
14365
14638
  if (!targetExists && base !== undefined) {
14366
14639
  const ready = originBaseReady(cwd, base);
14367
14640
  if (!ready.ok)
14368
- return { error: ready.error ?? "ensure-base-branch failed" };
14641
+ return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
14369
14642
  validatedBase = base;
14370
14643
  }
14371
14644
  if (dirty) {
14372
14645
  try {
14373
14646
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
14374
14647
  } catch (error) {
14375
- return { error: error instanceof Error ? error.message : "stash push failed" };
14648
+ return {
14649
+ error: error instanceof Error ? error.message : "stash push failed",
14650
+ phase: "post_mutation"
14651
+ };
14376
14652
  }
14377
14653
  stash_ref = "stash@{0}";
14378
14654
  journal(`stash push: ${stash_ref}`);
@@ -14383,14 +14659,14 @@ var branchSetup = ({
14383
14659
  } catch (error) {
14384
14660
  const message = error instanceof Error ? error.message : "checkout failed";
14385
14661
  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)`);
14662
+ return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
14387
14663
  }
14388
14664
  try {
14389
14665
  let effectiveBase = base;
14390
14666
  if (effectiveBase === undefined) {
14391
14667
  const lateResolved = baseBranch(cwd);
14392
14668
  if ("error" in lateResolved)
14393
- return failAfterStash(lateResolved.error);
14669
+ return failAfterStash(lateResolved.error, "preflight");
14394
14670
  effectiveBase = lateResolved.base;
14395
14671
  }
14396
14672
  const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
@@ -14437,7 +14713,7 @@ var branchSetup = ({
14437
14713
 
14438
14714
  // packages/workit-core/src/core/pr-create.ts
14439
14715
  import fs8 from "node:fs";
14440
- import path11 from "node:path";
14716
+ import path12 from "node:path";
14441
14717
  import { spawnSync as spawnSync4 } from "node:child_process";
14442
14718
  function parseGhRepo(remote) {
14443
14719
  remote = (remote || "").trim().replace(/\/+$/, "");
@@ -14502,11 +14778,11 @@ ${line}` : line;
14502
14778
  }
14503
14779
  function whichOnPath(tool) {
14504
14780
  const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
14505
- for (const dir of (process.env.PATH ?? "").split(path11.delimiter)) {
14781
+ for (const dir of (process.env.PATH ?? "").split(path12.delimiter)) {
14506
14782
  if (!dir)
14507
14783
  continue;
14508
14784
  for (const name of names) {
14509
- const candidate = path11.join(dir, name);
14785
+ const candidate = path12.join(dir, name);
14510
14786
  try {
14511
14787
  fs8.accessSync(candidate, fs8.constants.X_OK);
14512
14788
  return candidate;
@@ -14609,7 +14885,7 @@ function prCreate(env, cwd) {
14609
14885
  const draft = String(env.WF_PR_DRAFT ?? "false").toLowerCase() === "true";
14610
14886
  let baseUrl = cfg.youtrack_base_url;
14611
14887
  if (!baseUrl) {
14612
- const ytCfg = process.env.WORKFLOW_YOUTRACK_CONFIG ?? path11.join(path11.dirname(String(cfg.configPath)), "youtrack.json");
14888
+ const ytCfg = process.env.WORKFLOW_YOUTRACK_CONFIG ?? path12.join(path12.dirname(String(cfg.configPath)), "youtrack.json");
14613
14889
  try {
14614
14890
  const yt = JSON.parse(fs8.readFileSync(ytCfg, "utf8"));
14615
14891
  if (yt && typeof yt === "object")
@@ -14716,8 +14992,8 @@ function prCreate(env, cwd) {
14716
14992
 
14717
14993
  // packages/workit-core/src/core/repo-context.ts
14718
14994
  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";
14995
+ import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
14996
+ import path13 from "node:path";
14721
14997
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
14722
14998
  try {
14723
14999
  gitRevisionParts(value);
@@ -14851,7 +15127,7 @@ var findPrTemplate = (cwd) => {
14851
15127
  ]) {
14852
15128
  const parts = rel.split("/");
14853
15129
  const base = parts[parts.length - 1];
14854
- const dir = parts.length > 1 ? path12.join(cwd, ...parts.slice(0, -1)) : cwd;
15130
+ const dir = parts.length > 1 ? path13.join(cwd, ...parts.slice(0, -1)) : cwd;
14855
15131
  const actual = probeCaseInsensitive(dir, base);
14856
15132
  if (actual)
14857
15133
  return [...parts.slice(0, -1), actual].join("/");
@@ -14882,8 +15158,8 @@ var diffStatForRange = (cwd, range) => {
14882
15158
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
14883
15159
  };
14884
15160
  var packageScripts = (cwd, keys) => {
14885
- const pkgPath = path12.join(cwd, "package.json");
14886
- if (!existsSync6(pkgPath))
15161
+ const pkgPath = path13.join(cwd, "package.json");
15162
+ if (!existsSync7(pkgPath))
14887
15163
  return [];
14888
15164
  try {
14889
15165
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
@@ -14908,9 +15184,9 @@ var documentationFiles = (cwd) => {
14908
15184
  if (entry.isDirectory()) {
14909
15185
  if (excluded.has(entry.name))
14910
15186
  continue;
14911
- walk(path12.join(dir, entry.name), depth + 1);
15187
+ walk(path13.join(dir, entry.name), depth + 1);
14912
15188
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
14913
- matches.push("./" + path12.relative(cwd, path12.join(dir, entry.name)).replaceAll("\\", "/"));
15189
+ matches.push("./" + path13.relative(cwd, path13.join(dir, entry.name)).replaceAll("\\", "/"));
14914
15190
  }
14915
15191
  }
14916
15192
  };
@@ -14918,7 +15194,7 @@ var documentationFiles = (cwd) => {
14918
15194
  return matches.sort().slice(0, 200);
14919
15195
  };
14920
15196
  var readTrimmed = (file, maxLines) => {
14921
- if (!existsSync6(file))
15197
+ if (!existsSync7(file))
14922
15198
  return "";
14923
15199
  const lines = readFileSync7(file, "utf8").split(`
14924
15200
  `);
@@ -14926,7 +15202,7 @@ var readTrimmed = (file, maxLines) => {
14926
15202
  `);
14927
15203
  };
14928
15204
  function prReadyContext(root, range) {
14929
- const cwd = path12.resolve(repoRoot2(root));
15205
+ const cwd = path13.resolve(repoRoot2(root));
14930
15206
  if (range !== undefined && !isResolvableContextRange(cwd, range))
14931
15207
  return invalidRange(cwd);
14932
15208
  let stdout = "";
@@ -14984,7 +15260,7 @@ ${status.stderr}` : "");
14984
15260
  stdout += `template_path: ${template}
14985
15261
 
14986
15262
  `;
14987
- stdout += readTrimmed(path12.join(cwd, template), 220);
15263
+ stdout += readTrimmed(path13.join(cwd, template), 220);
14988
15264
  } else {
14989
15265
  stdout += `template_path: none
14990
15266
 
@@ -14994,7 +15270,7 @@ ${status.stderr}` : "");
14994
15270
  stdout += `
14995
15271
  `;
14996
15272
  stdout += printSection("Recent Validation Signals");
14997
- if (existsSync6(path12.join(cwd, "package.json"))) {
15273
+ if (existsSync7(path13.join(cwd, "package.json"))) {
14998
15274
  stdout += `package.json detected. Common scripts:
14999
15275
  `;
15000
15276
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -15003,7 +15279,7 @@ ${status.stderr}` : "");
15003
15279
  `) + `
15004
15280
  `;
15005
15281
  }
15006
- if (existsSync6(path12.join(cwd, "Cargo.toml")) || existsSync6(path12.join(cwd, "src-tauri/Cargo.toml"))) {
15282
+ if (existsSync7(path13.join(cwd, "Cargo.toml")) || existsSync7(path13.join(cwd, "src-tauri/Cargo.toml"))) {
15007
15283
  stdout += `Rust project detected.
15008
15284
  `;
15009
15285
  }
@@ -15036,7 +15312,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
15036
15312
  - Apply with the native workit_changelog_apply tool only (not hand-edits under Unreleased).
15037
15313
  - If Unreleased already has duplicate category headings, normalize_only first.`;
15038
15314
  function changelogContext(root, range) {
15039
- const cwd = path12.resolve(repoRoot2(root));
15315
+ const cwd = path13.resolve(repoRoot2(root));
15040
15316
  if (range !== undefined && !isResolvableContextRange(cwd, range))
15041
15317
  return invalidRange(cwd);
15042
15318
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -15054,8 +15330,8 @@ function changelogContext(root, range) {
15054
15330
  stdout += CHANGELOG_RULES + `
15055
15331
  `;
15056
15332
  stdout += printSection("Existing CHANGELOG.md");
15057
- const changelogPath = path12.join(cwd, "CHANGELOG.md");
15058
- if (existsSync6(changelogPath)) {
15333
+ const changelogPath = path13.join(cwd, "CHANGELOG.md");
15334
+ if (existsSync7(changelogPath)) {
15059
15335
  stdout += readTrimmed(changelogPath, 260) + `
15060
15336
  `;
15061
15337
  } else {
@@ -15074,7 +15350,7 @@ function changelogContext(root, range) {
15074
15350
  return { stdout, stderr: "", exitCode: 0, cwd };
15075
15351
  }
15076
15352
  function docsRefreshContext(root, range) {
15077
- const cwd = path12.resolve(repoRoot2(root));
15353
+ const cwd = path13.resolve(repoRoot2(root));
15078
15354
  if (range !== undefined && !isResolvableContextRange(cwd, range))
15079
15355
  return invalidRange(cwd);
15080
15356
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -15096,8 +15372,8 @@ function docsRefreshContext(root, range) {
15096
15372
  `) + `
15097
15373
  `;
15098
15374
  stdout += printSection("README Preview");
15099
- const readme = path12.join(cwd, "README.md");
15100
- if (existsSync6(readme)) {
15375
+ const readme = path13.join(cwd, "README.md");
15376
+ if (existsSync7(readme)) {
15101
15377
  stdout += readTrimmed(readme, 220) + `
15102
15378
  `;
15103
15379
  } else {
@@ -15105,8 +15381,8 @@ function docsRefreshContext(root, range) {
15105
15381
  `;
15106
15382
  }
15107
15383
  stdout += printSection("Package Scripts");
15108
- const pkgPath = path12.join(cwd, "package.json");
15109
- if (existsSync6(pkgPath)) {
15384
+ const pkgPath = path13.join(cwd, "package.json");
15385
+ if (existsSync7(pkgPath)) {
15110
15386
  try {
15111
15387
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
15112
15388
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
@@ -15119,7 +15395,7 @@ function docsRefreshContext(root, range) {
15119
15395
  return { stdout, stderr: "", exitCode: 0, cwd };
15120
15396
  }
15121
15397
  function releaseNotesContext(root, rangeOrTag) {
15122
- const cwd = path12.resolve(repoRoot2(root));
15398
+ const cwd = path13.resolve(repoRoot2(root));
15123
15399
  if (!rangeOrTag) {
15124
15400
  return { stdout: "", stderr: `ERROR: release tag or range required
15125
15401
  `, exitCode: 1, cwd };
@@ -15157,7 +15433,7 @@ function releaseNotesContext(root, rangeOrTag) {
15157
15433
  `;
15158
15434
  stdout += printSection("Existing Release Files");
15159
15435
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
15160
- if (existsSync6(path12.join(cwd, rel)))
15436
+ if (existsSync7(path13.join(cwd, rel)))
15161
15437
  stdout += rel + `
15162
15438
  `;
15163
15439
  }
@@ -15348,25 +15624,6 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
15348
15624
  import fs10 from "node:fs";
15349
15625
  import path14 from "node:path";
15350
15626
 
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
15627
  // packages/workit-core/src/core/templates.ts
15371
15628
  var repoRoot3 = assetRoot();
15372
15629
 
@@ -16572,6 +16829,94 @@ var safePath = (root, value) => {
16572
16829
  return null;
16573
16830
  }
16574
16831
  };
16832
+ var branchCreationBaseline = (root, target, base) => {
16833
+ const exists = gitValue(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]) !== null;
16834
+ if (exists)
16835
+ return { base_branch: base, target_exists: true, remote_base: null };
16836
+ const remote = run3(root, ["ls-remote", "origin", `refs/heads/${base}`]);
16837
+ const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
16838
+ return {
16839
+ base_branch: base,
16840
+ target_exists: false,
16841
+ remote_base: /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null
16842
+ };
16843
+ };
16844
+ var shortSha = (value) => typeof value === "string" && value ? value.slice(0, 8) : "unknown";
16845
+ var isProtectedBranch2 = (root, branch) => resolveBranchPolicyFor(root).protected.has(branch.toLowerCase());
16846
+ var actionProposalQuestion = (request, descriptorPayload) => {
16847
+ const payload = descriptorPayload ?? {};
16848
+ const resolved = payload.resolved ?? {};
16849
+ const payloadTarget = String(payload.target_branch ?? "");
16850
+ if (request.operation === "git.commit" && Array.isArray(payload.plan_steps)) {
16851
+ const steps = payload.plan_steps;
16852
+ return {
16853
+ presented: `Workit decision: action — Approve committing the plan's ${steps.length} listed tasks on \`${String(payload.plan_branch ?? "")}\`?`,
16854
+ approvedText: `Approve the ${steps.length} listed plan commits.`
16855
+ };
16856
+ }
16857
+ switch (request.operation) {
16858
+ case "git.branch_setup": {
16859
+ if (request.payload.action === "reapply_stash")
16860
+ return {
16861
+ presented: "Workit decision: action — Reapply the pre-checkout stash recorded in the branch manifest?",
16862
+ approvedText: "Reapply the recorded stash."
16863
+ };
16864
+ const target = String(payload.target_branch ?? "");
16865
+ if (resolved.target_exists === true)
16866
+ return {
16867
+ presented: `Workit decision: action — Switch to branch \`${target}\`?`,
16868
+ approvedText: `Switch to \`${target}\`.`
16869
+ };
16870
+ const base = String(resolved.base_branch ?? "the base branch");
16871
+ return {
16872
+ presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
16873
+ approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
16874
+ };
16875
+ }
16876
+ case "git.commit": {
16877
+ const paths = Array.isArray(resolved.paths) ? resolved.paths : [];
16878
+ return {
16879
+ presented: `Workit decision: action — Commit ${paths.length} staged file(s) as "${String(request.payload.message)}"?`,
16880
+ approvedText: `Commit as "${String(request.payload.message)}".`
16881
+ };
16882
+ }
16883
+ case "git.push":
16884
+ return {
16885
+ presented: `Workit decision: action — Push branch \`${String(payload.branch ?? "")}\` to origin?`,
16886
+ approvedText: `Push \`${String(payload.branch ?? "")}\` to origin.`
16887
+ };
16888
+ case "hosting.pull_request":
16889
+ return {
16890
+ presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
16891
+ approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
16892
+ };
16893
+ case "changelog.apply":
16894
+ return {
16895
+ presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
16896
+ approvedText: `Apply changelog changes at ${String(payload.path ?? "CHANGELOG.md")}.`
16897
+ };
16898
+ case "youtrack.update":
16899
+ return {
16900
+ presented: `Workit decision: action — Update YouTrack issue ${String(request.payload.issueId)}${request.payload.minutes ? ` and log ${String(request.payload.minutes)}m` : ""}?`,
16901
+ approvedText: `Update YouTrack issue ${String(request.payload.issueId)}.`
16902
+ };
16903
+ case "youtrack.time":
16904
+ return {
16905
+ presented: `Workit decision: action — Log ${String(request.payload.minutes)}m to YouTrack issue ${String(request.payload.issueId)}?`,
16906
+ approvedText: `Log time to YouTrack issue ${String(request.payload.issueId)}.`
16907
+ };
16908
+ case "youtrack.meeting":
16909
+ return {
16910
+ presented: `Workit decision: action — Log meeting time to YouTrack issue ${String(request.payload.issueId)}?`,
16911
+ approvedText: `Log meeting time to YouTrack issue ${String(request.payload.issueId)}.`
16912
+ };
16913
+ default:
16914
+ return {
16915
+ presented: `Workit decision: action — Approve ${request.operation}?`,
16916
+ approvedText: `Approve ${request.operation}.`
16917
+ };
16918
+ }
16919
+ };
16575
16920
  var approvedResolvedExternalAction = (content) => {
16576
16921
  try {
16577
16922
  const value = JSON.parse(content);
@@ -16835,24 +17180,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16835
17180
  const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
16836
17181
  if (!sdd_dir)
16837
17182
  return failure("invalid_input", "sdd_dir must stay inside the workspace");
17183
+ const action = request.payload.action ?? "setup";
16838
17184
  const policy = resolveBranchPolicyFor(root);
16839
- const target_branch = request.payload.target_branch ?? policy.defaultTargetBranch;
17185
+ const target_branch = request.payload.target_branch;
17186
+ if (action !== "reapply_stash" && !target_branch)
17187
+ return failure("invalid_input", "target_branch (the working branch to create or switch to) is required for setup", {
17188
+ outcome: "not_started",
17189
+ fields: [{ path: "target_branch", reason: "required for action=setup" }]
17190
+ });
16840
17191
  const head = gitValue(root, ["rev-parse", "HEAD"]);
16841
17192
  if (!head)
16842
17193
  return failure("storage_error", "current Git commit could not be resolved");
17194
+ const dirty = Boolean((gitRaw(root, ["status", "--short"]) ?? "").trim());
17195
+ const creation = action === "reapply_stash" || !target_branch ? { base_branch: policy.defaultTargetBranch } : branchCreationBaseline(root, target_branch, policy.defaultTargetBranch);
16843
17196
  const normalized = {
16844
17197
  operation: request.operation,
16845
17198
  payload: {
16846
17199
  ...request.payload,
16847
- action: request.payload.action ?? "setup",
17200
+ action,
16848
17201
  sdd_dir,
16849
17202
  stash: request.payload.stash ?? "no",
16850
- target_branch
17203
+ ...target_branch ? { target_branch } : {}
16851
17204
  }
16852
17205
  };
16853
17206
  return success(null, null, {
16854
17207
  request: normalized,
16855
- descriptorPayload: { ...normalized.payload, resolved: { head, target_branch } }
17208
+ descriptorPayload: {
17209
+ ...normalized.payload,
17210
+ resolved: { head, dirty, ...creation }
17211
+ }
16856
17212
  });
16857
17213
  }
16858
17214
  case "git.push": {
@@ -16866,7 +17222,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16866
17222
  }) : failure("invalid_input", "git push requires a resolvable current branch");
16867
17223
  }
16868
17224
  case "git.commit": {
17225
+ const planSteps = request.payload.plan_steps;
17226
+ const planBranch = request.payload.plan_branch;
16869
17227
  const head = gitValue(root, ["rev-parse", "HEAD"]);
17228
+ if (!head)
17229
+ return failure("storage_error", "Git state could not be resolved");
17230
+ const branch = gitValue(root, ["branch", "--show-current"]);
17231
+ if (planSteps !== undefined || planBranch !== undefined) {
17232
+ if (!Array.isArray(planSteps) || planSteps.some((step) => typeof step !== "string" || !step.trim()) || new Set(planSteps).size !== planSteps.length || planSteps.length === 0 || planSteps.length > 32)
17233
+ return failure("invalid_input", "plan_steps must be 1-32 unique non-empty commit messages", {
17234
+ outcome: "not_started",
17235
+ fields: [{ path: "plan_steps", reason: "unique strings" }]
17236
+ });
17237
+ if (typeof planBranch !== "string" || !planBranch.trim() || planBranch !== branch)
17238
+ return failure("invalid_input", "plan_branch must name the current working branch", {
17239
+ outcome: "not_started",
17240
+ fields: [{ path: "plan_branch", reason: "current branch" }]
17241
+ });
17242
+ if (isProtectedBranch2(root, planBranch))
17243
+ return failure("invalid_input", "plan_branch must not be a protected branch", {
17244
+ outcome: "not_started",
17245
+ fields: [{ path: "plan_branch", reason: "protected branch" }]
17246
+ });
17247
+ return success(null, null, {
17248
+ request,
17249
+ descriptorPayload: {
17250
+ plan_steps: planSteps,
17251
+ plan_branch: planBranch,
17252
+ resolved: { head, branch, steps: planSteps }
17253
+ }
17254
+ });
17255
+ }
16870
17256
  const staged = gitRaw(root, [
16871
17257
  "diff",
16872
17258
  "--cached",
@@ -16877,7 +17263,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16877
17263
  "HEAD"
16878
17264
  ]);
16879
17265
  const paths = stagedPaths(root);
16880
- if (!head || staged === null || paths === null)
17266
+ const message = request.payload.message;
17267
+ if (typeof message !== "string" || !message)
17268
+ return failure("invalid_input", "git.commit requires a commit message", {
17269
+ outcome: "not_started",
17270
+ fields: [{ path: "message", reason: "required unless plan_steps is provided" }]
17271
+ });
17272
+ if (staged === null || paths === null)
16881
17273
  return failure("storage_error", "Git state could not be resolved");
16882
17274
  let policy;
16883
17275
  try {
@@ -16892,13 +17284,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16892
17284
  `).filter(Boolean));
16893
17285
  flavor = detected.flavor ?? "conventional";
16894
17286
  }
16895
- if (!matchCommitFlavor(request.payload.message, flavor, policy.pattern))
17287
+ if (!matchCommitFlavor(message, flavor, policy.pattern))
16896
17288
  return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
16897
17289
  return success(null, null, {
16898
17290
  request,
16899
17291
  descriptorPayload: {
16900
17292
  ...request.payload,
16901
- resolved: { head, staged: sha256(staged), paths }
17293
+ resolved: { head, branch, staged: sha256(staged), paths }
16902
17294
  }
16903
17295
  });
16904
17296
  }
@@ -17068,10 +17460,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
17068
17460
  sdd_dir: request.payload.sdd_dir ?? "docs",
17069
17461
  workspace_root: root
17070
17462
  });
17071
- return result.error ? unknown2(request.operation) : success(null, null, result);
17463
+ if ("error" in result) {
17464
+ if (result.phase === "preflight")
17465
+ return failure("invalid_input", result.error, {
17466
+ outcome: "not_started",
17467
+ operation: request.operation
17468
+ });
17469
+ return unknown2(request.operation);
17470
+ }
17471
+ return success(null, null, result);
17072
17472
  }
17073
17473
  case "git.commit": {
17074
- const result = run3(root, ["commit", "-m", request.payload.message]);
17474
+ const message = request.payload.message;
17475
+ if (typeof message !== "string" || !message)
17476
+ return failure("invalid_input", "git.commit requires a commit message", {
17477
+ outcome: "not_started",
17478
+ fields: [{ path: "message", reason: "required" }]
17479
+ });
17480
+ const result = run3(root, ["commit", "-m", message]);
17075
17481
  return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown2(request.operation);
17076
17482
  }
17077
17483
  case "git.push": {
@@ -17244,8 +17650,34 @@ var nativeAuthority = (actor, reconciliationTokens = new WeakSet) => ({
17244
17650
  var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
17245
17651
  const store = new TaskStore(root);
17246
17652
  const selected = approvedExternalAction(store, "pi", actor, operation);
17247
- if (!selected.ok)
17653
+ if (!selected.ok) {
17654
+ const plan = planCommitBinding(store, "pi", actor, operation);
17655
+ if (plan) {
17656
+ const actionRef = externalActionRef("pi", actor, operation);
17657
+ return {
17658
+ taskId: plan.taskId,
17659
+ decisionId: plan.decisionId,
17660
+ actionRef,
17661
+ expectedRevision: plan.expectedRevision,
17662
+ expectedWorkspaceRevision: plan.expectedWorkspaceRevision,
17663
+ binding: plan.binding,
17664
+ step: plan.step,
17665
+ refresh: () => {
17666
+ const task = store.readTask(plan.taskId);
17667
+ const freshWorkspace = store.readWorkspace();
17668
+ if (!task.ok || !freshWorkspace.ok || !freshWorkspace.data)
17669
+ throw new Error("external action state changed");
17670
+ return {
17671
+ expectedRevision: task.data.revision,
17672
+ expectedWorkspaceRevision: freshWorkspace.data.revision
17673
+ };
17674
+ },
17675
+ reserveObservation: nativeExternalActionObservation(actor, actionRef, "reserve"),
17676
+ settleObservation: (outcome, revisions) => nativeExternalActionObservation(actor, actionRef, outcome, actionRef.kind === "host" ? actionRef.handle : "external-action", revisions)
17677
+ };
17678
+ }
17248
17679
  return selected;
17680
+ }
17249
17681
  const actionRef = externalActionRef("pi", actor, operation);
17250
17682
  return {
17251
17683
  taskId: selected.data.task.id,
@@ -17311,6 +17743,21 @@ var executeFamily = async (family, toolCallId, input, ctx) => {
17311
17743
  operation: "decision.record"
17312
17744
  }));
17313
17745
  const decision = parsed.data;
17746
+ const budgetIssue = workitBindingQuestionIssue([
17747
+ {
17748
+ header: `Workit decision: ${parsed.data.purpose}`,
17749
+ question: decision.binding.presented,
17750
+ options: [
17751
+ {
17752
+ label: "approved",
17753
+ description: decision.binding.displayed ?? decision.binding.approvedContent
17754
+ },
17755
+ { label: "rejected", description: "Reject this decision" }
17756
+ ]
17757
+ }
17758
+ ]);
17759
+ if (budgetIssue)
17760
+ return output(failure("invalid_input", budgetIssue));
17314
17761
  const approved = await ctx.ui.confirm("Workit decision", decision.binding.presented);
17315
17762
  if (approved !== (decision.response === "approved"))
17316
17763
  return output(failure("permission_denied", "Pi decision answer did not match the requested response"));
@@ -17383,6 +17830,22 @@ var registerWorkitTools = (pi, options = {}) => {
17383
17830
  ...context,
17384
17831
  nativeAuthority: nativeAuthority(actor, reconciliationTokens)
17385
17832
  });
17833
+ const localOperation = [
17834
+ "git.branch_setup",
17835
+ "git.commit",
17836
+ "git.push",
17837
+ "hosting.pull_request",
17838
+ "changelog.apply"
17839
+ ].includes(resolved.data.request.operation);
17840
+ if (localOperation) {
17841
+ const writer = assertLocalExternalActionWriter(ctx.cwd, { host: "pi", actor });
17842
+ if (!writer.ok)
17843
+ return output(failure("needs_input", "writer ownership is required before this action", {
17844
+ outcome: "not_started",
17845
+ operation: resolved.data.request.operation,
17846
+ guidance: "Acquire checkout writer ownership first (writer.acquire) and retry the action."
17847
+ }));
17848
+ }
17386
17849
  const descriptor = externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload);
17387
17850
  const priorRequest = priorExternalAction(store, "pi", actor, resolved.data.request.operation, resolved.data.request.payload);
17388
17851
  if (priorRequest.ok && priorRequest.data.entry.data.consumption?.state === "uncertain") {
@@ -17421,8 +17884,18 @@ var registerWorkitTools = (pi, options = {}) => {
17421
17884
  }
17422
17885
  if (priorRequest.ok && priorRequest.data.entry.data.consumption !== null)
17423
17886
  return output(failure("permission_denied", "external action was already settled"));
17887
+ 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);
17888
+ if (!drift.ok)
17889
+ return output(drift);
17424
17890
  const existing = approvedExternalAction(store, "pi", actor, descriptor);
17425
17891
  if (!existing.ok) {
17892
+ const commitMessage = resolved.data.request.operation === "git.commit" ? resolved.data.request.payload.message : undefined;
17893
+ const currentBranch = resolved.data.descriptorPayload.resolved?.branch;
17894
+ const plan = typeof commitMessage === "string" && commitMessage ? approvedPlanCommit(store, "pi", actor, commitMessage) : null;
17895
+ if (plan?.ok && plan.data.branch === currentBranch) {
17896
+ const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
17897
+ return output(result);
17898
+ }
17426
17899
  const prior = externalActionState(store, "pi", actor, descriptor);
17427
17900
  if (prior.ok && prior.data.entry.data.consumption !== null)
17428
17901
  return output(failure("permission_denied", "external action was already settled"));
@@ -17434,7 +17907,8 @@ var registerWorkitTools = (pi, options = {}) => {
17434
17907
  if (candidates.length !== 1)
17435
17908
  return output(failure("permission_denied", "external action requires exactly one active Pi task"));
17436
17909
  const task = candidates[0];
17437
- const approved = await ctx.ui.confirm("Workit external action", descriptor);
17910
+ const question = actionProposalQuestion(resolved.data.request, resolved.data.descriptorPayload);
17911
+ const approved = await ctx.ui.confirm(question.presented, question.approvedText);
17438
17912
  if (!approved)
17439
17913
  return output(failure("permission_denied", "external action was not approved"));
17440
17914
  const currentWorkspace = store.readWorkspace();
@@ -17450,8 +17924,9 @@ var registerWorkitTools = (pi, options = {}) => {
17450
17924
  taskId: task.id,
17451
17925
  workspaceId: currentWorkspace.data.id,
17452
17926
  scope: task.intent.data.scope,
17453
- presented: `Approve ${descriptor}`,
17927
+ presented: question.presented,
17454
17928
  approvedContent: descriptor,
17929
+ displayed: question.approvedText,
17455
17930
  contentRefs: []
17456
17931
  },
17457
17932
  response: "approved",
@@ -17460,6 +17935,9 @@ var registerWorkitTools = (pi, options = {}) => {
17460
17935
  if (!decision.ok)
17461
17936
  return output(decision);
17462
17937
  }
17938
+ const planSteps = resolved.data.descriptorPayload.plan_steps;
17939
+ if (resolved.data.request.operation === "git.commit" && Array.isArray(planSteps))
17940
+ return output(success(null, null, { plan_commits: planSteps.length }));
17463
17941
  const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
17464
17942
  return output(result);
17465
17943
  }