@brainervirus/workit-cli 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +722 -139
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -50927,10 +50927,12 @@ var decisionSchema = object({
50927
50927
  scope: scopeSchema,
50928
50928
  presented: text,
50929
50929
  approvedContent: text,
50930
- contentRefs: array(refSchema)
50930
+ displayed: text.optional(),
50931
+ contentRefs: array(refSchema),
50932
+ statedChoice: object({ ref: text, text }).strict().optional()
50931
50933
  }).strict(),
50932
50934
  digest,
50933
- response: _enum(["approved", "rejected"]),
50935
+ response: _enum(["approved", "rejected", "stated"]),
50934
50936
  requirementIds: array(digest),
50935
50937
  revoked: object({ at: utc, reason: text }).strict().nullable(),
50936
50938
  consumption: object({
@@ -50939,6 +50941,27 @@ var decisionSchema = object({
50939
50941
  actionRef: refSchema
50940
50942
  }).strict().nullable()
50941
50943
  }).strict();
50944
+ var BINDING_QUESTION_BUDGET = 300;
50945
+ var bindingQuestionHeader = /^Workit decision: (design|action|limitation|preference)$/;
50946
+ var workitBindingQuestionIssue = (questions) => {
50947
+ if (!Array.isArray(questions) || questions.length !== 1)
50948
+ return null;
50949
+ const question = questions[0];
50950
+ if (!question || typeof question !== "object")
50951
+ return null;
50952
+ if (typeof question.header !== "string" || !bindingQuestionHeader.test(question.header.trim()))
50953
+ return null;
50954
+ 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.`;
50955
+ if (typeof question.question === "string" && question.question.length > BINDING_QUESTION_BUDGET)
50956
+ return over("the question text", question.question);
50957
+ if (Array.isArray(question.options))
50958
+ for (const option of question.options) {
50959
+ const description = option.description;
50960
+ if (typeof description === "string" && description.length > BINDING_QUESTION_BUDGET)
50961
+ return over("an option description", description);
50962
+ }
50963
+ return null;
50964
+ };
50942
50965
  var findingSchema = object({
50943
50966
  claim: text,
50944
50967
  consequence: text,
@@ -51005,6 +51028,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
51005
51028
  path: ["decisionId"]
51006
51029
  });
51007
51030
  });
51031
+ var runtimeSchema = object({
51032
+ createdWith: text.nullable(),
51033
+ updatedWith: text
51034
+ }).strict();
51008
51035
  var taskRecordSchema = object({
51009
51036
  schemaVersion: literal(1),
51010
51037
  id,
@@ -51018,6 +51045,8 @@ var taskRecordSchema = object({
51018
51045
  status: _enum(["active", "paused", "closed"]),
51019
51046
  closure: closureSchema.nullable(),
51020
51047
  progress: progressSchema,
51048
+ pauseReason: text.nullable().optional(),
51049
+ runtime: runtimeSchema.optional(),
51021
51050
  assessments: array(entrySchema(assessmentSchema)),
51022
51051
  policy: policySchema.nullable(),
51023
51052
  policyChanges: array(policyChangeSchema),
@@ -51033,6 +51062,7 @@ var workspaceRecordSchema = object({
51033
51062
  id,
51034
51063
  revision,
51035
51064
  root: nonEmpty,
51065
+ runtime: runtimeSchema.optional(),
51036
51066
  writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
51037
51067
  }).strict();
51038
51068
  var capabilitySchema = object({
@@ -51060,6 +51090,9 @@ var taskSummarySchema = object({
51060
51090
  id,
51061
51091
  revision,
51062
51092
  workspaceRevision: revision,
51093
+ createdAt: utc,
51094
+ updatedAt: utc,
51095
+ runtime: runtimeSchema.nullable(),
51063
51096
  objective: text,
51064
51097
  status: _enum(["active", "paused", "closed"]),
51065
51098
  closure: closureSchema.nullable(),
@@ -51130,7 +51163,7 @@ var taskOperations = {
51130
51163
  expectedWorkspaceRevision: revision.optional(),
51131
51164
  outcome: outcomeSchema,
51132
51165
  summary: text,
51133
- decisionIds: array(id)
51166
+ decisionIds: array(id).optional()
51134
51167
  })
51135
51168
  };
51136
51169
  var policyOperations = {
@@ -51222,14 +51255,15 @@ var writerOperations = {
51222
51255
  ...taskId,
51223
51256
  expectedRevision: revision.optional(),
51224
51257
  expectedWorkspaceRevision: revision.optional(),
51225
- workerId: nullableId.optional()
51258
+ workerId: nullableId.optional(),
51259
+ reason: text.optional()
51226
51260
  }),
51227
51261
  release: operation({
51228
51262
  action: literal("release"),
51229
51263
  ...taskId,
51230
51264
  expectedRevision: revision.optional(),
51231
51265
  expectedWorkspaceRevision: revision.optional(),
51232
- reason: text
51266
+ reason: text.optional()
51233
51267
  })
51234
51268
  };
51235
51269
  var stateOperations = {
@@ -57208,6 +57242,33 @@ var metadataLockSchema = object({
57208
57242
  nonce: string2().min(1)
57209
57243
  }).strict();
57210
57244
  var now2 = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
57245
+ var cachedRuntimeVersion = null;
57246
+ var runtimeVersion = () => {
57247
+ if (cachedRuntimeVersion === null) {
57248
+ try {
57249
+ const pkg = JSON.parse(fs7.readFileSync(path19.join(packageRoot(), "package.json"), "utf8"));
57250
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
57251
+ } catch {
57252
+ cachedRuntimeVersion = "0.0.0";
57253
+ }
57254
+ }
57255
+ return cachedRuntimeVersion;
57256
+ };
57257
+ var stampNew = (record) => ({
57258
+ createdWith: record.runtime?.createdWith ?? null,
57259
+ updatedWith: runtimeVersion()
57260
+ });
57261
+ var isNewerVersion = (candidate, current) => {
57262
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
57263
+ const [next, now] = [parts(candidate), parts(current)];
57264
+ for (let index = 0;index < 3; index += 1) {
57265
+ const left = next[index] ?? 0;
57266
+ const right = now[index] ?? 0;
57267
+ if (left !== right)
57268
+ return left > right;
57269
+ }
57270
+ return false;
57271
+ };
57211
57272
  var jsonBytes = (value) => `${canonicalJson(value)}
57212
57273
  `;
57213
57274
  var digestBytes2 = (value) => createHash4("sha256").update(value).digest("hex");
@@ -57307,7 +57368,7 @@ class TaskStore {
57307
57368
  if (!current.ok)
57308
57369
  return current;
57309
57370
  if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
57310
- return failure("revision_conflict", "workspace revision does not match", {
57371
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
57311
57372
  expectedWorkspaceRevision: value.expectedWorkspaceRevision,
57312
57373
  actualWorkspaceRevision: current.data?.revision ?? null
57313
57374
  });
@@ -57319,11 +57380,17 @@ class TaskStore {
57319
57380
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
57320
57381
  if (current.data && !previousWorkspaceBytes)
57321
57382
  return failure("storage_error", "workspace snapshot disappeared during creation");
57322
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
57383
+ const workspace = current.data ? {
57384
+ ...current.data,
57385
+ revision: newRevision(),
57386
+ root: this.root,
57387
+ runtime: stampNew(current.data)
57388
+ } : {
57323
57389
  schemaVersion: SCHEMA_VERSION,
57324
57390
  id: newId(),
57325
57391
  revision: newRevision(),
57326
57392
  root: this.root,
57393
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
57327
57394
  writer: null
57328
57395
  };
57329
57396
  const timestamp = value.now ?? now2();
@@ -57345,6 +57412,7 @@ class TaskStore {
57345
57412
  status: "active",
57346
57413
  closure: null,
57347
57414
  progress: { summary: "", nextAction: null, blockers: [] },
57415
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
57348
57416
  assessments: [],
57349
57417
  policy: null,
57350
57418
  policyChanges: [],
@@ -57375,7 +57443,7 @@ class TaskStore {
57375
57443
  if (!current.ok)
57376
57444
  return current;
57377
57445
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
57378
- return failure("revision_conflict", "workspace revision does not match", {
57446
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
57379
57447
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
57380
57448
  actualWorkspaceRevision: current.data?.revision ?? null
57381
57449
  });
@@ -57383,11 +57451,17 @@ class TaskStore {
57383
57451
  if (current.data && !previousWorkspaceBytes)
57384
57452
  return failure("storage_error", "workspace snapshot disappeared during import");
57385
57453
  const timestamp = input.now ?? now2();
57386
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
57454
+ const workspace = current.data ? {
57455
+ ...current.data,
57456
+ revision: newRevision(),
57457
+ root: this.root,
57458
+ runtime: stampNew(current.data)
57459
+ } : {
57387
57460
  schemaVersion: SCHEMA_VERSION,
57388
57461
  id: input.workspaceId ?? newId(),
57389
57462
  revision: newRevision(),
57390
57463
  root: this.root,
57464
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
57391
57465
  writer: null
57392
57466
  };
57393
57467
  const task = {
@@ -57395,7 +57469,11 @@ class TaskStore {
57395
57469
  workspaceId: workspace.id,
57396
57470
  revision: newRevision(),
57397
57471
  createdAt: timestamp,
57398
- updatedAt: timestamp
57472
+ updatedAt: timestamp,
57473
+ runtime: {
57474
+ createdWith: input.task.runtime?.createdWith ?? null,
57475
+ updatedWith: runtimeVersion()
57476
+ }
57399
57477
  };
57400
57478
  const validTask = taskRecordSchema.safeParse(task);
57401
57479
  if (!validTask.success)
@@ -57444,7 +57522,8 @@ class TaskStore {
57444
57522
  workspaceId: current.data.workspaceId,
57445
57523
  createdAt: current.data.createdAt,
57446
57524
  revision: context.revision,
57447
- updatedAt: context.now
57525
+ updatedAt: context.now,
57526
+ runtime: stampNew(current.data)
57448
57527
  };
57449
57528
  const valid = taskRecordSchema.safeParse(record);
57450
57529
  if (!valid.success)
@@ -57478,7 +57557,8 @@ class TaskStore {
57478
57557
  ...changed.data,
57479
57558
  id: current.data.id,
57480
57559
  root: this.root,
57481
- revision: context.revision
57560
+ revision: context.revision,
57561
+ runtime: stampNew(current.data)
57482
57562
  };
57483
57563
  const valid = workspaceRecordSchema.safeParse(record);
57484
57564
  if (!valid.success)
@@ -57521,7 +57601,8 @@ class TaskStore {
57521
57601
  ...changedWorkspace.data,
57522
57602
  id: workspace.data.id,
57523
57603
  root: this.root,
57524
- revision: workspaceContext.revision
57604
+ revision: workspaceContext.revision,
57605
+ runtime: stampNew(workspace.data)
57525
57606
  });
57526
57607
  if (!nextWorkspace.success)
57527
57608
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -57546,7 +57627,8 @@ class TaskStore {
57546
57627
  workspaceId: task.data.workspaceId,
57547
57628
  createdAt: task.data.createdAt,
57548
57629
  revision: taskContext.revision,
57549
- updatedAt: taskContext.now
57630
+ updatedAt: taskContext.now,
57631
+ runtime: stampNew(task.data)
57550
57632
  });
57551
57633
  if (!nextTask.success) {
57552
57634
  this.markUncertain(nextWorkspace.data);
@@ -57766,7 +57848,8 @@ class TaskStore {
57766
57848
  const value = {
57767
57849
  ...workspace,
57768
57850
  writer: { ...workspace.writer, state: "uncertain" },
57769
- revision: newRevision()
57851
+ revision: newRevision(),
57852
+ runtime: stampNew(workspace)
57770
57853
  };
57771
57854
  this.replaceSnapshot(this.workspacePath, value, workspace);
57772
57855
  }
@@ -57980,7 +58063,12 @@ class TaskStore {
57980
58063
  if (isObject3(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
57981
58064
  return failure("unsupported_version", "unsupported snapshot schema version");
57982
58065
  const parsed = schema.safeParse(value);
57983
- return parsed.success ? success(null, null, parsed.data) : failure("recovery_required", "snapshot does not satisfy its schema");
58066
+ if (parsed.success)
58067
+ return success(null, null, parsed.data);
58068
+ const writerVersion = isObject3(value) && isObject3(value.runtime) ? value.runtime.updatedWith : null;
58069
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
58070
+ return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
58071
+ return failure("recovery_required", "snapshot does not satisfy its schema");
57984
58072
  }
57985
58073
  processStart(pid) {
57986
58074
  try {
@@ -57990,7 +58078,7 @@ class TaskStore {
57990
58078
  }
57991
58079
  }
57992
58080
  conflict(expected, actual) {
57993
- return failure("revision_conflict", "snapshot revision does not match", {
58081
+ return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
57994
58082
  expectedRevision: expected,
57995
58083
  actualRevision: actual
57996
58084
  });
@@ -58017,6 +58105,61 @@ class TaskStore {
58017
58105
  return path19.join(this.workitDir, ".gitignore");
58018
58106
  }
58019
58107
  }
58108
+ // packages/workit-core/src/core/workers.ts
58109
+ var same = (left, right) => {
58110
+ try {
58111
+ return canonicalJson(left) === canonicalJson(right);
58112
+ } catch {
58113
+ return false;
58114
+ }
58115
+ };
58116
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
58117
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
58118
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
58119
+ var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
58120
+ const code = char.charCodeAt(0);
58121
+ return code < 32 || code === 127;
58122
+ }) && !value.split(/[\\/]/).includes("..");
58123
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
58124
+ var workerIdOf = (value) => value.workerId ?? null;
58125
+ function assertProductWriteAllowed(input) {
58126
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
58127
+ return failure("invalid_input", "invalid product write path");
58128
+ if (!input.store)
58129
+ return failure("permission_denied", "write authorization requires core state");
58130
+ const task = input.store.readTask(input.task.id);
58131
+ if (!task.ok)
58132
+ return task;
58133
+ const workspace = input.store.readWorkspace();
58134
+ if (!workspace.ok)
58135
+ return workspace;
58136
+ if (!workspace.data)
58137
+ return failure("not_found", "workspace not found");
58138
+ const workspaceRecord = workspace.data;
58139
+ if (task.data.status !== "active")
58140
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
58141
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
58142
+ return failure("recovery_required", "task and workspace bindings are invalid");
58143
+ const writer = workspaceRecord.writer;
58144
+ if (!writer)
58145
+ return failure("permission_denied", "checkout has no writer owner");
58146
+ if (writer.state === "uncertain")
58147
+ return failure("recovery_required", "checkout writer requires recovery", {
58148
+ owner: writer.owner
58149
+ });
58150
+ const caller = callerValue(input.caller);
58151
+ const workerId = workerIdOf(input.caller);
58152
+ const owner = writer.owner;
58153
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
58154
+ return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
58155
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
58156
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
58157
+ return failure("permission_denied", "only implementers can own product writes");
58158
+ 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))))
58159
+ return failure("recovery_required", "worker session is not an active writer");
58160
+ return success(null, workspaceRecord.revision, owner);
58161
+ }
58162
+
58020
58163
  // packages/workit-core/src/core/task-evaluation.ts
58021
58164
  import { createHash as createHash6 } from "node:crypto";
58022
58165
  import * as fs9 from "node:fs";
@@ -58155,7 +58298,11 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
58155
58298
  return failure("invalid_input", "document references require a byte digest");
58156
58299
  const target = path20.resolve(root, reference.path);
58157
58300
  if (target !== root && !target.startsWith(`${root}${path20.sep}`))
58158
- return failure("invalid_input", "document reference escapes checkout");
58301
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
58302
+ fields: [
58303
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
58304
+ ]
58305
+ });
58159
58306
  try {
58160
58307
  const relative = path20.relative(root, target);
58161
58308
  let current = root;
@@ -58166,7 +58313,14 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
58166
58313
  const stat = fs8.lstatSync(current);
58167
58314
  const real = fs8.realpathSync(current);
58168
58315
  if (real !== root && !real.startsWith(`${root}${path20.sep}`))
58169
- return failure("invalid_input", "document reference escapes checkout");
58316
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
58317
+ fields: [
58318
+ {
58319
+ path: "contentRefs.path",
58320
+ reason: `${reference.path} resolves outside the checkout`
58321
+ }
58322
+ ]
58323
+ });
58170
58324
  if (stat.isSymbolicLink())
58171
58325
  return failure("invalid_input", "document reference uses a symlink");
58172
58326
  if (current === target && !stat.isFile())
@@ -58610,15 +58764,15 @@ var scopeRoots = (root, scope) => {
58610
58764
  for (const item of scope.paths.length ? scope.paths : ["."]) {
58611
58765
  const target = path21.resolve(root, item);
58612
58766
  if (!inside(root, target))
58613
- return failure("invalid_input", "candidate scope escapes checkout");
58767
+ 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` }] });
58614
58768
  let ancestor = target;
