@brainervirus/workit-pi 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,
@@ -7834,7 +7856,7 @@ var workerReportSchema = object({
7834
7856
  }).strict();
7835
7857
  var workerSchema = object({
7836
7858
  assignment: assignmentSchema,
7837
- state: _enum(["assigned", "running", "cancelling", "stopped", "unknown"]),
7859
+ state: _enum(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
7838
7860
  session: refSchema.nullable(),
7839
7861
  report: workerReportSchema.nullable()
7840
7862
  }).strict();
@@ -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");
9838
+ }
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;
9732
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);
9733
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)
@@ -10659,6 +10841,106 @@ function evaluateClosure(requestedOutcome, view) {
10659
10841
  });
10660
10842
  }
10661
10843
 
10844
+ // packages/workit-core/src/core/methods.ts
10845
+ var METHODS = {
10846
+ "workit-challenge": { dimensions: ["challenge", "decisions"] },
10847
+ "workit-behavioral-tdd": { dimensions: ["testing"] },
10848
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
10849
+ "workit-plan": { dimensions: ["artifacts", "continuity"] },
10850
+ "workit-implement": { dimensions: ["delegation"] },
10851
+ "workit-debug": { ruleIds: ["root-cause-investigation"] },
10852
+ "workit-handoff": { ruleIds: ["durable-handoff"] },
10853
+ "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
10854
+ };
10855
+ var methodMatches = (id, definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true && !(id === "workit-review" && requirement.ruleId === "self-review") || definition.ruleIds?.includes(requirement.ruleId) === true;
10856
+ var assuranceFor = (id, definition, capabilities) => {
10857
+ const keys = new Set([
10858
+ id,
10859
+ ...definition.dimensions ?? [],
10860
+ ...definition.ruleIds ?? []
10861
+ ]);
10862
+ const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
10863
+ if (id === "workit-review" && matches.length === 0)
10864
+ return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
10865
+ if (matches.some((capability) => capability.assurance === "unavailable"))
10866
+ return {
10867
+ assurance: "unavailable",
10868
+ reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
10869
+ };
10870
+ if (matches.some((capability) => capability.assurance === "enforced"))
10871
+ return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
10872
+ return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
10873
+ };
10874
+ function selectMethods(policy, capabilities) {
10875
+ return Object.entries(METHODS).flatMap(([id, definition]) => {
10876
+ const requirements = policy.requirements.filter((requirement) => methodMatches(id, definition, requirement));
10877
+ if (requirements.length === 0)
10878
+ return [];
10879
+ const capability = assuranceFor(id, definition, capabilities);
10880
+ const reasons = [
10881
+ ...new Set(requirements.map((requirement) => requirement.reason)),
10882
+ ...capability.reasons
10883
+ ];
10884
+ if (capability.assurance === "unavailable")
10885
+ reasons.unshift("required capability is unavailable");
10886
+ return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
10887
+ });
10888
+ }
10889
+ var invariantBootstrap = () => `
10890
+ Workit keeps one accountable lead and one shared task state. Inspect current task
10891
+ state before acting. An empty task.list means no session yet, not permission to
10892
+ skip Workit; for user-requested product, debug, or behavior work with no active
10893
+ or paused task, run task.start then policy.assess before other product mutations.
10894
+ Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
10895
+ to the current records; explicit values are still concurrency-checked, so never
10896
+ copy revisions between calls. Happy path: task.list, task.start {intent} with no
10897
+ revisions, policy.assess {assessment: {facts, signals, consequences,
10898
+ verification}} where facts are inferred or observed with file refs,
10899
+ writer.acquire {taskId} before product writes, evidence.record {evidence} where
10900
+ check and review kinds auto-bind the current tree and need no digests. Record
10901
+ self-review requirements with kind review and reviewContext matching your own
10902
+ host session; the lead may also record checks. Fresh-context review requires a
10903
+ session distinct from the task creator and every other evidence recorder. Record
10904
+ verification evidence last: later evidence or commits can reopen fixed findings
10905
+ for re-verification, so verify, resolve, then close with nothing in between.
10906
+ decision.record {binding with taskId/workspaceId from inspect plus presented and
10907
+ approvedContent} only through a native approval question asked receipt-shaped:
10908
+ header Workit decision: <purpose>, the same label repeated in the question
10909
+ text (some host UIs do not render headers), exactly two options approved and
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,
10922
+ summary, decisionIds}. A completed native subagent run stops its bound worker
10923
+ by itself; if a worker strands in cancelling with its run verifiably over,
10924
+ repeat worker.cancel {reason} to confirm the stop. Check and review evidence
10925
+ without a bound candidate goes stale; findings close only as fixed with passing
10926
+ verification, dismissed with supporting evidence, or deferred under an approved
10927
+ limitation. Use only the
10928
+ shared operations for task, policy, evidence, finding, decision, worker, writer,
10929
+ and state changes. Authority is bounded by the requested scope, current
10930
+ revision, caller/session provenance, and observed capabilities. Never claim host
10931
+ enforcement or evidence that the host cannot provide. Shell-executed writes are
10932
+ unattested agent-guided work even when a writer is held. Preserve unresolved
10933
+ requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
10934
+ always load on demand. Model-invoked: new instructions, interruptions, or
10935
+ forgotten items mid-task → load workit-steer before responding; about to open
10936
+ a PR or fresh from implementing → load workit-deslop; CI red → load
10937
+ workit-green-run; small-looking change before close or merge → load
10938
+ workit-blast-radius; ambiguous, consequential, or disputed proposal before
10939
+ recommending → load workit-challenge; spec or plan needs a flow diagram →
10940
+ load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
10941
+ act from memory of it.
10942
+ `.trim();
10943
+
10662
10944
  // packages/workit-core/src/core/task-context.ts
10663
10945
  function reconcileResume(view, observations = []) {
10664
10946
  if (observations.length > 0)
@@ -10668,7 +10950,7 @@ function reconcileResume(view, observations = []) {
10668
10950
  return candidate;
10669
10951
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
10670
10952
  const blockers = [...view.task.progress.blockers];
10671
- if (view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
10953
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
10672
10954
  blockers.push({
10673
10955
  reason: "worker state requires reconciliation",
10674
10956
  dependentAction: "resume",
@@ -10718,6 +11000,11 @@ function compactTaskContext(view) {
10718
11000
  digest: entry.data.digest,
10719
11001
  references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
10720
11002
  }));
11003
+ const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
11004
+ id: method.id,
11005
+ assurance: method.assurance,
11006
+ reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
11007
+ })) : [];
10721
11008
  const gaps = Array.from(new Set([
10722
11009
  ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
10723
11010
  ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
@@ -10727,15 +11014,18 @@ function compactTaskContext(view) {
10727
11014
  objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
10728
11015
  status: view.task.status,
10729
11016
  decisions,
11017
+ methods,
10730
11018
  gaps,
10731
11019
  nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
10732
11020
  };
10733
11021
  let encoded = JSON.stringify(context);
10734
- while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.gaps.length)) {
11022
+ while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
10735
11023
  if (context.gaps.length)
10736
11024
  context.gaps.pop();
10737
- else
11025
+ else if (context.decisions.length)
10738
11026
  context.decisions.pop();
11027
+ else
11028
+ context.methods.pop();
10739
11029
  encoded = JSON.stringify(context);
10740
11030
  }
10741
11031
  if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
@@ -10751,95 +11041,49 @@ var exportDigest = (bundle) => sha256({
10751
11041
  sourceWorkspaceId: bundle.sourceWorkspaceId,
10752
11042
  task: bundle.task
10753
11043
  });
10754
- // packages/workit-core/src/core/methods.ts
10755
- var METHODS = {
10756
- "workit-challenge": { dimensions: ["challenge", "decisions"] },
10757
- "workit-behavioral-tdd": { dimensions: ["testing"] },
10758
- "workit-review": { dimensions: ["review"] },
10759
- "workit-plan": { dimensions: ["artifacts", "continuity"] },
10760
- "workit-implement": { dimensions: ["delegation"] },
10761
- "workit-debug": { ruleIds: ["root-cause-investigation"] },
10762
- "workit-handoff": { ruleIds: ["durable-handoff"] },
10763
- "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
11044
+ // packages/workit-core/src/core/route-intent.ts
11045
+ var GUIDANCE = {
11046
+ "git.branch_setup": "use the Workit git.branch_setup action (workit_external_action in hosts, or `workit action git.branch_setup --payload '{}' --confirm --json` in the CLI)",
11047
+ "hosting.pull_request": "use the Workit hosting.pull_request action (workit_external_action in hosts, or `workit action hosting.pull_request --payload '{...}' --confirm --json` in the CLI), then drive the created PR through workit-babysit"
10764
11048
  };
10765
- var methodMatches = (id, definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true && !(id === "workit-review" && requirement.ruleId === "self-review") || definition.ruleIds?.includes(requirement.ruleId) === true;
10766
- var assuranceFor = (id, definition, capabilities) => {
10767
- const keys = new Set([
10768
- id,
10769
- ...definition.dimensions ?? [],
10770
- ...definition.ruleIds ?? []
10771
- ]);
10772
- const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
10773
- if (id === "workit-review" && matches.length === 0)
10774
- return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
10775
- if (matches.some((capability) => capability.assurance === "unavailable"))
10776
- return {
10777
- assurance: "unavailable",
10778
- reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
10779
- };
10780
- if (matches.some((capability) => capability.assurance === "enforced"))
10781
- return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
10782
- return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
11049
+ var unbalancedQuotes = (segment) => {
11050
+ const text = segment.replace(/\\./gu, "");
11051
+ return (text.match(/"/gu)?.length ?? 0) % 2 !== 0 || (text.match(/'/gu)?.length ?? 0) % 2 !== 0;
10783
11052
  };
10784
- function selectMethods(policy, capabilities) {
10785
- return Object.entries(METHODS).flatMap(([id, definition]) => {
10786
- const requirements = policy.requirements.filter((requirement) => methodMatches(id, definition, requirement));
10787
- if (requirements.length === 0)
10788
- return [];
10789
- const capability = assuranceFor(id, definition, capabilities);
10790
- const reasons = [
10791
- ...new Set(requirements.map((requirement) => requirement.reason)),
10792
- ...capability.reasons
10793
- ];
10794
- if (capability.assurance === "unavailable")
10795
- reasons.unshift("required capability is unavailable");
10796
- return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
10797
- });
11053
+ var segmentTokens = (segment) => {
11054
+ if (segment.includes("`") || segment.includes("$("))
11055
+ return null;
11056
+ if (unbalancedQuotes(segment))
11057
+ return null;
11058
+ const parts = segment.trim().split(/\s+/u);
11059
+ while (parts.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/u.test(parts[0]))
11060
+ parts.shift();
11061
+ return parts.length > 0 ? parts : null;
11062
+ };
11063
+ var unquote = (token) => token.replace(/^['"]|['"]$/gu, "");
11064
+ function shellRouteIntent(command) {
11065
+ if (typeof command !== "string" || command.trim() === "")
11066
+ return null;
11067
+ for (const raw of command.split(/&&|\|\||;|\n/u)) {
11068
+ const parts = segmentTokens(raw);
11069
+ if (!parts)
11070
+ continue;
11071
+ const head = unquote(parts[0]);
11072
+ const second = unquote(parts[1] ?? "");
11073
+ const third = unquote(parts[2] ?? "");
11074
+ if (head === "git" && second === "switch" && (third === "-c" || third === "--create"))
11075
+ return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
11076
+ if (head === "git" && second === "checkout" && (third === "-b" || third === "-B"))
11077
+ return { route: "git.branch_setup", guidance: GUIDANCE["git.branch_setup"] };
11078
+ if (head === "gh" && second === "pr" && third === "create")
11079
+ return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
11080
+ if (head === "glab" && second === "mr" && third === "create")
11081
+ return { route: "hosting.pull_request", guidance: GUIDANCE["hosting.pull_request"] };
11082
+ }
11083
+ return null;
10798
11084
  }
10799
- var invariantBootstrap = () => `
10800
- Workit keeps one accountable lead and one shared task state. Inspect current task
10801
- state before acting. An empty task.list means no session yet, not permission to
10802
- skip Workit; for user-requested product, debug, or behavior work with no active
10803
- or paused task, run task.start then policy.assess before other product mutations.
10804
- Omitted expectedRevision, expectedWorkspaceRevision, and writer workerId default
10805
- to the current records; explicit values are still concurrency-checked, so never
10806
- copy revisions between calls. Happy path: task.list, task.start {intent} with no
10807
- revisions, policy.assess {assessment: {facts, signals, consequences,
10808
- verification}} where facts are inferred or observed with file refs,
10809
- writer.acquire {taskId} before product writes, evidence.record {evidence} where
10810
- check and review kinds auto-bind the current tree and need no digests. Record
10811
- self-review requirements with kind review and reviewContext matching your own
10812
- host session; the lead may also record checks. Fresh-context review requires a
10813
- session distinct from the task creator and every other evidence recorder. Record
10814
- verification evidence last: later evidence or commits can reopen fixed findings
10815
- for re-verification, so verify, resolve, then close with nothing in between.
10816
- decision.record {binding with taskId/workspaceId from inspect plus presented and
10817
- approvedContent} only through a native approval question asked receipt-shaped:
10818
- header Workit decision: <purpose>, the same label repeated in the question
10819
- text (some host UIs do not render headers), exactly two options approved and
10820
- rejected; ask once, never re-ask to mint a receipt. task.close {outcome,
10821
- summary, decisionIds}. A completed native subagent run stops its bound worker
10822
- by itself; if a worker strands in cancelling with its run verifiably over,
10823
- repeat worker.cancel {reason} to confirm the stop. Check and review evidence
10824
- without a bound candidate goes stale; findings close only as fixed with passing
10825
- verification, dismissed with supporting evidence, or deferred under an approved
10826
- limitation. Use only the
10827
- shared operations for task, policy, evidence, finding, decision, worker, writer,
10828
- and state changes. Authority is bounded by the requested scope, current
10829
- revision, caller/session provenance, and observed capabilities. Never claim host
10830
- enforcement or evidence that the host cannot provide. Shell-executed writes are
10831
- unattested agent-guided work even when a writer is held. Preserve unresolved
10832
- requirements, gaps, and uncertain workers. Skill routing: slash aliases /wk-*
10833
- always load on demand. Model-invoked: new instructions, interruptions, or
10834
- forgotten items mid-task → load workit-steer before responding; about to open
10835
- a PR or fresh from implementing → load workit-deslop; CI red → load
10836
- workit-green-run; small-looking change before close or merge → load
10837
- workit-blast-radius; ambiguous, consequential, or disputed proposal before
10838
- recommending → load workit-challenge; spec or plan needs a flow diagram →
10839
- load workit-diagram, a UI sketch → load workit-mockup. Load the skill; never
10840
- act from memory of it.
10841
- `.trim();
10842
11085
  // packages/workit-core/src/core/external-action.ts
11086
+ import { execFileSync } from "node:child_process";
10843
11087
  var externalActionSchema = discriminatedUnion("operation", [
10844
11088
  object({
10845
11089
  operation: literal("git.branch_setup"),
@@ -10852,7 +11096,11 @@ var externalActionSchema = discriminatedUnion("operation", [
10852
11096
  }).strict(),
10853
11097
  object({
10854
11098
  operation: literal("git.commit"),
10855
- 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()
10856
11104
  }).strict(),
10857
11105
  object({
10858
11106
  operation: literal("git.push"),
@@ -10941,7 +11189,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
10941
11189
  payload,
10942
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 } : {}
10943
11191
  });
10944
- 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).";
10945
11193
  var externalActionRef = (host, actor, descriptor) => ({
10946
11194
  kind: "host",
10947
11195
  host,
@@ -10992,6 +11240,92 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
10992
11240
  return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
10993
11241
  return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
10994
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
+ };
10995
11329
  var approvedExternalAction = (store, host, actor, operation) => {
10996
11330
  const selected = externalActionState(store, host, actor, operation);
10997
11331
  if (!selected.ok)
@@ -11000,6 +11334,23 @@ var approvedExternalAction = (store, host, actor, operation) => {
11000
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");
11001
11335
  return selected;
11002
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
+ };
11003
11354
  var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
11004
11355
  let steps = [];
11005
11356
  try {
@@ -11277,7 +11628,7 @@ function resolveRequirements(input) {
11277
11628
  ].join(", ")}`,
11278
11629
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
11279
11630
  before: "dependent_action",
11280
- dependentAction: "dependent-action",
11631
+ dependentAction: null,
11281
11632
  acceptanceAllowed: false
11282
11633
  }));
11283
11634
  if (signals.productChoiceOpen.value === true)
@@ -11288,7 +11639,7 @@ function resolveRequirements(input) {
11288
11639
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
11289
11640
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
11290
11641
  before: "dependent_action",
11291
- dependentAction: "dependent-action",
11642
+ dependentAction: null,
11292
11643
  acceptanceAllowed: false
11293
11644
  }));
11294
11645
  if (signals.behaviorChange.value === true)
@@ -11446,59 +11797,6 @@ function diffPolicy(previous, next, reason, now) {
11446
11797
  return parsed.data;
11447
11798
  }
11448
11799
 
11449
- // packages/workit-core/src/core/workers.ts
11450
- var same = (left, right) => {
11451
- try {
11452
- return canonicalJson(left) === canonicalJson(right);
11453
- } catch {
11454
- return false;
11455
- }
11456
- };
11457
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
11458
- var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
11459
- const code = char.charCodeAt(0);
11460
- return code < 32 || code === 127;
11461
- }) && !value.split(/[\\/]/).includes("..");
11462
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
11463
- var workerIdOf = (value) => value.workerId ?? null;
11464
- function assertProductWriteAllowed(input) {
11465
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
11466
- return failure("invalid_input", "invalid product write path");
11467
- if (!input.store)
11468
- return failure("permission_denied", "write authorization requires core state");
11469
- const task = input.store.readTask(input.task.id);
11470
- if (!task.ok)
11471
- return task;
11472
- const workspace = input.store.readWorkspace();
11473
- if (!workspace.ok)
11474
- return workspace;
11475
- if (!workspace.data)
11476
- return failure("not_found", "workspace not found");
11477
- const workspaceRecord = workspace.data;
11478
- if (task.data.status !== "active")
11479
- return failure("invalid_transition", "paused or closed tasks cannot own product writes");
11480
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
11481
- return failure("recovery_required", "task and workspace bindings are invalid");
11482
- const writer = workspaceRecord.writer;
11483
- if (!writer)
11484
- return failure("permission_denied", "checkout has no writer owner");
11485
- if (writer.state === "uncertain")
11486
- return failure("recovery_required", "checkout writer requires recovery", {
11487
- owner: writer.owner
11488
- });
11489
- const caller = callerValue(input.caller);
11490
- const workerId = workerIdOf(input.caller);
11491
- const owner = writer.owner;
11492
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
11493
- return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
11494
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
11495
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
11496
- return failure("permission_denied", "only implementers can own product writes");
11497
- 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))))
11498
- return failure("recovery_required", "worker session is not an active writer");
11499
- return success(null, workspaceRecord.revision, owner);
11500
- }
11501
-
11502
11800
  // packages/workit-core/src/core/task-engine.ts
11503
11801
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
11504
11802
  kind,
@@ -11617,10 +11915,10 @@ var applyWorkerLifecycle = (input) => {
11617
11915
  const notStarted = input.dispatch === true;
11618
11916
  if (notStarted && (input.state !== "stopped" || input.session !== null))
11619
11917
  return failure("invalid_input", "a never-dispatched worker stops with no session");
11620
- if (notStarted && (entry.data.state === "stopped" || entry.data.state === "unknown"))
11621
- return failure("invalid_transition", "worker is no longer awaiting dispatch");
11622
- if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
11918
+ if (notStarted && entry.data.session !== null)
11623
11919
  return failure("invalid_transition", "worker already has an observed session");
11920
+ if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
11921
+ return failure("invalid_transition", "worker launch was not claimed");
11624
11922
  if ((input.state === "running" || input.state === "cancelling" || input.state === "stopped" && !notStarted) && !input.session)
11625
11923
  return failure("invalid_input", "running worker observations require a session");
11626
11924
  if (entry.data.session && !sameValue(entry.data.session, input.session))
@@ -11629,15 +11927,20 @@ var applyWorkerLifecycle = (input) => {
11629
11927
  return failure("invalid_transition", "worker has not started");
11630
11928
  if (input.state === "running" && entry.data.state === "stopped")
11631
11929
  return failure("invalid_transition", "stopped worker cannot run again");
11930
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
11632
11931
  const nextEntry = {
11633
11932
  ...entry,
11634
11933
  recordedAt: input.now ?? entry.recordedAt,
11635
11934
  provenance: authority.provenance,
11636
- 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
+ }
11637
11940
  };
11638
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;
11639
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;
11640
- 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)
11641
11944
  return success(task.data.revision, workspace.data.revision, entry);
11642
11945
  const changed = input.store.mutateTaskAndWorkspace({
11643
11946
  taskId: task.data.id,
@@ -12007,7 +12310,7 @@ class WorkitCore {
12007
12310
  activeWorkerBlocker(task, workspace) {
12008
12311
  if (workspace.writer)
12009
12312
  return failure("recovery_required", "writer ownership must be released first");
12010
- if (task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
12313
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
12011
12314
  return failure("recovery_required", "worker state requires reconciliation");
12012
12315
  return null;
12013
12316
  }
@@ -12244,11 +12547,6 @@ class WorkitCore {
12244
12547
  if (helper && !helper.ok)
12245
12548
  return helper;
12246
12549
  const evidence = input.evidence;
12247
- if (helper?.ok) {
12248
- const assignment = helper.data.data.assignment;
12249
- 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))
12250
- return failure("permission_denied", "evidence is outside the worker assignment");
12251
- }
12252
12550
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
12253
12551
  return failure("invalid_input", "missing or skipped evidence requires a reason");
12254
12552
  if (evidence.kind === "review") {
@@ -12265,6 +12563,11 @@ class WorkitCore {
12265
12563
  evidence.beforeCandidateId ??= null;
12266
12564
  evidence.candidateId ??= null;
12267
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
+ }
12268
12571
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
12269
12572
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
12270
12573
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -12771,7 +13074,12 @@ class WorkitCore {
12771
13074
  return failure("invalid_transition", "worker is not awaiting dispatch");
12772
13075
  return success(mutation.revision, null, {
12773
13076
  ...current,
12774
- workers: current.workers.map((item) => item.id === input.workerId ? { ...item, recordedAt: mutation.now, provenance: attested.data } : item)
13077
+ workers: current.workers.map((item) => item.id === input.workerId ? {
13078
+ ...item,
13079
+ recordedAt: mutation.now,
13080
+ provenance: attested.data,
13081
+ data: { ...item.data, state: "dispatching" }
13082
+ } : item)
12775
13083
  });
12776
13084
  }
12777
13085
  });
@@ -13155,6 +13463,9 @@ class WorkitCore {
13155
13463
  id: task.id,
13156
13464
  revision: task.revision,
13157
13465
  workspaceRevision: workspace.data.revision,
13466
+ createdAt: task.createdAt,
13467
+ updatedAt: task.updatedAt,
13468
+ runtime: task.runtime ?? null,
13158
13469
  objective: task.intent.data.objective,
13159
13470
  status: task.status,
13160
13471
  closure: task.closure,
@@ -13240,7 +13551,7 @@ class WorkitCore {
13240
13551
  const base = reconcileResume(effectiveView);
13241
13552
  if (!base.ok)
13242
13553
  return base;
13243
- const needsReconciliation = view.task.workers.some((entry) => ["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));
13244
13555
  const blockers = [...base.data.blockers];
13245
13556
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
13246
13557
  blockers.push({
@@ -13307,7 +13618,8 @@ class WorkitCore {
13307
13618
  ...current,
13308
13619
  status,
13309
13620
  candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
13310
- progress: status === "paused" ? { ...current.progress, summary: input.reason } : current.progress
13621
+ progress: current.progress,
13622
+ pauseReason: status === "paused" ? input.reason ?? null : null
13311
13623
  })
13312
13624
  });
13313
13625
  if (!changed.ok)
@@ -13343,7 +13655,7 @@ class WorkitCore {
13343
13655
  return workspace;
13344
13656
  if (!workspace.data)
13345
13657
  return failure("not_found", "workspace not found");
13346
- 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)))
13347
13659
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
13348
13660
  this.fillRevisions(input, task.data, workspace.data);
13349
13661
  const changed = this.store.mutateTaskAndWorkspace({
@@ -13461,15 +13773,15 @@ function resolveGitRevision(root, value) {
13461
13773
  }
13462
13774
  function resolveInside(root, candidate) {
13463
13775
  const base = realpathSync5(root);
13464
- const target = path6.resolve(base, candidate);
13465
- if (target !== base && !target.startsWith(base + path6.sep)) {
13776
+ const target = path7.resolve(base, candidate);
13777
+ if (target !== base && !target.startsWith(base + path7.sep)) {
13466
13778
  throw new Error("path must stay inside repository root");
13467
13779
  }
13468
13780
  let ancestor = target;
13469
- while (!existsSync3(ancestor))
13470
- ancestor = path6.dirname(ancestor);
13781
+ while (!existsSync4(ancestor))
13782
+ ancestor = path7.dirname(ancestor);
13471
13783
  const canonicalAncestor = realpathSync5(ancestor);
13472
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path6.sep)) {
13784
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path7.sep)) {
13473
13785
  throw new Error("path must stay inside repository root");
13474
13786
  }
13475
13787
  return target;
@@ -13512,6 +13824,13 @@ var piCapabilities = (ctx) => [
13512
13824
  assurance: "agent_guided",
13513
13825
  reason: "Pi extensions do not sandbox arbitrary shell commands.",
13514
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")]
13515
13834
  }
13516
13835
  ];
13517
13836
  var piContext = (ctx) => ({
@@ -13534,14 +13853,12 @@ var workitContext = (ctx) => {
13534
13853
 
13535
13854
  Native Pi session: ${session}. Project-local Workit state is unavailable until Pi trusts this project.`;
13536
13855
  let taskContext = null;
13537
- let methods = [];
13538
13856
  try {
13539
13857
  const store = new TaskStore(ctx.cwd);
13540
13858
  const tasks = store.listTasks();
13541
13859
  if (tasks.ok) {
13542
13860
  const task = tasks.data.filter((entry) => entry.status !== "closed" && (entry.intent.provenance.session?.kind === "host" && entry.intent.provenance.session.host === "pi" && entry.intent.provenance.session.handle === session || entry.workers.some((worker) => worker.data.session?.kind === "host" && worker.data.session.host === "pi" && worker.data.session.handle === session))).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
13543
13861
  if (task) {
13544
- methods = task.policy ? selectMethods(task.policy, piCapabilities(ctx)) : [];
13545
13862
  const core = new WorkitCore(store, piContext(ctx));
13546
13863
  const view = core.task({
13547
13864
  schemaVersion: 1,
@@ -13554,13 +13871,11 @@ Native Pi session: ${session}. Project-local Workit state is unavailable until P
13554
13871
  }
13555
13872
  }
13556
13873
  } catch {}
13557
- const methodText = methods.length ? `
13558
- Selected methods: ${methods.map((method) => `${method.id} (${method.assurance})`).join(", ")}.` : "";
13559
13874
  const taskText = taskContext ? `
13560
13875
  Current task context: ${taskContext}` : "";
13561
13876
  return `${invariantBootstrap()}
13562
13877
 
13563
- Native Pi session: ${session}.${methodText}${taskText}`;
13878
+ Native Pi session: ${session}.${taskText}`;
13564
13879
  };
13565
13880
 
13566
13881
  // packages/workit-core/src/core/skill-manifests.ts
@@ -13583,15 +13898,15 @@ var WORKIT_SKILL_ALIASES = {
13583
13898
  if (false) {}
13584
13899
 
13585
13900
  // packages/workit-core/src/core/branch.ts
13586
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
13587
- import { execFileSync as execFileSync2 } from "node:child_process";
13588
- 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";
13589
13904
 
13590
13905
  // packages/workit-core/src/core/git.ts
13591
- import { execFileSync } from "node:child_process";
13906
+ import { execFileSync as execFileSync2 } from "node:child_process";
13592
13907
  var run2 = (cwd, args) => {
13593
13908
  try {
13594
- const stdout = execFileSync("git", args, {
13909
+ const stdout = execFileSync2("git", args, {
13595
13910
  cwd,
13596
13911
  encoding: "utf8",
13597
13912
  stdio: ["pipe", "pipe", "pipe"]
@@ -13659,14 +13974,14 @@ var gitContext = (workspaceRoot, paths = []) => {
13659
13974
  import {
13660
13975
  copyFileSync,
13661
13976
  cpSync,
13662
- existsSync as existsSync4,
13977
+ existsSync as existsSync5,
13663
13978
  mkdirSync as mkdirSync2,
13664
13979
  readFileSync as readFileSync4,
13665
13980
  readdirSync as readdirSync3,
13666
13981
  writeFileSync as writeFileSync2
13667
13982
  } from "node:fs";
13668
13983
  import os from "node:os";
13669
- import path7 from "node:path";
13984
+ import path8 from "node:path";
13670
13985
 
13671
13986
  // packages/workit-core/src/core/boundary.ts
13672
13987
  var EVENT = {
@@ -13710,7 +14025,7 @@ var mergePreset = (preset, input = {}, current = {
13710
14025
  protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
13711
14026
  };
13712
14027
  };
13713
- 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");
13714
14029
  var migratedDir = null;
13715
14030
  var migrationFailed = false;
13716
14031
  var ensureConfigDir = (dir = resolveConfigDir()) => {
@@ -13720,12 +14035,12 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
13720
14035
  migratedDir = dir;
13721
14036
  return dir;
13722
14037
  }
13723
- const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workflow-toolkit");
13724
- if (!existsSync4(legacy)) {
14038
+ const legacy = path8.join(process.env.XDG_CONFIG_HOME || path8.join(os.homedir(), ".config"), "workflow-toolkit");
14039
+ if (!existsSync5(legacy)) {
13725
14040
  migratedDir = dir;
13726
14041
  return dir;
13727
14042
  }
13728
- if (!migrationFailed && existsSync4(dir)) {
14043
+ if (!migrationFailed && existsSync5(dir)) {
13729
14044
  migratedDir = dir;
13730
14045
  return dir;
13731
14046
  }
@@ -13733,9 +14048,9 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
13733
14048
  mkdirSync2(dir, { recursive: true });
13734
14049
  diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
13735
14050
  for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
13736
- const src = path7.join(legacy, entry.name);
13737
- const dest = path7.join(dir, entry.name);
13738
- if (existsSync4(dest))
14051
+ const src = path8.join(legacy, entry.name);
14052
+ const dest = path8.join(dir, entry.name);
14053
+ if (existsSync5(dest))
13739
14054
  continue;
13740
14055
  try {
13741
14056
  if (entry.isDirectory())
@@ -13808,7 +14123,7 @@ var parseConfigResult = (raw, file) => {
13808
14123
  };
13809
14124
  };
13810
14125
  var readConfigTyped = (dir) => {
13811
- const file = path7.join(dir ?? configDir(), "config.json");
14126
+ const file = path8.join(dir ?? configDir(), "config.json");
13812
14127
  return parseConfigResult(readSafe(file), file);
13813
14128
  };
13814
14129
  var readConfig = () => {
@@ -13854,9 +14169,9 @@ var resolveCommitPolicy = (config, workspace) => {
13854
14169
 
13855
14170
  // packages/workit-core/src/core/workspaces.ts
13856
14171
  import { readFileSync as readFileSync5, realpathSync as realpathSync6 } from "node:fs";
13857
- import path8 from "node:path";
14172
+ import path9 from "node:path";
13858
14173
  var readWorkspacesResult = (dir = configDir()) => {
13859
- const file = path8.join(dir, "workspaces.json");
14174
+ const file = path9.join(dir, "workspaces.json");
13860
14175
  let raw;
13861
14176
  try {
13862
14177
  raw = readFileSync5(file, "utf8");
@@ -13954,10 +14269,10 @@ var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
13954
14269
 
13955
14270
  // packages/workit-core/src/core/vcs-config.ts
13956
14271
  import fs7 from "node:fs";
13957
- import path9 from "node:path";
14272
+ import path10 from "node:path";
13958
14273
  import { spawnSync as spawnSync3 } from "node:child_process";
13959
14274
  var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
13960
- 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");
13961
14276
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
13962
14277
  var remoteProvider = (cwd) => {
13963
14278
  const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
@@ -14040,8 +14355,8 @@ function vcsConfig(mode, cwd) {
14040
14355
  }
14041
14356
  const prov = cfg[provider] ?? {};
14042
14357
  const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
14043
- const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path9.join(configDir(), `${provider}.token`));
14044
- const tokenPath = path9.resolve(tokenFile);
14358
+ const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path10.join(configDir(), `${provider}.token`));
14359
+ const tokenPath = path10.resolve(tokenFile);
14045
14360
  let tokenOk = false;
14046
14361
  if (fs7.existsSync(tokenPath)) {
14047
14362
  const token = fs7.readFileSync(tokenPath, "utf8").trim();
@@ -14050,7 +14365,7 @@ function vcsConfig(mode, cwd) {
14050
14365
  }
14051
14366
  const out = {
14052
14367
  ok: true,
14053
- configPath: path9.resolve(cfgPath),
14368
+ configPath: path10.resolve(cfgPath),
14054
14369
  provider,
14055
14370
  defaultTargetBranch: defaultTarget,
14056
14371
  pr: cfg.pr ?? {},
@@ -14151,7 +14466,7 @@ var baseBranch = (cwd) => {
14151
14466
  return { base: String(resolved.defaultTargetBranch ?? "develop") };
14152
14467
  };
14153
14468
  var originBaseReady = (cwd, base) => {
14154
- 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"] });
14155
14470
  try {
14156
14471
  try {
14157
14472
  run(["fetch", "origin", base, "--prune"]);
@@ -14159,7 +14474,7 @@ var originBaseReady = (cwd, base) => {
14159
14474
  run(["fetch", "origin", "--prune"]);
14160
14475
  }
14161
14476
  try {
14162
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
14477
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
14163
14478
  cwd,
14164
14479
  stdio: "pipe"
14165
14480
  });
@@ -14178,11 +14493,11 @@ var originBaseReady = (cwd, base) => {
14178
14493
  }
14179
14494
  };
14180
14495
  var fastForwardBase = (cwd, base) => {
14181
- 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"] });
14182
14497
  try {
14183
14498
  let hasLocalBase = true;
14184
14499
  try {
14185
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
14500
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
14186
14501
  cwd,
14187
14502
  stdio: "pipe"
14188
14503
  });
@@ -14222,19 +14537,18 @@ var branchSetup = ({
14222
14537
  workspace_root,
14223
14538
  log
14224
14539
  }) => {
14225
- const cwd = path10.resolve(workspace_root);
14226
- 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"] });
14227
14542
  const current = gitContext(cwd).branch;
14228
14543
  if (!current || current === "unknown")
14229
- return { error: "not in a git repository" };
14544
+ return { error: "not in a git repository", phase: "preflight" };
14230
14545
  const sdd = sdd_dir ?? "docs";
14231
- 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");
14232
14547
  const ensureManifestDir = () => {
14233
- const dir = path10.dirname(manifestPath);
14234
- if (!existsSync5(dir))
14548
+ const dir = path11.dirname(manifestPath);
14549
+ if (!existsSync6(dir))
14235
14550
  mkdirSync3(dir, { recursive: true, mode: 493 });
14236
14551
  };
14237
- ensureManifestDir();
14238
14552
  const readManifest = () => {
14239
14553
  try {
14240
14554
  return JSON.parse(readFileSync6(manifestPath, "utf8"));
@@ -14252,13 +14566,16 @@ var branchSetup = ({
14252
14566
  const manifest = readManifest();
14253
14567
  const ref = manifest.stash_ref;
14254
14568
  if (!ref)
14255
- return { error: "no stash_ref in manifest" };
14569
+ return { error: "no stash_ref in manifest", phase: "preflight" };
14256
14570
  journal(`pre-pop: ${String(ref)}`);
14257
14571
  try {
14258
14572
  exec(["stash", "pop", String(ref)]);
14259
14573
  } catch (error) {
14260
14574
  journal("pop: failed");
14261
- 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
+ };
14262
14579
  }
14263
14580
  journal("pop: ok");
14264
14581
  delete manifest.stash_ref;
@@ -14268,11 +14585,17 @@ var branchSetup = ({
14268
14585
  }
14269
14586
  const target = target_branch ?? "";
14270
14587
  if (!target)
14271
- 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
+ };
14272
14592
  if (isProtected(cwd, target))
14273
- return { error: `protected branch ${target}` };
14593
+ return { error: `protected branch ${target}`, phase: "preflight" };
14274
14594
  if (!allowedBranch(cwd, target))
14275
- 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
+ };
14276
14599
  let base;
14277
14600
  let targetExists = true;
14278
14601
  try {
@@ -14283,12 +14606,12 @@ var branchSetup = ({
14283
14606
  if (!targetExists) {
14284
14607
  const baseResolved = baseBranch(cwd);
14285
14608
  if ("error" in baseResolved)
14286
- return { error: baseResolved.error };
14609
+ return { error: baseResolved.error, phase: "preflight" };
14287
14610
  base = baseResolved.base;
14288
14611
  }
14289
14612
  journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
14290
14613
  let stash_ref;
14291
- const failAfterStash = (message) => {
14614
+ const failAfterStash = (message, phase = "post_mutation") => {
14292
14615
  let suffix = "";
14293
14616
  if (stash_ref) {
14294
14617
  journal(`pre-pop: ${stash_ref}`);
@@ -14301,27 +14624,31 @@ var branchSetup = ({
14301
14624
  suffix = " (changes preserved in stash)";
14302
14625
  }
14303
14626
  }
14304
- return { error: `${message}${suffix}` };
14627
+ return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
14305
14628
  };
14306
14629
  if (current !== target) {
14307
14630
  const dirty = Boolean(gitContext(cwd).status_short.trim());
14308
14631
  if (dirty && stash !== "yes") {
14309
14632
  return {
14310
- 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"
14311
14635
  };
14312
14636
  }
14313
14637
  let validatedBase;
14314
14638
  if (!targetExists && base !== undefined) {
14315
14639
  const ready = originBaseReady(cwd, base);
14316
14640
  if (!ready.ok)
14317
- return { error: ready.error ?? "ensure-base-branch failed" };
14641
+ return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
14318
14642
  validatedBase = base;
14319
14643
  }
14320
14644
  if (dirty) {
14321
14645
  try {
14322
14646
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
14323
14647
  } catch (error) {
14324
- 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
+ };
14325
14652
  }
14326
14653
  stash_ref = "stash@{0}";
14327
14654
  journal(`stash push: ${stash_ref}`);
@@ -14332,14 +14659,14 @@ var branchSetup = ({
14332
14659
  } catch (error) {
14333
14660
  const message = error instanceof Error ? error.message : "checkout failed";
14334
14661
  if (/worktree/i.test(message)) {
14335
- 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");
14336
14663
  }
14337
14664
  try {
14338
14665
  let effectiveBase = base;
14339
14666
  if (effectiveBase === undefined) {
14340
14667
  const lateResolved = baseBranch(cwd);
14341
14668
  if ("error" in lateResolved)
14342
- return failAfterStash(lateResolved.error);
14669
+ return failAfterStash(lateResolved.error, "preflight");
14343
14670
  effectiveBase = lateResolved.base;
14344
14671
  }
14345
14672
  const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
@@ -14386,7 +14713,7 @@ var branchSetup = ({
14386
14713
 
14387
14714
  // packages/workit-core/src/core/pr-create.ts
14388
14715
  import fs8 from "node:fs";
14389
- import path11 from "node:path";
14716
+ import path12 from "node:path";
14390
14717
  import { spawnSync as spawnSync4 } from "node:child_process";
14391
14718
  function parseGhRepo(remote) {
14392
14719
  remote = (remote || "").trim().replace(/\/+$/, "");
@@ -14451,11 +14778,11 @@ ${line}` : line;
14451
14778
  }
14452
14779
  function whichOnPath(tool) {
14453
14780
  const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
14454
- for (const dir of (process.env.PATH ?? "").split(path11.delimiter)) {
14781
+ for (const dir of (process.env.PATH ?? "").split(path12.delimiter)) {
14455
14782
  if (!dir)
14456
14783
  continue;
14457
14784
  for (const name of names) {
14458
- const candidate = path11.join(dir, name);
14785
+ const candidate = path12.join(dir, name);
14459
14786
  try {
14460
14787
  fs8.accessSync(candidate, fs8.constants.X_OK);
14461
14788
  return candidate;
@@ -14558,7 +14885,7 @@ function prCreate(env, cwd) {
14558
14885
  const draft = String(env.WF_PR_DRAFT ?? "false").toLowerCase() === "true";
14559
14886
  let baseUrl = cfg.youtrack_base_url;
14560
14887
  if (!baseUrl) {
14561
- 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");
14562
14889
  try {
14563
14890
  const yt = JSON.parse(fs8.readFileSync(ytCfg, "utf8"));
14564
14891
  if (yt && typeof yt === "object")
@@ -14665,8 +14992,8 @@ function prCreate(env, cwd) {
14665
14992
 
14666
14993
  // packages/workit-core/src/core/repo-context.ts
14667
14994
  import { spawnSync as spawnSync5 } from "node:child_process";
14668
- import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
14669
- 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";
14670
14997
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
14671
14998
  try {
14672
14999
  gitRevisionParts(value);
@@ -14800,7 +15127,7 @@ var findPrTemplate = (cwd) => {
14800
15127
  ]) {
14801
15128
  const parts = rel.split("/");
14802
15129
  const base = parts[parts.length - 1];
14803
- 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;
14804
15131
  const actual = probeCaseInsensitive(dir, base);
14805
15132
  if (actual)
14806
15133
  return [...parts.slice(0, -1), actual].join("/");
@@ -14831,8 +15158,8 @@ var diffStatForRange = (cwd, range) => {
14831
15158
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
14832
15159
  };
14833
15160
  var packageScripts = (cwd, keys) => {
14834
- const pkgPath = path12.join(cwd, "package.json");
14835
- if (!existsSync6(pkgPath))
15161
+ const pkgPath = path13.join(cwd, "package.json");
15162
+ if (!existsSync7(pkgPath))
14836
15163
  return [];
14837
15164
  try {
14838
15165
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
@@ -14857,9 +15184,9 @@ var documentationFiles = (cwd) => {
14857
15184
  if (entry.isDirectory()) {
14858
15185
  if (excluded.has(entry.name))
14859
15186
  continue;
14860
- walk(path12.join(dir, entry.name), depth + 1);
15187
+ walk(path13.join(dir, entry.name), depth + 1);
14861
15188
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
14862
- matches.push("./" + path12.relative(cwd, path12.join(dir, entry.name)).replaceAll("\\", "/"));
15189
+ matches.push("./" + path13.relative(cwd, path13.join(dir, entry.name)).replaceAll("\\", "/"));
14863
15190
  }
14864
15191
  }
14865
15192
  };
@@ -14867,7 +15194,7 @@ var documentationFiles = (cwd) => {
14867
15194
  return matches.sort().slice(0, 200);
14868
15195
  };
14869
15196
  var readTrimmed = (file, maxLines) => {
14870
- if (!existsSync6(file))
15197
+ if (!existsSync7(file))
14871
15198
  return "";
14872
15199
  const lines = readFileSync7(file, "utf8").split(`
14873
15200
  `);
@@ -14875,7 +15202,7 @@ var readTrimmed = (file, maxLines) => {
14875
15202
  `);
14876
15203
  };
14877
15204
  function prReadyContext(root, range) {
14878
- const cwd = path12.resolve(repoRoot2(root));
15205
+ const cwd = path13.resolve(repoRoot2(root));
14879
15206
  if (range !== undefined && !isResolvableContextRange(cwd, range))
14880
15207
  return invalidRange(cwd);
14881
15208
  let stdout = "";
@@ -14933,7 +15260,7 @@ ${status.stderr}` : "");
14933
15260
  stdout += `template_path: ${template}
14934
15261
 
14935
15262
  `;
14936
- stdout += readTrimmed(path12.join(cwd, template), 220);
15263
+ stdout += readTrimmed(path13.join(cwd, template), 220);
14937
15264
  } else {
14938
15265
  stdout += `template_path: none
14939
15266
 
@@ -14943,7 +15270,7 @@ ${status.stderr}` : "");
14943
15270
  stdout += `
14944
15271
  `;
14945
15272
  stdout += printSection("Recent Validation Signals");
14946
- if (existsSync6(path12.join(cwd, "package.json"))) {
15273
+ if (existsSync7(path13.join(cwd, "package.json"))) {
14947
15274
  stdout += `package.json detected. Common scripts:
14948
15275
  `;
14949
15276
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -14952,7 +15279,7 @@ ${status.stderr}` : "");
14952
15279
  `) + `
14953
15280
  `;
14954
15281
  }
14955
- 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"))) {
14956
15283
  stdout += `Rust project detected.
14957
15284
  `;
14958
15285
  }
@@ -14985,7 +15312,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
14985
15312
  - Apply with the native workit_changelog_apply tool only (not hand-edits under Unreleased).
14986
15313
  - If Unreleased already has duplicate category headings, normalize_only first.`;
14987
15314
  function changelogContext(root, range) {
14988
- const cwd = path12.resolve(repoRoot2(root));
15315
+ const cwd = path13.resolve(repoRoot2(root));
14989
15316
  if (range !== undefined && !isResolvableContextRange(cwd, range))
14990
15317
  return invalidRange(cwd);
14991
15318
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -15003,8 +15330,8 @@ function changelogContext(root, range) {
15003
15330
  stdout += CHANGELOG_RULES + `
15004
15331
  `;
15005
15332
  stdout += printSection("Existing CHANGELOG.md");
15006
- const changelogPath = path12.join(cwd, "CHANGELOG.md");
15007
- if (existsSync6(changelogPath)) {
15333
+ const changelogPath = path13.join(cwd, "CHANGELOG.md");
15334
+ if (existsSync7(changelogPath)) {
15008
15335
  stdout += readTrimmed(changelogPath, 260) + `
15009
15336
  `;
15010
15337
  } else {
@@ -15023,7 +15350,7 @@ function changelogContext(root, range) {
15023
15350
  return { stdout, stderr: "", exitCode: 0, cwd };
15024
15351
  }
15025
15352
  function docsRefreshContext(root, range) {
15026
- const cwd = path12.resolve(repoRoot2(root));
15353
+ const cwd = path13.resolve(repoRoot2(root));
15027
15354
  if (range !== undefined && !isResolvableContextRange(cwd, range))
15028
15355
  return invalidRange(cwd);
15029
15356
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -15045,8 +15372,8 @@ function docsRefreshContext(root, range) {
15045
15372
  `) + `
15046
15373
  `;
15047
15374
  stdout += printSection("README Preview");
15048
- const readme = path12.join(cwd, "README.md");
15049
- if (existsSync6(readme)) {
15375
+ const readme = path13.join(cwd, "README.md");
15376
+ if (existsSync7(readme)) {
15050
15377
  stdout += readTrimmed(readme, 220) + `
15051
15378
  `;
15052
15379
  } else {
@@ -15054,8 +15381,8 @@ function docsRefreshContext(root, range) {
15054
15381
  `;
15055
15382
  }
15056
15383
  stdout += printSection("Package Scripts");
15057
- const pkgPath = path12.join(cwd, "package.json");
15058
- if (existsSync6(pkgPath)) {
15384
+ const pkgPath = path13.join(cwd, "package.json");
15385
+ if (existsSync7(pkgPath)) {
15059
15386
  try {
15060
15387
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
15061
15388
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
@@ -15068,7 +15395,7 @@ function docsRefreshContext(root, range) {
15068
15395
  return { stdout, stderr: "", exitCode: 0, cwd };
15069
15396
  }
15070
15397
  function releaseNotesContext(root, rangeOrTag) {
15071
- const cwd = path12.resolve(repoRoot2(root));
15398
+ const cwd = path13.resolve(repoRoot2(root));
15072
15399
  if (!rangeOrTag) {
15073
15400
  return { stdout: "", stderr: `ERROR: release tag or range required
15074
15401
  `, exitCode: 1, cwd };
@@ -15106,7 +15433,7 @@ function releaseNotesContext(root, rangeOrTag) {
15106
15433
  `;
15107
15434
  stdout += printSection("Existing Release Files");
15108
15435
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
15109
- if (existsSync6(path12.join(cwd, rel)))
15436
+ if (existsSync7(path13.join(cwd, rel)))
15110
15437
  stdout += rel + `
15111
15438
  `;
15112
15439
  }
@@ -15297,25 +15624,6 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
15297
15624
  import fs10 from "node:fs";
15298
15625
  import path14 from "node:path";
15299
15626
 
15300
- // packages/workit-core/src/core/package-root.ts
15301
- import { existsSync as existsSync7 } from "node:fs";
15302
- import path13 from "node:path";
15303
- import { fileURLToPath } from "node:url";
15304
- var packageRoot = () => {
15305
- let dir = path13.dirname(fileURLToPath(import.meta.url));
15306
- while (true) {
15307
- const parent = path13.dirname(dir);
15308
- if (existsSync7(path13.join(dir, "package.json")) || parent === dir)
15309
- return dir;
15310
- dir = parent;
15311
- }
15312
- };
15313
- var assetRoot = () => {
15314
- const root = packageRoot();
15315
- const assets = path13.join(root, "assets");
15316
- return existsSync7(assets) ? assets : root;
15317
- };
15318
-
15319
15627
  // packages/workit-core/src/core/templates.ts
15320
15628
  var repoRoot3 = assetRoot();
15321
15629
 
@@ -16521,6 +16829,94 @@ var safePath = (root, value) => {
16521
16829
  return null;
16522
16830
  }
16523
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
+ };
16524
16920
  var approvedResolvedExternalAction = (content) => {
16525
16921
  try {
16526
16922
  const value = JSON.parse(content);
@@ -16784,24 +17180,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16784
17180
  const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
16785
17181
  if (!sdd_dir)
16786
17182
  return failure("invalid_input", "sdd_dir must stay inside the workspace");
17183
+ const action = request.payload.action ?? "setup";
16787
17184
  const policy = resolveBranchPolicyFor(root);
16788
- 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
+ });
16789
17191
  const head = gitValue(root, ["rev-parse", "HEAD"]);
16790
17192
  if (!head)
16791
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);
16792
17196
  const normalized = {
16793
17197
  operation: request.operation,
16794
17198
  payload: {
16795
17199
  ...request.payload,
16796
- action: request.payload.action ?? "setup",
17200
+ action,
16797
17201
  sdd_dir,
16798
17202
  stash: request.payload.stash ?? "no",
16799
- target_branch
17203
+ ...target_branch ? { target_branch } : {}
16800
17204
  }
16801
17205
  };
16802
17206
  return success(null, null, {
16803
17207
  request: normalized,
16804
- descriptorPayload: { ...normalized.payload, resolved: { head, target_branch } }
17208
+ descriptorPayload: {
17209
+ ...normalized.payload,
17210
+ resolved: { head, dirty, ...creation }
17211
+ }
16805
17212
  });
16806
17213
  }
16807
17214
  case "git.push": {
@@ -16815,7 +17222,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16815
17222
  }) : failure("invalid_input", "git push requires a resolvable current branch");
16816
17223
  }
16817
17224
  case "git.commit": {
17225
+ const planSteps = request.payload.plan_steps;
17226
+ const planBranch = request.payload.plan_branch;
16818
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
+ }
16819
17256
  const staged = gitRaw(root, [
16820
17257
  "diff",
16821
17258
  "--cached",
@@ -16826,7 +17263,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16826
17263
  "HEAD"
16827
17264
  ]);
16828
17265
  const paths = stagedPaths(root);
16829
- 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)
16830
17273
  return failure("storage_error", "Git state could not be resolved");
16831
17274
  let policy;
16832
17275
  try {
@@ -16841,13 +17284,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
16841
17284
  `).filter(Boolean));
16842
17285
  flavor = detected.flavor ?? "conventional";
16843
17286
  }
16844
- if (!matchCommitFlavor(request.payload.message, flavor, policy.pattern))
17287
+ if (!matchCommitFlavor(message, flavor, policy.pattern))
16845
17288
  return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
16846
17289
  return success(null, null, {
16847
17290
  request,
16848
17291
  descriptorPayload: {
16849
17292
  ...request.payload,
16850
- resolved: { head, staged: sha256(staged), paths }
17293
+ resolved: { head, branch, staged: sha256(staged), paths }
16851
17294
  }
16852
17295
  });
16853
17296
  }
@@ -17017,10 +17460,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
17017
17460
  sdd_dir: request.payload.sdd_dir ?? "docs",
17018
17461
  workspace_root: root
17019
17462
  });
17020
- 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);
17021
17472
  }
17022
17473
  case "git.commit": {
17023
- 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]);
17024
17481
  return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown2(request.operation);
17025
17482
  }
17026
17483
  case "git.push": {
@@ -17193,8 +17650,34 @@ var nativeAuthority = (actor, reconciliationTokens = new WeakSet) => ({
17193
17650
  var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operation) => {
17194
17651
  const store = new TaskStore(root);
17195
17652
  const selected = approvedExternalAction(store, "pi", actor, operation);
17196
- 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
+ }
17197
17679
  return selected;
17680
+ }
17198
17681
  const actionRef = externalActionRef("pi", actor, operation);
17199
17682
  return {
17200
17683
  taskId: selected.data.task.id,
@@ -17260,6 +17743,21 @@ var executeFamily = async (family, toolCallId, input, ctx) => {
17260
17743
  operation: "decision.record"
17261
17744
  }));
17262
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));
17263
17761
  const approved = await ctx.ui.confirm("Workit decision", decision.binding.presented);
17264
17762
  if (approved !== (decision.response === "approved"))
17265
17763
  return output(failure("permission_denied", "Pi decision answer did not match the requested response"));
@@ -17332,6 +17830,22 @@ var registerWorkitTools = (pi, options = {}) => {
17332
17830
  ...context,
17333
17831
  nativeAuthority: nativeAuthority(actor, reconciliationTokens)
17334
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
+ }
17335
17849
  const descriptor = externalActionDescriptor(resolved.data.request.operation, resolved.data.descriptorPayload);
17336
17850
  const priorRequest = priorExternalAction(store, "pi", actor, resolved.data.request.operation, resolved.data.request.payload);
17337
17851
  if (priorRequest.ok && priorRequest.data.entry.data.consumption?.state === "uncertain") {
@@ -17370,8 +17884,18 @@ var registerWorkitTools = (pi, options = {}) => {
17370
17884
  }
17371
17885
  if (priorRequest.ok && priorRequest.data.entry.data.consumption !== null)
17372
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);
17373
17890
  const existing = approvedExternalAction(store, "pi", actor, descriptor);
17374
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
+ }
17375
17899
  const prior = externalActionState(store, "pi", actor, descriptor);
17376
17900
  if (prior.ok && prior.data.entry.data.consumption !== null)
17377
17901
  return output(failure("permission_denied", "external action was already settled"));
@@ -17383,7 +17907,8 @@ var registerWorkitTools = (pi, options = {}) => {
17383
17907
  if (candidates.length !== 1)
17384
17908
  return output(failure("permission_denied", "external action requires exactly one active Pi task"));
17385
17909
  const task = candidates[0];
17386
- 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);
17387
17912
  if (!approved)
17388
17913
  return output(failure("permission_denied", "external action was not approved"));
17389
17914
  const currentWorkspace = store.readWorkspace();
@@ -17399,8 +17924,9 @@ var registerWorkitTools = (pi, options = {}) => {
17399
17924
  taskId: task.id,
17400
17925
  workspaceId: currentWorkspace.data.id,
17401
17926
  scope: task.intent.data.scope,
17402
- presented: `Approve ${descriptor}`,
17927
+ presented: question.presented,
17403
17928
  approvedContent: descriptor,
17929
+ displayed: question.approvedText,
17404
17930
  contentRefs: []
17405
17931
  },
17406
17932
  response: "approved",
@@ -17409,12 +17935,25 @@ var registerWorkitTools = (pi, options = {}) => {
17409
17935
  if (!decision.ok)
17410
17936
  return output(decision);
17411
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 }));
17412
17941
  const result = await nativeExternalActionRunner(ctx.cwd, actor, core)(descriptor, (step) => executeResolvedExternalAction(resolved.data, ctx.cwd, step, { host: "pi", actor }));
17413
17942
  return output(result);
17414
17943
  }
17415
17944
  });
17416
17945
  };
17417
17946
  var enforceNativeWriter = (event, ctx) => {
17947
+ if (event.toolName === "bash") {
17948
+ const command = event.input?.command;
17949
+ const route = typeof command === "string" ? shellRouteIntent(command) : null;
17950
+ if (route)
17951
+ return {
17952
+ block: true,
17953
+ reason: `direct branch or PR creation bypasses the Workit route; ${route.guidance}`
17954
+ };
17955
+ return;
17956
+ }
17418
17957
  if (event.toolName !== "write" && event.toolName !== "edit")
17419
17958
  return;
17420
17959
  if (!ctx.isProjectTrusted())