58615
58769
  while (!fs9.existsSync(ancestor) && ancestor !== root)
58616
58770
  ancestor = path21.dirname(ancestor);
58617
58771
  try {
58618
58772
  if (!inside(root, fs9.realpathSync(ancestor)))
58619
- return failure("invalid_input", "candidate scope escapes checkout");
58773
+ 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` }] });
58620
58774
  } catch {
58621
- return failure("invalid_input", "candidate scope cannot be inspected");
58775
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
58622
58776
  }
58623
58777
  roots.push(target);
58624
58778
  }
@@ -58633,7 +58787,7 @@ function captureCandidate(root, scope, environment = []) {
58633
58787
  }
58634
58788
  const normalizedScope = canonicalScope(scope);
58635
58789
  if (!normalizedScope)
58636
- return failure("invalid_input", "candidate scope is invalid");
58790
+ 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" }] });
58637
58791
  const roots = scopeRoots(checkout, normalizedScope);
58638
58792
  if (!roots.ok)
58639
58793
  return roots;
@@ -58787,7 +58941,7 @@ function evaluateEvidence(task, candidate) {
58787
58941
  }
58788
58942
  var scopeCovers2 = scopeCovers;
58789
58943
  var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
58790
- var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && data.response === "approved" && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
58944
+ var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
58791
58945
  var evidenceMatchesRequirement = (kind, evidenceKind, dimension) => {
58792
58946
  if (kind !== "passed")
58793
58947
  return false;
@@ -58822,7 +58976,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
58822
58976
  return true;
58823
58977
  const reviewSession = sessionFromRef(entry.data.reviewContext);
58824
58978
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
58825
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
58979
+ const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
58826
58980
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
58827
58981
  });
58828
58982
  if (passed.length) {
@@ -58863,7 +59017,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
58863
59017
  status: "satisfied",
58864
59018
  evidenceIds: [],
58865
59019
  decisionIds: decisions.map((decision) => decision.id),
58866
- reason: "an applicable approved decision satisfies the requirement"
59020
+ reason: "an applicable approved or stated decision satisfies the requirement"
58867
59021
  };
58868
59022
  const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
58869
59023
  if (limitations.length)
@@ -58918,7 +59072,17 @@ function evaluateClosure(requestedOutcome, view) {
58918
59072
  const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
58919
59073
  if (requestedOutcome !== "stopped" && blocking.length)
58920
59074
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
58921
- requirementIds: blocking.map((item) => item.requirementId)
59075
+ requirementIds: blocking.map((item) => item.requirementId),
59076
+ requirements: blocking.map((item) => {
59077
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
59078
+ return {
59079
+ requirementId: item.requirementId,
59080
+ ruleId: requirement?.ruleId ?? "unknown",
59081
+ reason: item.reason,
59082
+ satisfaction: requirement?.satisfaction ?? "",
59083
+ dependentAction: requirement?.dependentAction ?? null
59084
+ };
59085
+ })
58922
59086
  });
58923
59087
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
58924
59088
  if (requestedOutcome === "verified" && accepted.length)
@@ -58949,7 +59113,7 @@ function evaluateClosure(requestedOutcome, view) {
58949
59113
  var METHODS = {
58950
59114
  "workit-challenge": { dimensions: ["challenge", "decisions"] },
58951
59115
  "workit-behavioral-tdd": { dimensions: ["testing"] },
58952
- "workit-review": { dimensions: ["review"] },
59116
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
58953
59117
  "workit-plan": { dimensions: ["artifacts", "continuity"] },
58954
59118
  "workit-implement": { dimensions: ["delegation"] },
58955
59119
  "workit-debug": { ruleIds: ["root-cause-investigation"] },
@@ -59000,7 +59164,7 @@ function reconcileResume(view, observations = []) {
59000
59164
  return candidate;
59001
59165
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
59002
59166
  const blockers = [...view.task.progress.blockers];
59003
- if (view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
59167
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
59004
59168
  blockers.push({
59005
59169
  reason: "worker state requires reconciliation",
59006
59170
  dependentAction: "resume",
@@ -59092,6 +59256,7 @@ var exportDigest = (bundle) => sha256({
59092
59256
  task: bundle.task
59093
59257
  });
59094
59258
  // packages/workit-core/src/core/external-action.ts
59259
+ import { execFileSync as execFileSync2 } from "node:child_process";
59095
59260
  var externalActionSchema = discriminatedUnion("operation", [
59096
59261
  object({
59097
59262
  operation: literal("git.branch_setup"),
@@ -59104,7 +59269,11 @@ var externalActionSchema = discriminatedUnion("operation", [
59104
59269
  }).strict(),
59105
59270
  object({
59106
59271
  operation: literal("git.commit"),
59107
- payload: object({ message: string2().min(1) }).strict()
59272
+ payload: object({
59273
+ message: string2().min(1).optional(),
59274
+ plan_steps: array(string2().min(1)).optional(),
59275
+ plan_branch: string2().min(1).optional()
59276
+ }).strict()
59108
59277
  }).strict(),
59109
59278
  object({
59110
59279
  operation: literal("git.push"),
@@ -59193,7 +59362,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
59193
59362
  payload,
59194
59363
  ...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 } : {}
59195
59364
  });
59196
- 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).";
59365
+ 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).";
59197
59366
  var externalActionRef = (host, actor, descriptor) => ({
59198
59367
  kind: "host",
59199
59368
  host,
@@ -59244,14 +59413,185 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
59244
59413
  return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
59245
59414
  return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
59246
59415
  };
59416
+ var planCommitDescriptor = (content) => {
59417
+ try {
59418
+ const value = JSON.parse(content);
59419
+ if (value.operation !== "git.commit" || !value.payload)
59420
+ return null;
59421
+ const steps = value.payload.plan_steps;
59422
+ const branch = value.payload.plan_branch;
59423
+ if (!Array.isArray(steps) || steps.some((step) => typeof step !== "string" || !step) || typeof branch !== "string" || !branch)
59424
+ return null;
59425
+ return { steps, branch };
59426
+ } catch {
59427
+ return null;
59428
+ }
59429
+ };
59430
+ var approvedPlanCommit = (store, host, actor, message) => {
59431
+ if (typeof message !== "string" || !message)
59432
+ return failure("invalid_input", "commit message is required");
59433
+ const listed = store.listTasks();
59434
+ const workspace = store.readWorkspace();
59435
+ if (!listed.ok || !workspace.ok)
59436
+ return failure("storage_error", "external action state is unavailable");
59437
+ if (!workspace.data)
59438
+ return failure("not_found", "workspace not found");
59439
+ const matches = listed.data.flatMap((task) => {
59440
+ if (task.status !== "active" || task.workspaceId !== workspace.data.id)
59441
+ return [];
59442
+ if (task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
59443
+ return [];
59444
+ return task.decisions.flatMap((entry) => {
59445
+ const decision = entry.data;
59446
+ if (decision.purpose !== "action" || decision.response !== "approved" || decision.revoked !== null)
59447
+ return [];
59448
+ if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
59449
+ return [];
59450
+ const plan = planCommitDescriptor(decision.binding.approvedContent);
59451
+ if (!plan)
59452
+ return [];
59453
+ const completed = task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ?? [];
59454
+ if (plan.steps[completed.length] !== message)
59455
+ return [];
59456
+ return [{ task, entry, steps: plan.steps, branch: plan.branch, completed: [...completed] }];
59457
+ });
59458
+ });
59459
+ if (matches.length !== 1)
59460
+ return failure("permission_denied", matches.length === 0 ? "no plan commit authorization matches this message" : "multiple plan authorizations match this message");
59461
+ return success(workspace.data.revision, null, matches[0]);
59462
+ };
59463
+ var commitMessageFromDescriptor = (operation) => {
59464
+ try {
59465
+ const descriptor = JSON.parse(operation);
59466
+ return descriptor.operation === "git.commit" && typeof descriptor.payload?.message === "string" ? descriptor.payload.message : undefined;
59467
+ } catch {
59468
+ return;
59469
+ }
59470
+ };
59471
+ var planCommitBinding = (store, host, actor, operation) => {
59472
+ const message = commitMessageFromDescriptor(operation);
59473
+ if (!message)
59474
+ return null;
59475
+ const plan = approvedPlanCommit(store, host, actor, message);
59476
+ if (!plan.ok)
59477
+ return null;
59478
+ let branch;
59479
+ try {
59480
+ branch = execFileSync2("git", ["branch", "--show-current"], {
59481
+ cwd: store.root,
59482
+ encoding: "utf8",
59483
+ stdio: ["pipe", "pipe", "pipe"]
59484
+ }).trim();
59485
+ } catch {
59486
+ return null;
59487
+ }
59488
+ if (!branch || branch !== plan.data.branch)
59489
+ return null;
59490
+ const workspace = store.readWorkspace();
59491
+ if (!workspace.ok || !workspace.data)
59492
+ return null;
59493
+ return {
59494
+ taskId: plan.data.task.id,
59495
+ decisionId: plan.data.entry.id,
59496
+ binding: plan.data.entry.data.binding,
59497
+ expectedRevision: plan.data.task.revision,
59498
+ expectedWorkspaceRevision: workspace.data.revision,
59499
+ step: message
59500
+ };
59501
+ };
59247
59502
  var approvedExternalAction = (store, host, actor, operation) => {
59248
59503
  const selected = externalActionState(store, host, actor, operation);
59249
- if (!selected.ok)
59504
+ if (selected.ok) {
59505
+ if (selected.data.entry.data.consumption !== null)
59506
+ 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");
59250
59507
  return selected;
59251
- if (selected.data.entry.data.consumption !== null)
59252
- 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");
59508
+ }
59509
+ const intent = approvedBranchSetupIntent(store, host, actor, operation);
59510
+ if (intent.ok) {
59511
+ if (intent.data.entry.data.consumption !== null)
59512
+ return failure(intent.data.entry.data.consumption.state === "uncertain" ? "external_outcome_unknown" : "permission_denied", intent.data.entry.data.consumption.state === "uncertain" ? "previous external action outcome is unknown" : "external action was already settled");
59513
+ return intent;
59514
+ }
59253
59515
  return selected;
59254
59516
  };
59517
+ var branchSetupIntent = (descriptor) => {
59518
+ try {
59519
+ const value = JSON.parse(descriptor);
59520
+ if (value.operation !== "git.branch_setup" || !value.payload)
59521
+ return null;
59522
+ const { target_branch, resolved } = value.payload;
59523
+ if (typeof target_branch !== "string" || !target_branch || !resolved)
59524
+ return null;
59525
+ if (typeof resolved.base_branch !== "string" || typeof resolved.target_exists !== "boolean")
59526
+ return null;
59527
+ return {
59528
+ target: target_branch,
59529
+ base: resolved.base_branch,
59530
+ remoteBase: typeof resolved.remote_base === "string" ? resolved.remote_base : null,
59531
+ targetExists: resolved.target_exists
59532
+ };
59533
+ } catch {
59534
+ return null;
59535
+ }
59536
+ };
59537
+ var approvedBranchSetupIntent = (store, host, actor, operation) => {
59538
+ const current = branchSetupIntent(operation);
59539
+ if (!current)
59540
+ return failure("permission_denied", "no approved action is bound to this session");
59541
+ const listed = store.listTasks();
59542
+ const workspace = store.readWorkspace();
59543
+ if (!listed.ok || !workspace.ok)
59544
+ return failure("storage_error", "external action state is unavailable");
59545
+ if (!workspace.data)
59546
+ return failure("not_found", "workspace not found");
59547
+ const candidates = listed.data.flatMap((task) => {
59548
+ if (task.status !== "active" || task.workspaceId !== workspace.data.id || task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
59549
+ return [];
59550
+ return task.decisions.filter((entry) => entry.data.purpose === "action" && entry.data.response === "approved" && entry.data.revoked === null).map((entry) => ({ task, entry }));
59551
+ });
59552
+ const matches = candidates.filter(({ entry }) => {
59553
+ const intent = branchSetupIntent(entry.data.binding.approvedContent);
59554
+ return intent !== null && intent.target === current.target && intent.base === current.base && intent.remoteBase === current.remoteBase && intent.targetExists === current.targetExists;
59555
+ });
59556
+ if (matches.length !== 1)
59557
+ return failure("permission_denied", matches.length === 0 ? "no approved action is bound to this session" : "multiple approved actions match this session");
59558
+ return success(workspace.data.revision, null, { ...matches[0], workspace: workspace.data });
59559
+ };
59560
+ var priorResolvedDrift = (store, host, actor, operation, payload, currentResolved) => {
59561
+ const prior = priorExternalAction(store, host, actor, operation, payload);
59562
+ if (!prior.ok)
59563
+ return success(null, null, null);
59564
+ let baseline;
59565
+ try {
59566
+ const parsed = JSON.parse(prior.data.entry.data.binding.approvedContent);
59567
+ baseline = parsed.payload?.resolved;
59568
+ } catch {
59569
+ return success(null, null, null);
59570
+ }
59571
+ if (baseline === undefined || baseline === null)
59572
+ return success(null, null, null);
59573
+ if (operation === "git.branch_setup") {
59574
+ const before = branchSetupIntent(prior.data.entry.data.binding.approvedContent);
59575
+ let after = null;
59576
+ try {
59577
+ after = branchSetupIntent(JSON.stringify({
59578
+ operation,
59579
+ payload: { ...payload, resolved: currentResolved }
59580
+ }));
59581
+ } catch {
59582
+ after = null;
59583
+ }
59584
+ if (before && after) {
59585
+ const moved = before.target !== after.target ? "target_branch" : before.base !== after.base ? "base_branch" : before.remoteBase !== after.remoteBase ? "remote_base" : before.targetExists !== after.targetExists ? "target_exists" : null;
59586
+ if (moved === null)
59587
+ return success(null, null, null);
59588
+ return failure("invalid_input", `repository state changed since approval (${moved} moved); show the current state and approve the action again`, { outcome: "not_started", operation });
59589
+ }
59590
+ }
59591
+ if (canonicalJson(baseline) === canonicalJson(currentResolved))
59592
+ return success(null, null, null);
59593
+ return failure("invalid_input", "repository state changed since approval; show the current state and approve the action again", { outcome: "not_started", operation });
59594
+ };
59255
59595
  var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
59256
59596
  let steps = [];
59257
59597
  try {
@@ -59510,7 +59850,7 @@ function resolveRequirements(input) {
59510
59850
  ].join(", ")}`,
59511
59851
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
59512
59852
  before: "dependent_action",
59513
- dependentAction: "dependent-action",
59853
+ dependentAction: null,
59514
59854
  acceptanceAllowed: false
59515
59855
  }));
59516
59856
  if (signals.productChoiceOpen.value === true)
@@ -59521,7 +59861,7 @@ function resolveRequirements(input) {
59521
59861
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
59522
59862
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
59523
59863
  before: "dependent_action",
59524
- dependentAction: "dependent-action",
59864
+ dependentAction: null,
59525
59865
  acceptanceAllowed: false
59526
59866
  }));
59527
59867
  if (signals.behaviorChange.value === true)
@@ -59679,59 +60019,6 @@ function diffPolicy(previous, next, reason, now) {
59679
60019
  return parsed.data;
59680
60020
  }
59681
60021
 
59682
- // packages/workit-core/src/core/workers.ts
59683
- var same = (left, right) => {
59684
- try {
59685
- return canonicalJson(left) === canonicalJson(right);
59686
- } catch {
59687
- return false;
59688
- }
59689
- };
59690
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
59691
- var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
59692
- const code = char.charCodeAt(0);
59693
- return code < 32 || code === 127;
59694
- }) && !value.split(/[\\/]/).includes("..");
59695
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
59696
- var workerIdOf = (value) => value.workerId ?? null;
59697
- function assertProductWriteAllowed(input) {
59698
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
59699
- return failure("invalid_input", "invalid product write path");
59700
- if (!input.store)
59701
- return failure("permission_denied", "write authorization requires core state");
59702
- const task = input.store.readTask(input.task.id);
59703
- if (!task.ok)
59704
- return task;
59705
- const workspace = input.store.readWorkspace();
59706
- if (!workspace.ok)
59707
- return workspace;
59708
- if (!workspace.data)
59709
- return failure("not_found", "workspace not found");
59710
- const workspaceRecord = workspace.data;
59711
- if (task.data.status !== "active")
59712
- return failure("invalid_transition", "paused or closed tasks cannot own product writes");
59713
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
59714
- return failure("recovery_required", "task and workspace bindings are invalid");
59715
- const writer = workspaceRecord.writer;
59716
- if (!writer)
59717
- return failure("permission_denied", "checkout has no writer owner");
59718
- if (writer.state === "uncertain")
59719
- return failure("recovery_required", "checkout writer requires recovery", {
59720
- owner: writer.owner
59721
- });
59722
- const caller = callerValue(input.caller);
59723
- const workerId = workerIdOf(input.caller);
59724
- const owner = writer.owner;
59725
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
59726
- return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
59727
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
59728
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
59729
- return failure("permission_denied", "only implementers can own product writes");
59730
- 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))))
59731
- return failure("recovery_required", "worker session is not an active writer");
59732
- return success(null, workspaceRecord.revision, owner);
59733
- }
59734
-
59735
60022
  // packages/workit-core/src/core/task-engine.ts
59736
60023
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
59737
60024
  kind,
@@ -59862,15 +60149,20 @@ var applyWorkerLifecycle = (input) => {
59862
60149
  return failure("invalid_transition", "worker has not started");
59863
60150
  if (input.state === "running" && entry.data.state === "stopped")
59864
60151
  return failure("invalid_transition", "stopped worker cannot run again");
60152
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
59865
60153
  const nextEntry = {
59866
60154
  ...entry,
59867
60155
  recordedAt: input.now ?? entry.recordedAt,
59868
60156
  provenance: authority.provenance,
59869
- data: { ...entry.data, state: input.state, session: input.session }
60157
+ data: {
60158
+ ...entry.data,
60159
+ state: terminalCancel ? "cancelling" : input.state,
60160
+ session: input.session
60161
+ }
59870
60162
  };
59871
60163
  const shouldClear = input.state === "stopped" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
59872
60164
  const shouldUncertain = input.state === "unknown" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
59873
- if (entry.data.state === input.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
60165
+ if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
59874
60166
  return success(task.data.revision, workspace.data.revision, entry);
59875
60167
  const changed = input.store.mutateTaskAndWorkspace({
59876
60168
  taskId: task.data.id,
@@ -60240,7 +60532,7 @@ class WorkitCore {
60240
60532
  activeWorkerBlocker(task, workspace) {
60241
60533
  if (workspace.writer)
60242
60534
  return failure("recovery_required", "writer ownership must be released first");
60243
- if (task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state)))
60535
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
60244
60536
  return failure("recovery_required", "worker state requires reconciliation");
60245
60537
  return null;
60246
60538
  }
@@ -60477,11 +60769,6 @@ class WorkitCore {
60477
60769
  if (helper && !helper.ok)
60478
60770
  return helper;
60479
60771
  const evidence = input.evidence;
60480
- if (helper?.ok) {
60481
- const assignment = helper.data.data.assignment;
60482
- 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))
60483
- return failure("permission_denied", "evidence is outside the worker assignment");
60484
- }
60485
60772
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
60486
60773
  return failure("invalid_input", "missing or skipped evidence requires a reason");
60487
60774
  if (evidence.kind === "review") {
@@ -60498,6 +60785,11 @@ class WorkitCore {
60498
60785
  evidence.beforeCandidateId ??= null;
60499
60786
  evidence.candidateId ??= null;
60500
60787
  }
60788
+ if (helper?.ok) {
60789
+ const assignment = helper.data.data.assignment;
60790
+ 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))
60791
+ return failure("permission_denied", "evidence is outside the worker assignment");
60792
+ }
60501
60793
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
60502
60794
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
60503
60795
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -60551,7 +60843,12 @@ class WorkitCore {
60551
60843
  const input = parsed.data;
60552
60844
  if (nativeRequired && input.action !== "record")
60553
60845
  return failure("permission_denied", "native observation is only valid for recording decisions");
60554
- if (nativeRequired && nativeObservation === undefined)
60846
+ const stated = input.action === "record" && input.response === "stated";
60847
+ if (stated && input.purpose === "action")
60848
+ return failure("permission_denied", "stated choices cannot authorize mutating actions");
60849
+ if (stated && !input.binding?.statedChoice)
60850
+ return failure("invalid_input", "stated choices require binding.statedChoice");
60851
+ if (nativeRequired && nativeObservation === undefined && !stated)
60555
60852
  return failure("permission_denied", "native decision observation is required");
60556
60853
  const task = this.store.readTask(input.taskId);
60557
60854
  if (!task.ok)
@@ -60584,7 +60881,7 @@ class WorkitCore {
60584
60881
  consumption: null
60585
60882
  };
60586
60883
  const data = { ...base, digest: decisionDigest(base) };
60587
- const native = nativeRequired ? verifyNativeDecision(this.context.nativeAuthority, {
60884
+ const native = nativeRequired && !stated ? verifyNativeDecision(this.context.nativeAuthority, {
60588
60885
  observation: nativeObservation,
60589
60886
  expected: {
60590
60887
  taskId: task.data.id,
@@ -61393,6 +61690,9 @@ class WorkitCore {
61393
61690
  id: task.id,
61394
61691
  revision: task.revision,
61395
61692
  workspaceRevision: workspace.data.revision,
61693
+ createdAt: task.createdAt,
61694
+ updatedAt: task.updatedAt,
61695
+ runtime: task.runtime ?? null,
61396
61696
  objective: task.intent.data.objective,
61397
61697
  status: task.status,
61398
61698
  closure: task.closure,
@@ -61478,7 +61778,7 @@ class WorkitCore {
61478
61778
  const base = reconcileResume(effectiveView);
61479
61779
  if (!base.ok)
61480
61780
  return base;
61481
- const needsReconciliation = view.task.workers.some((entry) => ["dispatching", "running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
61781
+ const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
61482
61782
  const blockers = [...base.data.blockers];
61483
61783
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
61484
61784
  blockers.push({
@@ -61545,7 +61845,8 @@ class WorkitCore {
61545
61845
  ...current,
61546
61846
  status,
61547
61847
  candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
61548
- progress: status === "paused" ? { ...current.progress, summary: input.reason } : current.progress
61848
+ progress: current.progress,
61849
+ pauseReason: status === "paused" ? input.reason ?? null : null
61549
61850
  })
61550
61851
  });
61551
61852
  if (!changed.ok)
@@ -61581,7 +61882,7 @@ class WorkitCore {
61581
61882
  return workspace;
61582
61883
  if (!workspace.data)
61583
61884
  return failure("not_found", "workspace not found");
61584
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
61885
+ if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
61585
61886
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
61586
61887
  this.fillRevisions(input, task.data, workspace.data);
61587
61888
  const changed = this.store.mutateTaskAndWorkspace({
@@ -61729,14 +62030,14 @@ function run(root, executable, args, env = {}) {
61729
62030
 
61730
62031
  // packages/workit-core/src/core/branch.ts
61731
62032
  import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "node:fs";
61732
- import { execFileSync as execFileSync3 } from "node:child_process";
62033
+ import { execFileSync as execFileSync4 } from "node:child_process";
61733
62034
  import path24 from "node:path";
61734
62035
 
61735
62036
  // packages/workit-core/src/core/git.ts
61736
- import { execFileSync as execFileSync2 } from "node:child_process";
62037
+ import { execFileSync as execFileSync3 } from "node:child_process";
61737
62038
  var run2 = (cwd, args) => {
61738
62039
  try {
61739
- const stdout = execFileSync2("git", args, {
62040
+ const stdout = execFileSync3("git", args, {
61740
62041
  cwd,
61741
62042
  encoding: "utf8",
61742
62043
  stdio: ["pipe", "pipe", "pipe"]
@@ -61999,7 +62300,7 @@ var baseBranch = (cwd) => {
61999
62300
  return { base: String(resolved.defaultTargetBranch ?? "develop") };
62000
62301
  };
62001
62302
  var originBaseReady = (cwd, base) => {
62002
- const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
62303
+ const run = (args) => execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
62003
62304
  try {
62004
62305
  try {
62005
62306
  run(["fetch", "origin", base, "--prune"]);
@@ -62007,7 +62308,7 @@ var originBaseReady = (cwd, base) => {
62007
62308
  run(["fetch", "origin", "--prune"]);
62008
62309
  }
62009
62310
  try {
62010
- execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
62311
+ execFileSync4("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
62011
62312
  cwd,
62012
62313
  stdio: "pipe"
62013
62314
  });
@@ -62026,11 +62327,11 @@ var originBaseReady = (cwd, base) => {
62026
62327
  }
62027
62328
  };
62028
62329
  var fastForwardBase = (cwd, base) => {
62029
- const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
62330
+ const run = (args) => execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
62030
62331
  try {
62031
62332
  let hasLocalBase = true;
62032
62333
  try {
62033
- execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
62334
+ execFileSync4("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
62034
62335
  cwd,
62035
62336
  stdio: "pipe"
62036
62337
  });
@@ -62071,10 +62372,10 @@ var branchSetup = ({
62071
62372
  log
62072
62373
  }) => {
62073
62374
  const cwd = path24.resolve(workspace_root);
62074
- const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
62375
+ const exec = (args) => execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
62075
62376
  const current = gitContext(cwd).branch;
62076
62377
  if (!current || current === "unknown")
62077
- return { error: "not in a git repository" };
62378
+ return { error: "not in a git repository", phase: "preflight" };
62078
62379
  const sdd = sdd_dir ?? "docs";
62079
62380
  const manifestPath = path24.isAbsolute(sdd) ? path24.join(sdd, "manifest.json") : path24.join(cwd, sdd, "manifest.json");
62080
62381
  const ensureManifestDir = () => {
@@ -62082,7 +62383,6 @@ var branchSetup = ({
62082
62383
  if (!existsSync17(dir))
62083
62384
  mkdirSync8(dir, { recursive: true, mode: 493 });
62084
62385
  };
62085
- ensureManifestDir();
62086
62386
  const readManifest = () => {
62087
62387
  try {
62088
62388
  return JSON.parse(readFileSync15(manifestPath, "utf8"));
@@ -62100,13 +62400,16 @@ var branchSetup = ({
62100
62400
  const manifest = readManifest();
62101
62401
  const ref = manifest.stash_ref;
62102
62402
  if (!ref)
62103
- return { error: "no stash_ref in manifest" };
62403
+ return { error: "no stash_ref in manifest", phase: "preflight" };
62104
62404
  journal(`pre-pop: ${String(ref)}`);
62105
62405
  try {
62106
62406
  exec(["stash", "pop", String(ref)]);
62107
62407
  } catch (error) {
62108
62408
  journal("pop: failed");
62109
- return { error: error instanceof Error ? error.message : "stash pop failed" };
62409
+ return {
62410
+ error: error instanceof Error ? error.message : "stash pop failed",
62411
+ phase: "post_mutation"
62412
+ };
62110
62413
  }
62111
62414
  journal("pop: ok");
62112
62415
  delete manifest.stash_ref;
@@ -62116,11 +62419,17 @@ var branchSetup = ({
62116
62419
  }
62117
62420
  const target = target_branch ?? "";
62118
62421
  if (!target)
62119
- return { error: "target branch required" };
62422
+ return {
62423
+ error: "target_branch (the working branch to create or switch to) is required for setup",
62424
+ phase: "preflight"
62425
+ };
62120
62426
  if (isProtected(cwd, target))
62121
- return { error: `protected branch ${target}` };
62427
+ return { error: `protected branch ${target}`, phase: "preflight" };
62122
62428
  if (!allowedBranch(cwd, target))
62123
- return { error: `target branch ${target} is not allowed by the branch policy` };
62429
+ return {
62430
+ error: `target branch ${target} is not allowed by the branch policy`,
62431
+ phase: "preflight"
62432
+ };
62124
62433
  let base;
62125
62434
  let targetExists = true;
62126
62435
  try {
@@ -62131,12 +62440,12 @@ var branchSetup = ({
62131
62440
  if (!targetExists) {
62132
62441
  const baseResolved = baseBranch(cwd);
62133
62442
  if ("error" in baseResolved)
62134
- return { error: baseResolved.error };
62443
+ return { error: baseResolved.error, phase: "preflight" };
62135
62444
  base = baseResolved.base;
62136
62445
  }
62137
62446
  journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
62138
62447
  let stash_ref;
62139
- const failAfterStash = (message) => {
62448
+ const failAfterStash = (message, phase = "post_mutation") => {
62140
62449
  let suffix = "";
62141
62450
  if (stash_ref) {
62142
62451
  journal(`pre-pop: ${stash_ref}`);
@@ -62149,27 +62458,31 @@ var branchSetup = ({
62149
62458
  suffix = " (changes preserved in stash)";
62150
62459
  }
62151
62460
  }
62152
- return { error: `${message}${suffix}` };
62461
+ return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
62153
62462
  };
62154
62463
  if (current !== target) {
62155
62464
  const dirty = Boolean(gitContext(cwd).status_short.trim());
62156
62465
  if (dirty && stash !== "yes") {
62157
62466
  return {
62158
- error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes"
62467
+ error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
62468
+ phase: "preflight"
62159
62469
  };
62160
62470
  }
62161
62471
  let validatedBase;
62162
62472
  if (!targetExists && base !== undefined) {
62163
62473
  const ready = originBaseReady(cwd, base);
62164
62474
  if (!ready.ok)
62165
- return { error: ready.error ?? "ensure-base-branch failed" };
62475
+ return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
62166
62476
  validatedBase = base;
62167
62477
  }
62168
62478
  if (dirty) {
62169
62479
  try {
62170
62480
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
62171
62481
  } catch (error) {
62172
- return { error: error instanceof Error ? error.message : "stash push failed" };
62482
+ return {
62483
+ error: error instanceof Error ? error.message : "stash push failed",
62484
+ phase: "post_mutation"
62485
+ };
62173
62486
  }
62174
62487
  stash_ref = "stash@{0}";
62175
62488
  journal(`stash push: ${stash_ref}`);
@@ -62180,14 +62493,14 @@ var branchSetup = ({
62180
62493
  } catch (error) {
62181
62494
  const message = error instanceof Error ? error.message : "checkout failed";
62182
62495
  if (/worktree/i.test(message)) {
62183
- return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`);
62496
+ return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
62184
62497
  }
62185
62498
  try {
62186
62499
  let effectiveBase = base;
62187
62500
  if (effectiveBase === undefined) {
62188
62501
  const lateResolved = baseBranch(cwd);
62189
62502
  if ("error" in lateResolved)
62190
- return failAfterStash(lateResolved.error);
62503
+ return failAfterStash(lateResolved.error, "preflight");
62191
62504
  effectiveBase = lateResolved.base;
62192
62505
  }
62193
62506
  const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
@@ -64016,6 +64329,101 @@ var safePath = (root, value) => {
64016
64329
  return null;
64017
64330
  }
64018
64331
  };
64332
+ var branchCreationBaseline = (root, target, base) => {
64333
+ const exists = gitValue(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]) !== null;
64334
+ if (exists)
64335
+ return { base_branch: base, target_exists: true, remote_base: null };
64336
+ const remote = run3(root, ["ls-remote", "origin", `refs/heads/${base}`]);
64337
+ const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
64338
+ return {
64339
+ base_branch: base,
64340
+ target_exists: false,
64341
+ remote_base: /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null
64342
+ };
64343
+ };
64344
+ var shortSha = (value) => typeof value === "string" && value ? value.slice(0, 8) : "unknown";
64345
+ var isProtectedBranch2 = (root, branch) => resolveBranchPolicyFor(root).protected.has(branch.toLowerCase());
64346
+ var actionProposalQuestion = (request, descriptorPayload) => {
64347
+ const payload = descriptorPayload ?? {};
64348
+ const resolved = payload.resolved ?? {};
64349
+ const payloadTarget = String(payload.target_branch ?? "");
64350
+ if (request.operation === "git.commit" && Array.isArray(payload.plan_steps)) {
64351
+ const steps = payload.plan_steps;
64352
+ return {
64353
+ presented: `Workit decision: action — Approve committing the plan's ${steps.length} listed tasks on \`${String(payload.plan_branch ?? "")}\`?`,
64354
+ approvedText: `Approve the ${steps.length} listed plan commits.`
64355
+ };
64356
+ }
64357
+ switch (request.operation) {
64358
+ case "git.branch_setup": {
64359
+ if (request.payload.action === "reapply_stash")
64360
+ return {
64361
+ presented: "Workit decision: action — Reapply the pre-checkout stash recorded in the branch manifest?",
64362
+ approvedText: "Reapply the recorded stash."
64363
+ };
64364
+ const target = String(payload.target_branch ?? "");
64365
+ const stashes = payload.stash === "yes" && resolved.dirty === true;
64366
+ if (resolved.target_exists === true)
64367
+ return stashes ? {
64368
+ presented: `Workit decision: action — Stash the dirty working tree and switch to branch \`${target}\`?`,
64369
+ approvedText: `Stash and switch to \`${target}\`.`
64370
+ } : {
64371
+ presented: `Workit decision: action — Switch to branch \`${target}\`?`,
64372
+ approvedText: `Switch to \`${target}\`.`
64373
+ };
64374
+ const base = String(resolved.base_branch ?? "the base branch");
64375
+ return stashes ? {
64376
+ presented: `Workit decision: action — Stash the dirty working tree and create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
64377
+ approvedText: `Stash and create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
64378
+ } : {
64379
+ presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
64380
+ approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
64381
+ };
64382
+ }
64383
+ case "git.commit": {
64384
+ const paths = Array.isArray(resolved.paths) ? resolved.paths : [];
64385
+ return {
64386
+ presented: `Workit decision: action — Commit ${paths.length} staged file(s) as "${String(request.payload.message)}"?`,
64387
+ approvedText: `Commit as "${String(request.payload.message)}".`
64388
+ };
64389
+ }
64390
+ case "git.push":
64391
+ return {
64392
+ presented: `Workit decision: action — Push branch \`${String(payload.branch ?? "")}\` to origin?`,
64393
+ approvedText: `Push \`${String(payload.branch ?? "")}\` to origin.`
64394
+ };
64395
+ case "hosting.pull_request":
64396
+ return {
64397
+ presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
64398
+ approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
64399
+ };
64400
+ case "changelog.apply":
64401
+ return {
64402
+ presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
64403
+ approvedText: `Apply changelog changes at ${String(payload.path ?? "CHANGELOG.md")}.`
64404
+ };
64405
+ case "youtrack.update":
64406
+ return {
64407
+ presented: `Workit decision: action — Update YouTrack issue ${String(request.payload.issueId)}${request.payload.minutes ? ` and log ${String(request.payload.minutes)}m` : ""}?`,
64408
+ approvedText: `Update YouTrack issue ${String(request.payload.issueId)}.`
64409
+ };
64410
+ case "youtrack.time":
64411
+ return {
64412
+ presented: `Workit decision: action — Log ${String(request.payload.minutes)}m to YouTrack issue ${String(request.payload.issueId)}?`,
64413
+ approvedText: `Log time to YouTrack issue ${String(request.payload.issueId)}.`
64414
+ };
64415
+ case "youtrack.meeting":
64416
+ return {
64417
+ presented: `Workit decision: action — Log meeting time to YouTrack issue ${String(request.payload.issueId)}?`,
64418
+ approvedText: `Log meeting time to YouTrack issue ${String(request.payload.issueId)}.`
64419
+ };
64420
+ default:
64421
+ return {
64422
+ presented: `Workit decision: action — Approve ${request.operation}?`,
64423
+ approvedText: `Approve ${request.operation}.`
64424
+ };
64425
+ }
64426
+ };
64019
64427
  var approvedResolvedExternalAction = (content) => {
64020
64428
  try {
64021
64429
  const value = JSON.parse(content);
@@ -64279,24 +64687,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
64279
64687
  const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
64280
64688
  if (!sdd_dir)
64281
64689
  return failure("invalid_input", "sdd_dir must stay inside the workspace");
64690
+ const action = request.payload.action ?? "setup";
64282
64691
  const policy = resolveBranchPolicyFor(root);
64283
- const target_branch = request.payload.target_branch ?? policy.defaultTargetBranch;
64692
+ const target_branch = request.payload.target_branch;
64693
+ if (action !== "reapply_stash" && !target_branch)
64694
+ return failure("invalid_input", "target_branch (the working branch to create or switch to) is required for setup", {
64695
+ outcome: "not_started",
64696
+ fields: [{ path: "target_branch", reason: "required for action=setup" }]
64697
+ });
64284
64698
  const head = gitValue(root, ["rev-parse", "HEAD"]);
64285
64699
  if (!head)
64286
64700
  return failure("storage_error", "current Git commit could not be resolved");
64701
+ const dirty = Boolean((gitRaw(root, ["status", "--short"]) ?? "").trim());
64702
+ const creation = action === "reapply_stash" || !target_branch ? { base_branch: policy.defaultTargetBranch } : branchCreationBaseline(root, target_branch, policy.defaultTargetBranch);
64287
64703
  const normalized = {
64288
64704
  operation: request.operation,
64289
64705
  payload: {
64290
64706
  ...request.payload,
64291
- action: request.payload.action ?? "setup",
64707
+ action,
64292
64708
  sdd_dir,
64293
64709
  stash: request.payload.stash ?? "no",
64294
- target_branch
64710
+ ...target_branch ? { target_branch } : {}
64295
64711
  }
64296
64712
  };
64297
64713
  return success(null, null, {
64298
64714
  request: normalized,
64299
- descriptorPayload: { ...normalized.payload, resolved: { head, target_branch } }
64715
+ descriptorPayload: {
64716
+ ...normalized.payload,
64717
+ resolved: { head, dirty, ...creation }
64718
+ }
64300
64719
  });
64301
64720
  }
64302
64721
  case "git.push": {
@@ -64310,7 +64729,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
64310
64729
  }) : failure("invalid_input", "git push requires a resolvable current branch");
64311
64730
  }
64312
64731
  case "git.commit": {
64732
+ const planSteps = request.payload.plan_steps;
64733
+ const planBranch = request.payload.plan_branch;
64313
64734
  const head = gitValue(root, ["rev-parse", "HEAD"]);
64735
+ if (!head)
64736
+ return failure("storage_error", "Git state could not be resolved");
64737
+ const branch = gitValue(root, ["branch", "--show-current"]);
64738
+ if (planSteps !== undefined || planBranch !== undefined) {
64739
+ if (!Array.isArray(planSteps) || planSteps.some((step) => typeof step !== "string" || !step.trim()) || new Set(planSteps).size !== planSteps.length || planSteps.length === 0 || planSteps.length > 32)
64740
+ return failure("invalid_input", "plan_steps must be 1-32 unique non-empty commit messages", {
64741
+ outcome: "not_started",
64742
+ fields: [{ path: "plan_steps", reason: "unique strings" }]
64743
+ });
64744
+ if (typeof planBranch !== "string" || !planBranch.trim() || planBranch !== branch)
64745
+ return failure("invalid_input", "plan_branch must name the current working branch", {
64746
+ outcome: "not_started",
64747
+ fields: [{ path: "plan_branch", reason: "current branch" }]
64748
+ });
64749
+ if (isProtectedBranch2(root, planBranch))
64750
+ return failure("invalid_input", "plan_branch must not be a protected branch", {
64751
+ outcome: "not_started",
64752
+ fields: [{ path: "plan_branch", reason: "protected branch" }]
64753
+ });
64754
+ return success(null, null, {
64755
+ request,
64756
+ descriptorPayload: {
64757
+ plan_steps: planSteps,
64758
+ plan_branch: planBranch,
64759
+ resolved: { head, branch, steps: planSteps }
64760
+ }
64761
+ });
64762
+ }
64314
64763
  const staged = gitRaw(root, [
64315
64764
  "diff",
64316
64765
  "--cached",
@@ -64321,7 +64770,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
64321
64770
  "HEAD"
64322
64771
  ]);
64323
64772
  const paths = stagedPaths(root);
64324
- if (!head || staged === null || paths === null)
64773
+ const message = request.payload.message;
64774
+ if (typeof message !== "string" || !message)
64775
+ return failure("invalid_input", "git.commit requires a commit message", {
64776
+ outcome: "not_started",
64777
+ fields: [{ path: "message", reason: "required unless plan_steps is provided" }]
64778
+ });
64779
+ if (staged === null || paths === null)
64325
64780
  return failure("storage_error", "Git state could not be resolved");
64326
64781
  let policy;
64327
64782
  try {
@@ -64336,13 +64791,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
64336
64791
  `).filter(Boolean));
64337
64792
  flavor = detected.flavor ?? "conventional";
64338
64793
  }
64339
- if (!matchCommitFlavor(request.payload.message, flavor, policy.pattern))
64794
+ if (!matchCommitFlavor(message, flavor, policy.pattern))
64340
64795
  return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
64341
64796
  return success(null, null, {
64342
64797
  request,
64343
64798
  descriptorPayload: {
64344
64799
  ...request.payload,
64345
- resolved: { head, staged: sha256(staged), paths }
64800
+ resolved: { head, branch, staged: sha256(staged), paths }
64346
64801
  }
64347
64802
  });
64348
64803
  }
@@ -64512,10 +64967,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
64512
64967
  sdd_dir: request.payload.sdd_dir ?? "docs",
64513
64968
  workspace_root: root
64514
64969
  });
64515
- return result.error ? unknown2(request.operation) : success(null, null, result);
64970
+ if ("error" in result) {
64971
+ if (result.phase === "preflight")
64972
+ return failure("invalid_input", result.error, {
64973
+ outcome: "not_started",
64974
+ operation: request.operation
64975
+ });
64976
+ return unknown2(request.operation);
64977
+ }
64978
+ return success(null, null, result);
64516
64979
  }
64517
64980
  case "git.commit": {
64518
- const result = run3(root, ["commit", "-m", request.payload.message]);
64981
+ const message = request.payload.message;
64982
+ if (typeof message !== "string" || !message)
64983
+ return failure("invalid_input", "git.commit requires a commit message", {
64984
+ outcome: "not_started",
64985
+ fields: [{ path: "message", reason: "required" }]
64986
+ });
64987
+ const result = run3(root, ["commit", "-m", message]);
64519
64988
  return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown2(request.operation);
64520
64989
  }
64521
64990
  case "git.push": {
@@ -64921,7 +65390,29 @@ async function runTaskCommand(argv, deps = {}) {
64921
65390
  }
64922
65391
  const core = new WorkitCore(new TaskStore(root), contextFor(root, parsed.parsed.actor !== undefined ? { ...deps, actor: parsed.parsed.actor } : deps, observedConfirmation ? "host_observed" : "agent_reported"));
64923
65392
  let result;
64924
- if (parsed.parsed.handoff) {
65393
+ const decisionBudgetIssue = (() => {
65394
+ if (parsed.parsed.family !== "decision" || parsed.parsed.action !== "record")
65395
+ return null;
65396
+ const request = parsed.parsed.request;
65397
+ if (typeof request.purpose !== "string" || !request.binding)
65398
+ return null;
65399
+ return workitBindingQuestionIssue([
65400
+ {
65401
+ header: `Workit decision: ${request.purpose}`,
65402
+ question: typeof request.binding.presented === "string" ? request.binding.presented : "",
65403
+ options: [
65404
+ {
65405
+ label: "approved",
65406
+ description: typeof request.binding.displayed === "string" ? request.binding.displayed : typeof request.binding.approvedContent === "string" ? request.binding.approvedContent : ""
65407
+ },
65408
+ { label: "rejected", description: "Reject this decision" }
65409
+ ]
65410
+ }
65411
+ ]);
65412
+ })();
65413
+ if (decisionBudgetIssue)
65414
+ result = failure("invalid_input", decisionBudgetIssue);
65415
+ else if (parsed.parsed.handoff) {
64925
65416
  const exported = dispatch(core, "state", parsed.parsed.request);
64926
65417
  if (!exported.ok)
64927
65418
  result = exported;
@@ -64985,8 +65476,34 @@ var cliActionAuthority = (actor, reconciliationTokens = new WeakSet) => ({
64985
65476
  var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operationValue) => {
64986
65477
  const store = new TaskStore(root);
64987
65478
  const approved = approvedExternalAction(store, "workit_cli", actor, operationValue);
64988
- if (!approved.ok)
65479
+ if (!approved.ok) {
65480
+ const plan = planCommitBinding(store, "workit_cli", actor, operationValue);
65481
+ if (plan) {
65482
+ const actionRef = externalActionRef("workit_cli", actor, operationValue);
65483
+ return {
65484
+ taskId: plan.taskId,
65485
+ decisionId: plan.decisionId,
65486
+ actionRef,
65487
+ expectedRevision: plan.expectedRevision,
65488
+ expectedWorkspaceRevision: plan.expectedWorkspaceRevision,
65489
+ binding: plan.binding,
65490
+ step: plan.step,
65491
+ refresh: () => {
65492
+ const task = store.readTask(plan.taskId);
65493
+ const freshWorkspace = store.readWorkspace();
65494
+ if (!task.ok || !freshWorkspace.ok || !freshWorkspace.data)
65495
+ throw new Error("external action state changed");
65496
+ return {
65497
+ expectedRevision: task.data.revision,
65498
+ expectedWorkspaceRevision: freshWorkspace.data.revision
65499
+ };
65500
+ },
65501
+ reserveObservation: nativeExternalActionObservation(actor, actionRef, "reserve"),
65502
+ settleObservation: (outcome, revisions) => nativeExternalActionObservation(actor, actionRef, outcome, actionRef.kind === "host" ? actionRef.handle : "external-action", revisions)
65503
+ };
65504
+ }
64989
65505
  return approved;
65506
+ }
64990
65507
  const actionRef = externalActionRef("workit_cli", actor, operationValue);
64991
65508
  return {
64992
65509
  taskId: approved.data.task.id,
@@ -65050,7 +65567,7 @@ async function runActionCommand(argv, deps = {}) {
65050
65567
  return 2;
65051
65568
  }
65052
65569
  const root = deps.root ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? deps.cwd ?? process.cwd();
65053
- const resolved = resolveExternalActionRequest(root, parsed.data);
65570
+ let resolved = resolveExternalActionRequest(root, parsed.data);
65054
65571
  if (!resolved.ok) {
65055
65572
  if (json)
65056
65573
  jsonResult(outOf(deps), resolved);
@@ -65058,6 +65575,14 @@ async function runActionCommand(argv, deps = {}) {
65058
65575
  printHuman(resolved, deps);
65059
65576
  return 2;
65060
65577
  }
65578
+ if (resolved.data.request.operation === "git.branch_setup" && resolved.data.descriptorPayload.resolved?.dirty === true && resolved.data.descriptorPayload.stash !== "yes") {
65579
+ const upgraded = resolveExternalActionRequest(root, {
65580
+ ...parsed.data,
65581
+ payload: { ...parsed.data.payload, stash: "yes" }
65582
+ });
65583
+ if (upgraded.ok)
65584
+ resolved = upgraded;
65585
+ }
65061
65586
  const normalized = resolved.data.request;
65062
65587
  const descriptor = externalActionDescriptor(normalized.operation, resolved.data.descriptorPayload);
65063
65588
  if (preview) {
@@ -65183,8 +65708,56 @@ async function runActionCommand(argv, deps = {}) {
65183
65708
  printHuman(result, deps);
65184
65709
  return 1;
65185
65710
  }
65711
+ const drift = normalized.operation === "git.branch_setup" ? priorResolvedDrift(store, "workit_cli", actor, normalized.operation, normalized.payload, resolved.data.descriptorPayload.resolved) : success(null, null, null);
65712
+ if (!drift.ok) {
65713
+ if (json)
65714
+ jsonResult(outOf(deps), drift);
65715
+ else
65716
+ printHuman(drift, deps);
65717
+ return 1;
65718
+ }
65719
+ const localOperation = [
65720
+ "git.branch_setup",
65721
+ "git.commit",
65722
+ "git.push",
65723
+ "hosting.pull_request",
65724
+ "changelog.apply"
65725
+ ].includes(normalized.operation);
65726
+ if (localOperation) {
65727
+ const writer = assertLocalExternalActionWriter(root, { host: "workit_cli", actor });
65728
+ if (!writer.ok) {
65729
+ const result = failure("needs_input", "writer ownership is required before this action", {
65730
+ outcome: "not_started",
65731
+ operation: normalized.operation,
65732
+ guidance: "Acquire checkout writer ownership first (writer.acquire) and retry the action."
65733
+ });
65734
+ if (json)
65735
+ jsonResult(outOf(deps), result);
65736
+ else
65737
+ printHuman(result, deps);
65738
+ return 1;
65739
+ }
65740
+ }
65741
+ const question = actionProposalQuestion(normalized, resolved.data.descriptorPayload);
65742
+ const commitMessage = normalized.operation === "git.commit" ? normalized.payload.message : undefined;
65743
+ const currentBranch = resolved.data.descriptorPayload.resolved?.branch;
65744
+ const plan = typeof commitMessage === "string" && commitMessage ? approvedPlanCommit(store, "workit_cli", actor, commitMessage) : null;
65745
+ if (plan?.ok && plan.data.branch === currentBranch) {
65746
+ const core = new WorkitCore(store, {
65747
+ ...contextFor(root, { ...deps, actor }, "host_observed"),
65748
+ nativeAuthority: cliActionAuthority(actor),
65749
+ workerId: null
65750
+ });
65751
+ const runner = nativeExternalActionRunner(root, actor, core);
65752
+ const result = await runner(descriptor, (step) => executeResolvedExternalAction(resolved.data, root, step, { host: "workit_cli", actor }));
65753
+ if (json)
65754
+ jsonResult(outOf(deps), result);
65755
+ else
65756
+ printHuman(result, deps);
65757
+ return result.ok ? 0 : 1;
65758
+ }
65186
65759
  if (!json)
65187
- write2(outOf(deps), `External action preview: ${descriptor}`);
65760
+ write2(outOf(deps), question.presented);
65188
65761
  const accepted = await observeConsent(deps);
65189
65762
  if (!accepted.accepted || !accepted.observed) {
65190
65763
  const result = failure("permission_denied", "external action was not confirmed by the terminal");
@@ -65210,8 +65783,9 @@ async function runActionCommand(argv, deps = {}) {
65210
65783
  taskId: task.id,
65211
65784
  workspaceId: workspace.data.id,
65212
65785
  scope: task.intent.data.scope,
65213
- presented: `Approve ${descriptor}`,
65786
+ presented: question.presented,
65214
65787
  approvedContent: descriptor,
65788
+ displayed: question.approvedText,
65215
65789
  contentRefs: []
65216
65790
  },
65217
65791
  response: "approved",
@@ -65228,6 +65802,15 @@ async function runActionCommand(argv, deps = {}) {
65228
65802
  printHuman(decision, deps);
65229
65803
  return 1;
65230
65804
  }
65805
+ const planSteps = resolved.data.descriptorPayload.plan_steps;
65806
+ if (normalized.operation === "git.commit" && Array.isArray(planSteps)) {
65807
+ const result = success(null, null, { plan_commits: planSteps.length });
65808
+ if (json)
65809
+ jsonResult(outOf(deps), result);
65810
+ else
65811
+ printHuman(result, deps);
65812
+ return 0;
65813
+ }
65231
65814
  const runner = nativeExternalActionRunner(root, actor, core);
65232
65815
  const result = await runner(descriptor, (step) => executeResolvedExternalAction(resolved.data, root, step, { host: "workit_cli", actor }));
65233
65816
  if (json)