@brainervirus/workit-cli 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.
Files changed (2) hide show
  1. package/dist/index.js +688 -135
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -50927,6 +50927,7 @@ var decisionSchema = object({
50927
50927
  scope: scopeSchema,
50928
50928
  presented: text,
50929
50929
  approvedContent: text,
50930
+ displayed: text.optional(),
50930
50931
  contentRefs: array(refSchema)
50931
50932
  }).strict(),
50932
50933
  digest,
@@ -50939,6 +50940,27 @@ var decisionSchema = object({
50939
50940
  actionRef: refSchema
50940
50941
  }).strict().nullable()
50941
50942
  }).strict();
50943
+ var BINDING_QUESTION_BUDGET = 300;
50944
+ var bindingQuestionHeader = /^Workit decision: (design|action|limitation|preference)$/;
50945
+ var workitBindingQuestionIssue = (questions) => {
50946
+ if (!Array.isArray(questions) || questions.length !== 1)
50947
+ return null;
50948
+ const question = questions[0];
50949
+ if (!question || typeof question !== "object")
50950
+ return null;
50951
+ if (typeof question.header !== "string" || !bindingQuestionHeader.test(question.header.trim()))
50952
+ return null;
50953
+ 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.`;
50954
+ if (typeof question.question === "string" && question.question.length > BINDING_QUESTION_BUDGET)
50955
+ return over("the question text", question.question);
50956
+ if (Array.isArray(question.options))
50957
+ for (const option of question.options) {
50958
+ const description = option.description;
50959
+ if (typeof description === "string" && description.length > BINDING_QUESTION_BUDGET)
50960
+ return over("an option description", description);
50961
+ }
50962
+ return null;
50963
+ };
50942
50964
  var findingSchema = object({
50943
50965
  claim: text,
50944
50966
  consequence: text,
@@ -50965,7 +50987,7 @@ var workerReportSchema = object({
50965
50987
  }).strict();
50966
50988
  var workerSchema = object({
50967
50989
  assignment: assignmentSchema,
50968
- state: _enum(["assigned", "running", "cancelling", "stopped", "unknown"]),
50990
+ state: _enum(["assigned", "dispatching", "running", "cancelling", "stopped", "unknown"]),
50969
50991
  session: refSchema.nullable(),
50970
50992
  report: workerReportSchema.nullable()
50971
50993
  }).strict();
@@ -51005,6 +51027,10 @@ var actionProgressListSchema = array(actionProgressSchema).check((ctx) => {
51005
51027
  path: ["decisionId"]
51006
51028
  });
51007
51029
  });
51030
+ var runtimeSchema = object({
51031
+ createdWith: text.nullable(),
51032
+ updatedWith: text
51033
+ }).strict();
51008
51034
  var taskRecordSchema = object({
51009
51035
  schemaVersion: literal(1),
51010
51036
  id,
@@ -51018,6 +51044,8 @@ var taskRecordSchema = object({
51018
51044
  status: _enum(["active", "paused", "closed"]),
51019
51045
  closure: closureSchema.nullable(),
51020
51046
  progress: progressSchema,
51047
+ pauseReason: text.nullable().optional(),
51048
+ runtime: runtimeSchema.optional(),
51021
51049
  assessments: array(entrySchema(assessmentSchema)),
51022
51050
  policy: policySchema.nullable(),
51023
51051
  policyChanges: array(policyChangeSchema),
@@ -51033,6 +51061,7 @@ var workspaceRecordSchema = object({
51033
51061
  id,
51034
51062
  revision,
51035
51063
  root: nonEmpty,
51064
+ runtime: runtimeSchema.optional(),
51036
51065
  writer: object({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
51037
51066
  }).strict();
51038
51067
  var capabilitySchema = object({
@@ -51060,6 +51089,9 @@ var taskSummarySchema = object({
51060
51089
  id,
51061
51090
  revision,
51062
51091
  workspaceRevision: revision,
51092
+ createdAt: utc,
51093
+ updatedAt: utc,
51094
+ runtime: runtimeSchema.nullable(),
51063
51095
  objective: text,
51064
51096
  status: _enum(["active", "paused", "closed"]),
51065
51097
  closure: closureSchema.nullable(),
@@ -51130,7 +51162,7 @@ var taskOperations = {
51130
51162
  expectedWorkspaceRevision: revision.optional(),
51131
51163
  outcome: outcomeSchema,
51132
51164
  summary: text,
51133
- decisionIds: array(id)
51165
+ decisionIds: array(id).optional()
51134
51166
  })
51135
51167
  };
51136
51168
  var policyOperations = {
@@ -51222,14 +51254,15 @@ var writerOperations = {
51222
51254
  ...taskId,
51223
51255
  expectedRevision: revision.optional(),
51224
51256
  expectedWorkspaceRevision: revision.optional(),
51225
- workerId: nullableId.optional()
51257
+ workerId: nullableId.optional(),
51258
+ reason: text.optional()
51226
51259
  }),
51227
51260
  release: operation({
51228
51261
  action: literal("release"),
51229
51262
  ...taskId,
51230
51263
  expectedRevision: revision.optional(),
51231
51264
  expectedWorkspaceRevision: revision.optional(),
51232
- reason: text
51265
+ reason: text.optional()
51233
51266
  })
51234
51267
  };
51235
51268
  var stateOperations = {
@@ -57208,6 +57241,33 @@ var metadataLockSchema = object({
57208
57241
  nonce: string2().min(1)
57209
57242
  }).strict();
57210
57243
  var now2 = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
57244
+ var cachedRuntimeVersion = null;
57245
+ var runtimeVersion = () => {
57246
+ if (cachedRuntimeVersion === null) {
57247
+ try {
57248
+ const pkg = JSON.parse(fs7.readFileSync(path19.join(packageRoot(), "package.json"), "utf8"));
57249
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
57250
+ } catch {
57251
+ cachedRuntimeVersion = "0.0.0";
57252
+ }
57253
+ }
57254
+ return cachedRuntimeVersion;
57255
+ };
57256
+ var stampNew = (record) => ({
57257
+ createdWith: record.runtime?.createdWith ?? null,
57258
+ updatedWith: runtimeVersion()
57259
+ });
57260
+ var isNewerVersion = (candidate, current) => {
57261
+ const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
57262
+ const [next, now] = [parts(candidate), parts(current)];
57263
+ for (let index = 0;index < 3; index += 1) {
57264
+ const left = next[index] ?? 0;
57265
+ const right = now[index] ?? 0;
57266
+ if (left !== right)
57267
+ return left > right;
57268
+ }
57269
+ return false;
57270
+ };
57211
57271
  var jsonBytes = (value) => `${canonicalJson(value)}
57212
57272
  `;
57213
57273
  var digestBytes2 = (value) => createHash4("sha256").update(value).digest("hex");
@@ -57307,7 +57367,7 @@ class TaskStore {
57307
57367
  if (!current.ok)
57308
57368
  return current;
57309
57369
  if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
57310
- return failure("revision_conflict", "workspace revision does not match", {
57370
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
57311
57371
  expectedWorkspaceRevision: value.expectedWorkspaceRevision,
57312
57372
  actualWorkspaceRevision: current.data?.revision ?? null
57313
57373
  });
@@ -57319,11 +57379,17 @@ class TaskStore {
57319
57379
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
57320
57380
  if (current.data && !previousWorkspaceBytes)
57321
57381
  return failure("storage_error", "workspace snapshot disappeared during creation");
57322
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
57382
+ const workspace = current.data ? {
57383
+ ...current.data,
57384
+ revision: newRevision(),
57385
+ root: this.root,
57386
+ runtime: stampNew(current.data)
57387
+ } : {
57323
57388
  schemaVersion: SCHEMA_VERSION,
57324
57389
  id: newId(),
57325
57390
  revision: newRevision(),
57326
57391
  root: this.root,
57392
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
57327
57393
  writer: null
57328
57394
  };
57329
57395
  const timestamp = value.now ?? now2();
@@ -57345,6 +57411,7 @@ class TaskStore {
57345
57411
  status: "active",
57346
57412
  closure: null,
57347
57413
  progress: { summary: "", nextAction: null, blockers: [] },
57414
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
57348
57415
  assessments: [],
57349
57416
  policy: null,
57350
57417
  policyChanges: [],
@@ -57375,7 +57442,7 @@ class TaskStore {
57375
57442
  if (!current.ok)
57376
57443
  return current;
57377
57444
  if (current.data ? input.expectedWorkspaceRevision !== current.data.revision : input.expectedWorkspaceRevision !== null)
57378
- return failure("revision_conflict", "workspace revision does not match", {
57445
+ return failure("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
57379
57446
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
57380
57447
  actualWorkspaceRevision: current.data?.revision ?? null
57381
57448
  });
@@ -57383,11 +57450,17 @@ class TaskStore {
57383
57450
  if (current.data && !previousWorkspaceBytes)
57384
57451
  return failure("storage_error", "workspace snapshot disappeared during import");
57385
57452
  const timestamp = input.now ?? now2();
57386
- const workspace = current.data ? { ...current.data, revision: newRevision(), root: this.root } : {
57453
+ const workspace = current.data ? {
57454
+ ...current.data,
57455
+ revision: newRevision(),
57456
+ root: this.root,
57457
+ runtime: stampNew(current.data)
57458
+ } : {
57387
57459
  schemaVersion: SCHEMA_VERSION,
57388
57460
  id: input.workspaceId ?? newId(),
57389
57461
  revision: newRevision(),
57390
57462
  root: this.root,
57463
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
57391
57464
  writer: null
57392
57465
  };
57393
57466
  const task = {
@@ -57395,7 +57468,11 @@ class TaskStore {
57395
57468
  workspaceId: workspace.id,
57396
57469
  revision: newRevision(),
57397
57470
  createdAt: timestamp,
57398
- updatedAt: timestamp
57471
+ updatedAt: timestamp,
57472
+ runtime: {
57473
+ createdWith: input.task.runtime?.createdWith ?? null,
57474
+ updatedWith: runtimeVersion()
57475
+ }
57399
57476
  };
57400
57477
  const validTask = taskRecordSchema.safeParse(task);
57401
57478
  if (!validTask.success)
@@ -57444,7 +57521,8 @@ class TaskStore {
57444
57521
  workspaceId: current.data.workspaceId,
57445
57522
  createdAt: current.data.createdAt,
57446
57523
  revision: context.revision,
57447
- updatedAt: context.now
57524
+ updatedAt: context.now,
57525
+ runtime: stampNew(current.data)
57448
57526
  };
57449
57527
  const valid = taskRecordSchema.safeParse(record);
57450
57528
  if (!valid.success)
@@ -57478,7 +57556,8 @@ class TaskStore {
57478
57556
  ...changed.data,
57479
57557
  id: current.data.id,
57480
57558
  root: this.root,
57481
- revision: context.revision
57559
+ revision: context.revision,
57560
+ runtime: stampNew(current.data)
57482
57561
  };
57483
57562
  const valid = workspaceRecordSchema.safeParse(record);
57484
57563
  if (!valid.success)
@@ -57521,7 +57600,8 @@ class TaskStore {
57521
57600
  ...changedWorkspace.data,
57522
57601
  id: workspace.data.id,
57523
57602
  root: this.root,
57524
- revision: workspaceContext.revision
57603
+ revision: workspaceContext.revision,
57604
+ runtime: stampNew(workspace.data)
57525
57605
  });
57526
57606
  if (!nextWorkspace.success)
57527
57607
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -57546,7 +57626,8 @@ class TaskStore {
57546
57626
  workspaceId: task.data.workspaceId,
57547
57627
  createdAt: task.data.createdAt,
57548
57628
  revision: taskContext.revision,
57549
- updatedAt: taskContext.now
57629
+ updatedAt: taskContext.now,
57630
+ runtime: stampNew(task.data)
57550
57631
  });
57551
57632
  if (!nextTask.success) {
57552
57633
  this.markUncertain(nextWorkspace.data);
@@ -57766,7 +57847,8 @@ class TaskStore {
57766
57847
  const value = {
57767
57848
  ...workspace,
57768
57849
  writer: { ...workspace.writer, state: "uncertain" },
57769
- revision: newRevision()
57850
+ revision: newRevision(),
57851
+ runtime: stampNew(workspace)
57770
57852
  };
57771
57853
  this.replaceSnapshot(this.workspacePath, value, workspace);
57772
57854
  }
@@ -57980,7 +58062,12 @@ class TaskStore {
57980
58062
  if (isObject3(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
57981
58063
  return failure("unsupported_version", "unsupported snapshot schema version");
57982
58064
  const parsed = schema.safeParse(value);
57983
- return parsed.success ? success(null, null, parsed.data) : failure("recovery_required", "snapshot does not satisfy its schema");
58065
+ if (parsed.success)
58066
+ return success(null, null, parsed.data);
58067
+ const writerVersion = isObject3(value) && isObject3(value.runtime) ? value.runtime.updatedWith : null;
58068
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
58069
+ return failure("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
58070
+ return failure("recovery_required", "snapshot does not satisfy its schema");
57984
58071
  }
57985
58072
  processStart(pid) {
57986
58073
  try {
@@ -57990,7 +58077,7 @@ class TaskStore {
57990
58077
  }
57991
58078
  }
57992
58079
  conflict(expected, actual) {
57993
- return failure("revision_conflict", "snapshot revision does not match", {
58080
+ return failure("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
57994
58081
  expectedRevision: expected,
57995
58082
  actualRevision: actual
57996
58083
  });
@@ -58017,6 +58104,61 @@ class TaskStore {
58017
58104
  return path19.join(this.workitDir, ".gitignore");
58018
58105
  }
58019
58106
  }
58107
+ // packages/workit-core/src/core/workers.ts
58108
+ var same = (left, right) => {
58109
+ try {
58110
+ return canonicalJson(left) === canonicalJson(right);
58111
+ } catch {
58112
+ return false;
58113
+ }
58114
+ };
58115
+ var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
58116
+ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
58117
+ var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
58118
+ var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
58119
+ const code = char.charCodeAt(0);
58120
+ return code < 32 || code === 127;
58121
+ }) && !value.split(/[\\/]/).includes("..");
58122
+ var callerValue = (value) => ({ host: value.host, actor: value.actor });
58123
+ var workerIdOf = (value) => value.workerId ?? null;
58124
+ function assertProductWriteAllowed(input) {
58125
+ if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
58126
+ return failure("invalid_input", "invalid product write path");
58127
+ if (!input.store)
58128
+ return failure("permission_denied", "write authorization requires core state");
58129
+ const task = input.store.readTask(input.task.id);
58130
+ if (!task.ok)
58131
+ return task;
58132
+ const workspace = input.store.readWorkspace();
58133
+ if (!workspace.ok)
58134
+ return workspace;
58135
+ if (!workspace.data)
58136
+ return failure("not_found", "workspace not found");
58137
+ const workspaceRecord = workspace.data;
58138
+ if (task.data.status !== "active")
58139
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
58140
+ if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
58141
+ return failure("recovery_required", "task and workspace bindings are invalid");
58142
+ const writer = workspaceRecord.writer;
58143
+ if (!writer)
58144
+ return failure("permission_denied", "checkout has no writer owner");
58145
+ if (writer.state === "uncertain")
58146
+ return failure("recovery_required", "checkout writer requires recovery", {
58147
+ owner: writer.owner
58148
+ });
58149
+ const caller = callerValue(input.caller);
58150
+ const workerId = workerIdOf(input.caller);
58151
+ const owner = writer.owner;
58152
+ if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
58153
+ return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
58154
+ const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
58155
+ if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
58156
+ return failure("permission_denied", "only implementers can own product writes");
58157
+ 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))))
58158
+ return failure("recovery_required", "worker session is not an active writer");
58159
+ return success(null, workspaceRecord.revision, owner);
58160
+ }
58161
+
58020
58162
  // packages/workit-core/src/core/task-evaluation.ts
58021
58163
  import { createHash as createHash6 } from "node:crypto";
58022
58164
  import * as fs9 from "node:fs";
@@ -58155,7 +58297,11 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
58155
58297
  return failure("invalid_input", "document references require a byte digest");
58156
58298
  const target = path20.resolve(root, reference.path);
58157
58299
  if (target !== root && !target.startsWith(`${root}${path20.sep}`))
58158
- return failure("invalid_input", "document reference escapes checkout");
58300
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
58301
+ fields: [
58302
+ { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
58303
+ ]
58304
+ });
58159
58305
  try {
58160
58306
  const relative = path20.relative(root, target);
58161
58307
  let current = root;
@@ -58166,7 +58312,14 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
58166
58312
  const stat = fs8.lstatSync(current);
58167
58313
  const real = fs8.realpathSync(current);
58168
58314
  if (real !== root && !real.startsWith(`${root}${path20.sep}`))
58169
- return failure("invalid_input", "document reference escapes checkout");
58315
+ return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
58316
+ fields: [
58317
+ {
58318
+ path: "contentRefs.path",
58319
+ reason: `${reference.path} resolves outside the checkout`
58320
+ }
58321
+ ]
58322
+ });
58170
58323
  if (stat.isSymbolicLink())
58171
58324
  return failure("invalid_input", "document reference uses a symlink");
58172
58325
  if (current === target && !stat.isFile())
@@ -58610,15 +58763,15 @@ var scopeRoots = (root, scope) => {
58610
58763
  for (const item of scope.paths.length ? scope.paths : ["."]) {
58611
58764
  const target = path21.resolve(root, item);
58612
58765
  if (!inside(root, target))
58613
- return failure("invalid_input", "candidate scope escapes checkout");
58766
+ 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
58767
  let ancestor = target;
58615
58768
  while (!fs9.existsSync(ancestor) && ancestor !== root)
58616
58769
  ancestor = path21.dirname(ancestor);
58617
58770
  try {
58618
58771
  if (!inside(root, fs9.realpathSync(ancestor)))
58619
- return failure("invalid_input", "candidate scope escapes checkout");
58772
+ 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
58773
  } catch {
58621
- return failure("invalid_input", "candidate scope cannot be inspected");
58774
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
58622
58775
  }
58623
58776
  roots.push(target);
58624
58777
  }
@@ -58633,7 +58786,7 @@ function captureCandidate(root, scope, environment = []) {
58633
58786
  }
58634
58787
  const normalizedScope = canonicalScope(scope);
58635
58788
  if (!normalizedScope)
58636
- return failure("invalid_input", "candidate scope is invalid");
58789
+ 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
58790
  const roots = scopeRoots(checkout, normalizedScope);
58638
58791
  if (!roots.ok)
58639
58792
  return roots;
@@ -58822,7 +58975,7 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
58822
58975
  return true;
58823
58976
  const reviewSession = sessionFromRef(entry.data.reviewContext);
58824
58977
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
58825
- const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session));
58978
+ const sameEvidenceSession = task.evidence.some((other) => other.id !== entry.id && other.data.kind === "review" && sameSession(reviewSession, other.provenance.session));
58826
58979
  return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
58827
58980
  });
58828
58981
  if (passed.length) {
@@ -58918,7 +59071,17 @@ function evaluateClosure(requestedOutcome, view) {
58918
59071
  const blocking = evaluations.filter((item) => item.status === "unsatisfied" || item.status === "unavailable");
58919
59072
  if (requestedOutcome !== "stopped" && blocking.length)
58920
59073
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
58921
- requirementIds: blocking.map((item) => item.requirementId)
59074
+ requirementIds: blocking.map((item) => item.requirementId),
59075
+ requirements: blocking.map((item) => {
59076
+ const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
59077
+ return {
59078
+ requirementId: item.requirementId,
59079
+ ruleId: requirement?.ruleId ?? "unknown",
59080
+ reason: item.reason,
59081
+ satisfaction: requirement?.satisfaction ?? "",
59082
+ dependentAction: requirement?.dependentAction ?? null
59083
+ };
59084
+ })
58922
59085
  });
58923
59086
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
58924
59087
  if (requestedOutcome === "verified" && accepted.length)
@@ -58945,6 +59108,52 @@ function evaluateClosure(requestedOutcome, view) {
58945
59108
  });
58946
59109
  }
58947
59110
 
59111
+ // packages/workit-core/src/core/methods.ts
59112
+ var METHODS = {
59113
+ "workit-challenge": { dimensions: ["challenge", "decisions"] },
59114
+ "workit-behavioral-tdd": { dimensions: ["testing"] },
59115
+ "workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review"] },
59116
+ "workit-plan": { dimensions: ["artifacts", "continuity"] },
59117
+ "workit-implement": { dimensions: ["delegation"] },
59118
+ "workit-debug": { ruleIds: ["root-cause-investigation"] },
59119
+ "workit-handoff": { ruleIds: ["durable-handoff"] },
59120
+ "workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
59121
+ };
59122
+ var methodMatches = (id, definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true && !(id === "workit-review" && requirement.ruleId === "self-review") || definition.ruleIds?.includes(requirement.ruleId) === true;
59123
+ var assuranceFor = (id, definition, capabilities) => {
59124
+ const keys = new Set([
59125
+ id,
59126
+ ...definition.dimensions ?? [],
59127
+ ...definition.ruleIds ?? []
59128
+ ]);
59129
+ const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
59130
+ if (id === "workit-review" && matches.length === 0)
59131
+ return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
59132
+ if (matches.some((capability) => capability.assurance === "unavailable"))
59133
+ return {
59134
+ assurance: "unavailable",
59135
+ reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
59136
+ };
59137
+ if (matches.some((capability) => capability.assurance === "enforced"))
59138
+ return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
59139
+ return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
59140
+ };
59141
+ function selectMethods(policy, capabilities) {
59142
+ return Object.entries(METHODS).flatMap(([id, definition]) => {
59143
+ const requirements = policy.requirements.filter((requirement) => methodMatches(id, definition, requirement));
59144
+ if (requirements.length === 0)
59145
+ return [];
59146
+ const capability = assuranceFor(id, definition, capabilities);
59147
+ const reasons = [
59148
+ ...new Set(requirements.map((requirement) => requirement.reason)),
59149
+ ...capability.reasons
59150
+ ];
59151
+ if (capability.assurance === "unavailable")
59152
+ reasons.unshift("required capability is unavailable");
59153
+ return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
59154
+ });
59155
+ }
59156
+
58948
59157
  // packages/workit-core/src/core/task-context.ts
58949
59158
  function reconcileResume(view, observations = []) {
58950
59159
  if (observations.length > 0)
@@ -58954,7 +59163,7 @@ function reconcileResume(view, observations = []) {
58954
59163
  return candidate;
58955
59164
  const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
58956
59165
  const blockers = [...view.task.progress.blockers];
58957
- if (view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
59166
+ if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
58958
59167
  blockers.push({
58959
59168
  reason: "worker state requires reconciliation",
58960
59169
  dependentAction: "resume",
@@ -59004,6 +59213,11 @@ function compactTaskContext(view) {
59004
59213
  digest: entry.data.digest,
59005
59214
  references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
59006
59215
  }));
59216
+ const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
59217
+ id: method.id,
59218
+ assurance: method.assurance,
59219
+ reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
59220
+ })) : [];
59007
59221
  const gaps = Array.from(new Set([
59008
59222
  ...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
59009
59223
  ...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
@@ -59013,15 +59227,18 @@ function compactTaskContext(view) {
59013
59227
  objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
59014
59228
  status: view.task.status,
59015
59229
  decisions,
59230
+ methods,
59016
59231
  gaps,
59017
59232
  nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
59018
59233
  };
59019
59234
  let encoded = JSON.stringify(context);
59020
- while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.gaps.length)) {
59235
+ while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
59021
59236
  if (context.gaps.length)
59022
59237
  context.gaps.pop();
59023
- else
59238
+ else if (context.decisions.length)
59024
59239
  context.decisions.pop();
59240
+ else
59241
+ context.methods.pop();
59025
59242
  encoded = JSON.stringify(context);
59026
59243
  }
59027
59244
  if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
@@ -59038,6 +59255,7 @@ var exportDigest = (bundle) => sha256({
59038
59255
  task: bundle.task
59039
59256
  });
59040
59257
  // packages/workit-core/src/core/external-action.ts
59258
+ import { execFileSync as execFileSync2 } from "node:child_process";
59041
59259
  var externalActionSchema = discriminatedUnion("operation", [
59042
59260
  object({
59043
59261
  operation: literal("git.branch_setup"),
@@ -59050,7 +59268,11 @@ var externalActionSchema = discriminatedUnion("operation", [
59050
59268
  }).strict(),
59051
59269
  object({
59052
59270
  operation: literal("git.commit"),
59053
- payload: object({ message: string2().min(1) }).strict()
59271
+ payload: object({
59272
+ message: string2().min(1).optional(),
59273
+ plan_steps: array(string2().min(1)).optional(),
59274
+ plan_branch: string2().min(1).optional()
59275
+ }).strict()
59054
59276
  }).strict(),
59055
59277
  object({
59056
59278
  operation: literal("git.push"),
@@ -59139,7 +59361,7 @@ var externalActionDescriptor = (operation, payload) => canonicalJson({
59139
59361
  payload,
59140
59362
  ...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 } : {}
59141
59363
  });
59142
- 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).";
59364
+ 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).";
59143
59365
  var externalActionRef = (host, actor, descriptor) => ({
59144
59366
  kind: "host",
59145
59367
  host,
@@ -59190,6 +59412,92 @@ var priorExternalAction = (store, host, actor, operation, payload) => {
59190
59412
  return failure("permission_denied", candidates.length === 0 ? "no prior external action matches this request" : "multiple prior external actions match this request");
59191
59413
  return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
59192
59414
  };
59415
+ var planCommitDescriptor = (content) => {
59416
+ try {
59417
+ const value = JSON.parse(content);
59418
+ if (value.operation !== "git.commit" || !value.payload)
59419
+ return null;
59420
+ const steps = value.payload.plan_steps;
59421
+ const branch = value.payload.plan_branch;
59422
+ if (!Array.isArray(steps) || steps.some((step) => typeof step !== "string" || !step) || typeof branch !== "string" || !branch)
59423
+ return null;
59424
+ return { steps, branch };
59425
+ } catch {
59426
+ return null;
59427
+ }
59428
+ };
59429
+ var approvedPlanCommit = (store, host, actor, message) => {
59430
+ if (typeof message !== "string" || !message)
59431
+ return failure("invalid_input", "commit message is required");
59432
+ const listed = store.listTasks();
59433
+ const workspace = store.readWorkspace();
59434
+ if (!listed.ok || !workspace.ok)
59435
+ return failure("storage_error", "external action state is unavailable");
59436
+ if (!workspace.data)
59437
+ return failure("not_found", "workspace not found");
59438
+ const matches = listed.data.flatMap((task) => {
59439
+ if (task.status !== "active" || task.workspaceId !== workspace.data.id)
59440
+ return [];
59441
+ if (task.intent.provenance.session?.kind !== "host" || task.intent.provenance.session.host !== host || task.intent.provenance.session.handle !== actor)
59442
+ return [];
59443
+ return task.decisions.flatMap((entry) => {
59444
+ const decision = entry.data;
59445
+ if (decision.purpose !== "action" || decision.response !== "approved" || decision.revoked !== null)
59446
+ return [];
59447
+ if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
59448
+ return [];
59449
+ const plan = planCommitDescriptor(decision.binding.approvedContent);
59450
+ if (!plan)
59451
+ return [];
59452
+ const completed = task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ?? [];
59453
+ if (plan.steps[completed.length] !== message)
59454
+ return [];
59455
+ return [{ task, entry, steps: plan.steps, branch: plan.branch, completed: [...completed] }];
59456
+ });
59457
+ });
59458
+ if (matches.length !== 1)
59459
+ return failure("permission_denied", matches.length === 0 ? "no plan commit authorization matches this message" : "multiple plan authorizations match this message");
59460
+ return success(workspace.data.revision, null, matches[0]);
59461
+ };
59462
+ var commitMessageFromDescriptor = (operation) => {
59463
+ try {
59464
+ const descriptor = JSON.parse(operation);
59465
+ return descriptor.operation === "git.commit" && typeof descriptor.payload?.message === "string" ? descriptor.payload.message : undefined;
59466
+ } catch {
59467
+ return;
59468
+ }
59469
+ };
59470
+ var planCommitBinding = (store, host, actor, operation) => {
59471
+ const message = commitMessageFromDescriptor(operation);
59472
+ if (!message)
59473
+ return null;
59474
+ const plan = approvedPlanCommit(store, host, actor, message);
59475
+ if (!plan.ok)
59476
+ return null;
59477
+ let branch;
59478
+ try {
59479
+ branch = execFileSync2("git", ["branch", "--show-current"], {
59480
+ cwd: store.root,
59481
+ encoding: "utf8",
59482
+ stdio: ["pipe", "pipe", "pipe"]
59483
+ }).trim();
59484
+ } catch {
59485
+ return null;
59486
+ }
59487
+ if (!branch || branch !== plan.data.branch)
59488
+ return null;
59489
+ const workspace = store.readWorkspace();
59490
+ if (!workspace.ok || !workspace.data)
59491
+ return null;
59492
+ return {
59493
+ taskId: plan.data.task.id,
59494
+ decisionId: plan.data.entry.id,
59495
+ binding: plan.data.entry.data.binding,
59496
+ expectedRevision: plan.data.task.revision,
59497
+ expectedWorkspaceRevision: workspace.data.revision,
59498
+ step: message
59499
+ };
59500
+ };
59193
59501
  var approvedExternalAction = (store, host, actor, operation) => {
59194
59502
  const selected = externalActionState(store, host, actor, operation);
59195
59503
  if (!selected.ok)
@@ -59198,6 +59506,23 @@ var approvedExternalAction = (store, host, actor, operation) => {
59198
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");
59199
59507
  return selected;
59200
59508
  };
59509
+ var priorResolvedDrift = (store, host, actor, operation, payload, currentResolved) => {
59510
+ const prior = priorExternalAction(store, host, actor, operation, payload);
59511
+ if (!prior.ok)
59512
+ return success(null, null, null);
59513
+ let baseline;
59514
+ try {
59515
+ const parsed = JSON.parse(prior.data.entry.data.binding.approvedContent);
59516
+ baseline = parsed.payload?.resolved;
59517
+ } catch {
59518
+ return success(null, null, null);
59519
+ }
59520
+ if (baseline === undefined || baseline === null)
59521
+ return success(null, null, null);
59522
+ if (canonicalJson(baseline) === canonicalJson(currentResolved))
59523
+ return success(null, null, null);
59524
+ return failure("invalid_input", "repository state changed since approval; show the current state and approve the action again", { outcome: "not_started", operation });
59525
+ };
59201
59526
  var createAuthorizedExternalActionRunner = (core, bind) => async (operation, effect) => {
59202
59527
  let steps = [];
59203
59528
  try {
@@ -59456,7 +59781,7 @@ function resolveRequirements(input) {
59456
59781
  ].join(", ")}`,
59457
59782
  satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
59458
59783
  before: "dependent_action",
59459
- dependentAction: "dependent-action",
59784
+ dependentAction: null,
59460
59785
  acceptanceAllowed: false
59461
59786
  }));
59462
59787
  if (signals.productChoiceOpen.value === true)
@@ -59467,7 +59792,7 @@ function resolveRequirements(input) {
59467
59792
  reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
59468
59793
  satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
59469
59794
  before: "dependent_action",
59470
- dependentAction: "dependent-action",
59795
+ dependentAction: null,
59471
59796
  acceptanceAllowed: false
59472
59797
  }));
59473
59798
  if (signals.behaviorChange.value === true)
@@ -59625,59 +59950,6 @@ function diffPolicy(previous, next, reason, now) {
59625
59950
  return parsed.data;
59626
59951
  }
59627
59952
 
59628
- // packages/workit-core/src/core/workers.ts
59629
- var same = (left, right) => {
59630
- try {
59631
- return canonicalJson(left) === canonicalJson(right);
59632
- } catch {
59633
- return false;
59634
- }
59635
- };
59636
- var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
59637
- var validPath = (value) => typeof value === "string" && value.length > 0 && ![...value].some((char) => {
59638
- const code = char.charCodeAt(0);
59639
- return code < 32 || code === 127;
59640
- }) && !value.split(/[\\/]/).includes("..");
59641
- var callerValue = (value) => ({ host: value.host, actor: value.actor });
59642
- var workerIdOf = (value) => value.workerId ?? null;
59643
- function assertProductWriteAllowed(input) {
59644
- if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
59645
- return failure("invalid_input", "invalid product write path");
59646
- if (!input.store)
59647
- return failure("permission_denied", "write authorization requires core state");
59648
- const task = input.store.readTask(input.task.id);
59649
- if (!task.ok)
59650
- return task;
59651
- const workspace = input.store.readWorkspace();
59652
- if (!workspace.ok)
59653
- return workspace;
59654
- if (!workspace.data)
59655
- return failure("not_found", "workspace not found");
59656
- const workspaceRecord = workspace.data;
59657
- if (task.data.status !== "active")
59658
- return failure("invalid_transition", "paused or closed tasks cannot own product writes");
59659
- if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
59660
- return failure("recovery_required", "task and workspace bindings are invalid");
59661
- const writer = workspaceRecord.writer;
59662
- if (!writer)
59663
- return failure("permission_denied", "checkout has no writer owner");
59664
- if (writer.state === "uncertain")
59665
- return failure("recovery_required", "checkout writer requires recovery", {
59666
- owner: writer.owner
59667
- });
59668
- const caller = callerValue(input.caller);
59669
- const workerId = workerIdOf(input.caller);
59670
- const owner = writer.owner;
59671
- if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
59672
- return failure("writer_conflict", "checkout is owned by another validated actor", { owner });
59673
- const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
59674
- if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
59675
- return failure("permission_denied", "only implementers can own product writes");
59676
- 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))))
59677
- return failure("recovery_required", "worker session is not an active writer");
59678
- return success(null, workspaceRecord.revision, owner);
59679
- }
59680
-
59681
59953
  // packages/workit-core/src/core/task-engine.ts
59682
59954
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
59683
59955
  kind,
@@ -59796,10 +60068,10 @@ var applyWorkerLifecycle = (input) => {
59796
60068
  const notStarted = input.dispatch === true;
59797
60069
  if (notStarted && (input.state !== "stopped" || input.session !== null))
59798
60070
  return failure("invalid_input", "a never-dispatched worker stops with no session");
59799
- if (notStarted && (entry.data.state === "stopped" || entry.data.state === "unknown"))
59800
- return failure("invalid_transition", "worker is no longer awaiting dispatch");
59801
- if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
60071
+ if (notStarted && entry.data.session !== null)
59802
60072
  return failure("invalid_transition", "worker already has an observed session");
60073
+ if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
60074
+ return failure("invalid_transition", "worker launch was not claimed");
59803
60075
  if ((input.state === "running" || input.state === "cancelling" || input.state === "stopped" && !notStarted) && !input.session)
59804
60076
  return failure("invalid_input", "running worker observations require a session");
59805
60077
  if (entry.data.session && !sameValue(entry.data.session, input.session))
@@ -59808,15 +60080,20 @@ var applyWorkerLifecycle = (input) => {
59808
60080
  return failure("invalid_transition", "worker has not started");
59809
60081
  if (input.state === "running" && entry.data.state === "stopped")
59810
60082
  return failure("invalid_transition", "stopped worker cannot run again");
60083
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
59811
60084
  const nextEntry = {
59812
60085
  ...entry,
59813
60086
  recordedAt: input.now ?? entry.recordedAt,
59814
60087
  provenance: authority.provenance,
59815
- data: { ...entry.data, state: input.state, session: input.session }
60088
+ data: {
60089
+ ...entry.data,
60090
+ state: terminalCancel ? "cancelling" : input.state,
60091
+ session: input.session
60092
+ }
59816
60093
  };
59817
60094
  const shouldClear = input.state === "stopped" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
59818
60095
  const shouldUncertain = input.state === "unknown" && workspace.data.writer !== null && workspace.data.writer.owner.taskId === task.data.id && workspace.data.writer.owner.workerId === input.workerId;
59819
- if (entry.data.state === input.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
60096
+ if (entry.data.state === nextEntry.data.state && sameValue(entry.data.session, input.session) && !shouldClear && !shouldUncertain)
59820
60097
  return success(task.data.revision, workspace.data.revision, entry);
59821
60098
  const changed = input.store.mutateTaskAndWorkspace({
59822
60099
  taskId: task.data.id,
@@ -60186,7 +60463,7 @@ class WorkitCore {
60186
60463
  activeWorkerBlocker(task, workspace) {
60187
60464
  if (workspace.writer)
60188
60465
  return failure("recovery_required", "writer ownership must be released first");
60189
- if (task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state)))
60466
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
60190
60467
  return failure("recovery_required", "worker state requires reconciliation");
60191
60468
  return null;
60192
60469
  }
@@ -60423,11 +60700,6 @@ class WorkitCore {
60423
60700
  if (helper && !helper.ok)
60424
60701
  return helper;
60425
60702
  const evidence = input.evidence;
60426
- if (helper?.ok) {
60427
- const assignment = helper.data.data.assignment;
60428
- 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))
60429
- return failure("permission_denied", "evidence is outside the worker assignment");
60430
- }
60431
60703
  if ((evidence.result === "missing" || evidence.result === "skipped") && !evidence.summary.trim())
60432
60704
  return failure("invalid_input", "missing or skipped evidence requires a reason");
60433
60705
  if (evidence.kind === "review") {
@@ -60444,6 +60716,11 @@ class WorkitCore {
60444
60716
  evidence.beforeCandidateId ??= null;
60445
60717
  evidence.candidateId ??= null;
60446
60718
  }
60719
+ if (helper?.ok) {
60720
+ const assignment = helper.data.data.assignment;
60721
+ 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))
60722
+ return failure("permission_denied", "evidence is outside the worker assignment");
60723
+ }
60447
60724
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
60448
60725
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
60449
60726
  if (candidateId && candidateId !== currentCandidate.data.id && !knownCandidates.has(candidateId))
@@ -60950,7 +61227,12 @@ class WorkitCore {
60950
61227
  return failure("invalid_transition", "worker is not awaiting dispatch");
60951
61228
  return success(mutation.revision, null, {
60952
61229
  ...current,
60953
- workers: current.workers.map((item) => item.id === input.workerId ? { ...item, recordedAt: mutation.now, provenance: attested.data } : item)
61230
+ workers: current.workers.map((item) => item.id === input.workerId ? {
61231
+ ...item,
61232
+ recordedAt: mutation.now,
61233
+ provenance: attested.data,
61234
+ data: { ...item.data, state: "dispatching" }
61235
+ } : item)
60954
61236
  });
60955
61237
  }
60956
61238
  });
@@ -61334,6 +61616,9 @@ class WorkitCore {
61334
61616
  id: task.id,
61335
61617
  revision: task.revision,
61336
61618
  workspaceRevision: workspace.data.revision,
61619
+ createdAt: task.createdAt,
61620
+ updatedAt: task.updatedAt,
61621
+ runtime: task.runtime ?? null,
61337
61622
  objective: task.intent.data.objective,
61338
61623
  status: task.status,
61339
61624
  closure: task.closure,
@@ -61419,7 +61704,7 @@ class WorkitCore {
61419
61704
  const base = reconcileResume(effectiveView);
61420
61705
  if (!base.ok)
61421
61706
  return base;
61422
- const needsReconciliation = view.task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(observedStates.get(entry.id) ?? entry.data.state));
61707
+ const needsReconciliation = view.task.workers.some((entry) => isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state));
61423
61708
  const blockers = [...base.data.blockers];
61424
61709
  if (needsReconciliation && !blockers.some((entry) => entry.reason === "worker state requires reconciliation"))
61425
61710
  blockers.push({
@@ -61486,7 +61771,8 @@ class WorkitCore {
61486
61771
  ...current,
61487
61772
  status,
61488
61773
  candidates: resumeCandidate && !current.candidates.some((candidate) => candidate.id === resumeCandidate.id) ? [...current.candidates, resumeCandidate] : current.candidates,
61489
- progress: status === "paused" ? { ...current.progress, summary: input.reason } : current.progress
61774
+ progress: current.progress,
61775
+ pauseReason: status === "paused" ? input.reason ?? null : null
61490
61776
  })
61491
61777
  });
61492
61778
  if (!changed.ok)
@@ -61522,7 +61808,7 @@ class WorkitCore {
61522
61808
  return workspace;
61523
61809
  if (!workspace.data)
61524
61810
  return failure("not_found", "workspace not found");
61525
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
61811
+ if (workspace.data.writer || task.data.workers.some((entry) => isUncertainWorker(entry.data.state)))
61526
61812
  return failure("recovery_required", "scope revision requires worker ownership reconciliation");
61527
61813
  this.fillRevisions(input, task.data, workspace.data);
61528
61814
  const changed = this.store.mutateTaskAndWorkspace({
@@ -61670,14 +61956,14 @@ function run(root, executable, args, env = {}) {
61670
61956
 
61671
61957
  // packages/workit-core/src/core/branch.ts
61672
61958
  import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "node:fs";
61673
- import { execFileSync as execFileSync3 } from "node:child_process";
61959
+ import { execFileSync as execFileSync4 } from "node:child_process";
61674
61960
  import path24 from "node:path";
61675
61961
 
61676
61962
  // packages/workit-core/src/core/git.ts
61677
- import { execFileSync as execFileSync2 } from "node:child_process";
61963
+ import { execFileSync as execFileSync3 } from "node:child_process";
61678
61964
  var run2 = (cwd, args) => {
61679
61965
  try {
61680
- const stdout = execFileSync2("git", args, {
61966
+ const stdout = execFileSync3("git", args, {
61681
61967
  cwd,
61682
61968
  encoding: "utf8",
61683
61969
  stdio: ["pipe", "pipe", "pipe"]
@@ -61940,7 +62226,7 @@ var baseBranch = (cwd) => {
61940
62226
  return { base: String(resolved.defaultTargetBranch ?? "develop") };
61941
62227
  };
61942
62228
  var originBaseReady = (cwd, base) => {
61943
- const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
62229
+ const run = (args) => execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
61944
62230
  try {
61945
62231
  try {
61946
62232
  run(["fetch", "origin", base, "--prune"]);
@@ -61948,7 +62234,7 @@ var originBaseReady = (cwd, base) => {
61948
62234
  run(["fetch", "origin", "--prune"]);
61949
62235
  }
61950
62236
  try {
61951
- execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
62237
+ execFileSync4("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
61952
62238
  cwd,
61953
62239
  stdio: "pipe"
61954
62240
  });
@@ -61967,11 +62253,11 @@ var originBaseReady = (cwd, base) => {
61967
62253
  }
61968
62254
  };
61969
62255
  var fastForwardBase = (cwd, base) => {
61970
- const run = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
62256
+ const run = (args) => execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
61971
62257
  try {
61972
62258
  let hasLocalBase = true;
61973
62259
  try {
61974
- execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
62260
+ execFileSync4("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
61975
62261
  cwd,
61976
62262
  stdio: "pipe"
61977
62263
  });
@@ -62012,10 +62298,10 @@ var branchSetup = ({
62012
62298
  log
62013
62299
  }) => {
62014
62300
  const cwd = path24.resolve(workspace_root);
62015
- const exec = (args) => execFileSync3("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
62301
+ const exec = (args) => execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
62016
62302
  const current = gitContext(cwd).branch;
62017
62303
  if (!current || current === "unknown")
62018
- return { error: "not in a git repository" };
62304
+ return { error: "not in a git repository", phase: "preflight" };
62019
62305
  const sdd = sdd_dir ?? "docs";
62020
62306
  const manifestPath = path24.isAbsolute(sdd) ? path24.join(sdd, "manifest.json") : path24.join(cwd, sdd, "manifest.json");
62021
62307
  const ensureManifestDir = () => {
@@ -62023,7 +62309,6 @@ var branchSetup = ({
62023
62309
  if (!existsSync17(dir))
62024
62310
  mkdirSync8(dir, { recursive: true, mode: 493 });
62025
62311
  };
62026
- ensureManifestDir();
62027
62312
  const readManifest = () => {
62028
62313
  try {
62029
62314
  return JSON.parse(readFileSync15(manifestPath, "utf8"));
@@ -62041,13 +62326,16 @@ var branchSetup = ({
62041
62326
  const manifest = readManifest();
62042
62327
  const ref = manifest.stash_ref;
62043
62328
  if (!ref)
62044
- return { error: "no stash_ref in manifest" };
62329
+ return { error: "no stash_ref in manifest", phase: "preflight" };
62045
62330
  journal(`pre-pop: ${String(ref)}`);
62046
62331
  try {
62047
62332
  exec(["stash", "pop", String(ref)]);
62048
62333
  } catch (error) {
62049
62334
  journal("pop: failed");
62050
- return { error: error instanceof Error ? error.message : "stash pop failed" };
62335
+ return {
62336
+ error: error instanceof Error ? error.message : "stash pop failed",
62337
+ phase: "post_mutation"
62338
+ };
62051
62339
  }
62052
62340
  journal("pop: ok");
62053
62341
  delete manifest.stash_ref;
@@ -62057,11 +62345,17 @@ var branchSetup = ({
62057
62345
  }
62058
62346
  const target = target_branch ?? "";
62059
62347
  if (!target)
62060
- return { error: "target branch required" };
62348
+ return {
62349
+ error: "target_branch (the working branch to create or switch to) is required for setup",
62350
+ phase: "preflight"
62351
+ };
62061
62352
  if (isProtected(cwd, target))
62062
- return { error: `protected branch ${target}` };
62353
+ return { error: `protected branch ${target}`, phase: "preflight" };
62063
62354
  if (!allowedBranch(cwd, target))
62064
- return { error: `target branch ${target} is not allowed by the branch policy` };
62355
+ return {
62356
+ error: `target branch ${target} is not allowed by the branch policy`,
62357
+ phase: "preflight"
62358
+ };
62065
62359
  let base;
62066
62360
  let targetExists = true;
62067
62361
  try {
@@ -62072,12 +62366,12 @@ var branchSetup = ({
62072
62366
  if (!targetExists) {
62073
62367
  const baseResolved = baseBranch(cwd);
62074
62368
  if ("error" in baseResolved)
62075
- return { error: baseResolved.error };
62369
+ return { error: baseResolved.error, phase: "preflight" };
62076
62370
  base = baseResolved.base;
62077
62371
  }
62078
62372
  journal(`entry: current=${current} target=${target} base=${base ?? "-"}`);
62079
62373
  let stash_ref;
62080
- const failAfterStash = (message) => {
62374
+ const failAfterStash = (message, phase = "post_mutation") => {
62081
62375
  let suffix = "";
62082
62376
  if (stash_ref) {
62083
62377
  journal(`pre-pop: ${stash_ref}`);
@@ -62090,27 +62384,31 @@ var branchSetup = ({
62090
62384
  suffix = " (changes preserved in stash)";
62091
62385
  }
62092
62386
  }
62093
- return { error: `${message}${suffix}` };
62387
+ return { error: `${message}${suffix}`, phase: stash_ref ? "post_mutation" : phase };
62094
62388
  };
62095
62389
  if (current !== target) {
62096
62390
  const dirty = Boolean(gitContext(cwd).status_short.trim());
62097
62391
  if (dirty && stash !== "yes") {
62098
62392
  return {
62099
- error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes"
62393
+ error: "dirty working tree — ask with native question, then call workit_branch_setup with stash=yes",
62394
+ phase: "preflight"
62100
62395
  };
62101
62396
  }
62102
62397
  let validatedBase;
62103
62398
  if (!targetExists && base !== undefined) {
62104
62399
  const ready = originBaseReady(cwd, base);
62105
62400
  if (!ready.ok)
62106
- return { error: ready.error ?? "ensure-base-branch failed" };
62401
+ return { error: ready.error ?? "ensure-base-branch failed", phase: "preflight" };
62107
62402
  validatedBase = base;
62108
62403
  }
62109
62404
  if (dirty) {
62110
62405
  try {
62111
62406
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`]);
62112
62407
  } catch (error) {
62113
- return { error: error instanceof Error ? error.message : "stash push failed" };
62408
+ return {
62409
+ error: error instanceof Error ? error.message : "stash push failed",
62410
+ phase: "post_mutation"
62411
+ };
62114
62412
  }
62115
62413
  stash_ref = "stash@{0}";
62116
62414
  journal(`stash push: ${stash_ref}`);
@@ -62121,14 +62419,14 @@ var branchSetup = ({
62121
62419
  } catch (error) {
62122
62420
  const message = error instanceof Error ? error.message : "checkout failed";
62123
62421
  if (/worktree/i.test(message)) {
62124
- return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`);
62422
+ return failAfterStash(`branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`, "preflight");
62125
62423
  }
62126
62424
  try {
62127
62425
  let effectiveBase = base;
62128
62426
  if (effectiveBase === undefined) {
62129
62427
  const lateResolved = baseBranch(cwd);
62130
62428
  if ("error" in lateResolved)
62131
- return failAfterStash(lateResolved.error);
62429
+ return failAfterStash(lateResolved.error, "preflight");
62132
62430
  effectiveBase = lateResolved.base;
62133
62431
  }
62134
62432
  const baseResult = validatedBase !== undefined ? fastForwardBase(cwd, effectiveBase) : ensureBaseBranch(cwd, effectiveBase);
@@ -63957,6 +64255,94 @@ var safePath = (root, value) => {
63957
64255
  return null;
63958
64256
  }
63959
64257
  };
64258
+ var branchCreationBaseline = (root, target, base) => {
64259
+ const exists = gitValue(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]) !== null;
64260
+ if (exists)
64261
+ return { base_branch: base, target_exists: true, remote_base: null };
64262
+ const remote = run3(root, ["ls-remote", "origin", `refs/heads/${base}`]);
64263
+ const sha = remote.exitCode === 0 ? remote.stdout.trim().split(/\s+/)[0] : "";
64264
+ return {
64265
+ base_branch: base,
64266
+ target_exists: false,
64267
+ remote_base: /^[0-9a-f]{40}$/.test(sha ?? "") ? sha : null
64268
+ };
64269
+ };
64270
+ var shortSha = (value) => typeof value === "string" && value ? value.slice(0, 8) : "unknown";
64271
+ var isProtectedBranch2 = (root, branch) => resolveBranchPolicyFor(root).protected.has(branch.toLowerCase());
64272
+ var actionProposalQuestion = (request, descriptorPayload) => {
64273
+ const payload = descriptorPayload ?? {};
64274
+ const resolved = payload.resolved ?? {};
64275
+ const payloadTarget = String(payload.target_branch ?? "");
64276
+ if (request.operation === "git.commit" && Array.isArray(payload.plan_steps)) {
64277
+ const steps = payload.plan_steps;
64278
+ return {
64279
+ presented: `Workit decision: action — Approve committing the plan's ${steps.length} listed tasks on \`${String(payload.plan_branch ?? "")}\`?`,
64280
+ approvedText: `Approve the ${steps.length} listed plan commits.`
64281
+ };
64282
+ }
64283
+ switch (request.operation) {
64284
+ case "git.branch_setup": {
64285
+ if (request.payload.action === "reapply_stash")
64286
+ return {
64287
+ presented: "Workit decision: action — Reapply the pre-checkout stash recorded in the branch manifest?",
64288
+ approvedText: "Reapply the recorded stash."
64289
+ };
64290
+ const target = String(payload.target_branch ?? "");
64291
+ if (resolved.target_exists === true)
64292
+ return {
64293
+ presented: `Workit decision: action — Switch to branch \`${target}\`?`,
64294
+ approvedText: `Switch to \`${target}\`.`
64295
+ };
64296
+ const base = String(resolved.base_branch ?? "the base branch");
64297
+ return {
64298
+ presented: `Workit decision: action — Create branch \`${target}\` from \`${base}\` (currently ${shortSha(resolved.remote_base)})?`,
64299
+ approvedText: `Create \`${target}\` from \`${base}\` at ${shortSha(resolved.remote_base)}.`
64300
+ };
64301
+ }
64302
+ case "git.commit": {
64303
+ const paths = Array.isArray(resolved.paths) ? resolved.paths : [];
64304
+ return {
64305
+ presented: `Workit decision: action — Commit ${paths.length} staged file(s) as "${String(request.payload.message)}"?`,
64306
+ approvedText: `Commit as "${String(request.payload.message)}".`
64307
+ };
64308
+ }
64309
+ case "git.push":
64310
+ return {
64311
+ presented: `Workit decision: action — Push branch \`${String(payload.branch ?? "")}\` to origin?`,
64312
+ approvedText: `Push \`${String(payload.branch ?? "")}\` to origin.`
64313
+ };
64314
+ case "hosting.pull_request":
64315
+ return {
64316
+ presented: `Workit decision: action — Open a PR from \`${String(resolved.source_branch ?? "")}\` to \`${payloadTarget}\` titled "${String(request.payload.title)}"?`,
64317
+ approvedText: `Open the PR: ${String(resolved.source_branch ?? "")} → ${payloadTarget}, "${String(request.payload.title)}".`
64318
+ };
64319
+ case "changelog.apply":
64320
+ return {
64321
+ presented: `Workit decision: action — Apply changelog changes at \`${String(payload.path ?? "CHANGELOG.md")}\`?`,
64322
+ approvedText: `Apply changelog changes at ${String(payload.path ?? "CHANGELOG.md")}.`
64323
+ };
64324
+ case "youtrack.update":
64325
+ return {
64326
+ presented: `Workit decision: action — Update YouTrack issue ${String(request.payload.issueId)}${request.payload.minutes ? ` and log ${String(request.payload.minutes)}m` : ""}?`,
64327
+ approvedText: `Update YouTrack issue ${String(request.payload.issueId)}.`
64328
+ };
64329
+ case "youtrack.time":
64330
+ return {
64331
+ presented: `Workit decision: action — Log ${String(request.payload.minutes)}m to YouTrack issue ${String(request.payload.issueId)}?`,
64332
+ approvedText: `Log time to YouTrack issue ${String(request.payload.issueId)}.`
64333
+ };
64334
+ case "youtrack.meeting":
64335
+ return {
64336
+ presented: `Workit decision: action — Log meeting time to YouTrack issue ${String(request.payload.issueId)}?`,
64337
+ approvedText: `Log meeting time to YouTrack issue ${String(request.payload.issueId)}.`
64338
+ };
64339
+ default:
64340
+ return {
64341
+ presented: `Workit decision: action — Approve ${request.operation}?`,
64342
+ approvedText: `Approve ${request.operation}.`
64343
+ };
64344
+ }
64345
+ };
63960
64346
  var approvedResolvedExternalAction = (content) => {
63961
64347
  try {
63962
64348
  const value = JSON.parse(content);
@@ -64220,24 +64606,35 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
64220
64606
  const sdd_dir = safePath(root, request.payload.sdd_dir ?? "docs");
64221
64607
  if (!sdd_dir)
64222
64608
  return failure("invalid_input", "sdd_dir must stay inside the workspace");
64609
+ const action = request.payload.action ?? "setup";
64223
64610
  const policy = resolveBranchPolicyFor(root);
64224
- const target_branch = request.payload.target_branch ?? policy.defaultTargetBranch;
64611
+ const target_branch = request.payload.target_branch;
64612
+ if (action !== "reapply_stash" && !target_branch)
64613
+ return failure("invalid_input", "target_branch (the working branch to create or switch to) is required for setup", {
64614
+ outcome: "not_started",
64615
+ fields: [{ path: "target_branch", reason: "required for action=setup" }]
64616
+ });
64225
64617
  const head = gitValue(root, ["rev-parse", "HEAD"]);
64226
64618
  if (!head)
64227
64619
  return failure("storage_error", "current Git commit could not be resolved");
64620
+ const dirty = Boolean((gitRaw(root, ["status", "--short"]) ?? "").trim());
64621
+ const creation = action === "reapply_stash" || !target_branch ? { base_branch: policy.defaultTargetBranch } : branchCreationBaseline(root, target_branch, policy.defaultTargetBranch);
64228
64622
  const normalized = {
64229
64623
  operation: request.operation,
64230
64624
  payload: {
64231
64625
  ...request.payload,
64232
- action: request.payload.action ?? "setup",
64626
+ action,
64233
64627
  sdd_dir,
64234
64628
  stash: request.payload.stash ?? "no",
64235
- target_branch
64629
+ ...target_branch ? { target_branch } : {}
64236
64630
  }
64237
64631
  };
64238
64632
  return success(null, null, {
64239
64633
  request: normalized,
64240
- descriptorPayload: { ...normalized.payload, resolved: { head, target_branch } }
64634
+ descriptorPayload: {
64635
+ ...normalized.payload,
64636
+ resolved: { head, dirty, ...creation }
64637
+ }
64241
64638
  });
64242
64639
  }
64243
64640
  case "git.push": {
@@ -64251,7 +64648,37 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
64251
64648
  }) : failure("invalid_input", "git push requires a resolvable current branch");
64252
64649
  }
64253
64650
  case "git.commit": {
64651
+ const planSteps = request.payload.plan_steps;
64652
+ const planBranch = request.payload.plan_branch;
64254
64653
  const head = gitValue(root, ["rev-parse", "HEAD"]);
64654
+ if (!head)
64655
+ return failure("storage_error", "Git state could not be resolved");
64656
+ const branch = gitValue(root, ["branch", "--show-current"]);
64657
+ if (planSteps !== undefined || planBranch !== undefined) {
64658
+ if (!Array.isArray(planSteps) || planSteps.some((step) => typeof step !== "string" || !step.trim()) || new Set(planSteps).size !== planSteps.length || planSteps.length === 0 || planSteps.length > 32)
64659
+ return failure("invalid_input", "plan_steps must be 1-32 unique non-empty commit messages", {
64660
+ outcome: "not_started",
64661
+ fields: [{ path: "plan_steps", reason: "unique strings" }]
64662
+ });
64663
+ if (typeof planBranch !== "string" || !planBranch.trim() || planBranch !== branch)
64664
+ return failure("invalid_input", "plan_branch must name the current working branch", {
64665
+ outcome: "not_started",
64666
+ fields: [{ path: "plan_branch", reason: "current branch" }]
64667
+ });
64668
+ if (isProtectedBranch2(root, planBranch))
64669
+ return failure("invalid_input", "plan_branch must not be a protected branch", {
64670
+ outcome: "not_started",
64671
+ fields: [{ path: "plan_branch", reason: "protected branch" }]
64672
+ });
64673
+ return success(null, null, {
64674
+ request,
64675
+ descriptorPayload: {
64676
+ plan_steps: planSteps,
64677
+ plan_branch: planBranch,
64678
+ resolved: { head, branch, steps: planSteps }
64679
+ }
64680
+ });
64681
+ }
64255
64682
  const staged = gitRaw(root, [
64256
64683
  "diff",
64257
64684
  "--cached",
@@ -64262,7 +64689,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
64262
64689
  "HEAD"
64263
64690
  ]);
64264
64691
  const paths = stagedPaths(root);
64265
- if (!head || staged === null || paths === null)
64692
+ const message = request.payload.message;
64693
+ if (typeof message !== "string" || !message)
64694
+ return failure("invalid_input", "git.commit requires a commit message", {
64695
+ outcome: "not_started",
64696
+ fields: [{ path: "message", reason: "required unless plan_steps is provided" }]
64697
+ });
64698
+ if (staged === null || paths === null)
64266
64699
  return failure("storage_error", "Git state could not be resolved");
64267
64700
  let policy;
64268
64701
  try {
@@ -64277,13 +64710,13 @@ var resolveExternalActionRequest = (root, request, preservedDateMs) => {
64277
64710
  `).filter(Boolean));
64278
64711
  flavor = detected.flavor ?? "conventional";
64279
64712
  }
64280
- if (!matchCommitFlavor(request.payload.message, flavor, policy.pattern))
64713
+ if (!matchCommitFlavor(message, flavor, policy.pattern))
64281
64714
  return failure("invalid_input", `Commit message does not match the ${flavor} flavor (commitPolicy.preset)`);
64282
64715
  return success(null, null, {
64283
64716
  request,
64284
64717
  descriptorPayload: {
64285
64718
  ...request.payload,
64286
- resolved: { head, staged: sha256(staged), paths }
64719
+ resolved: { head, branch, staged: sha256(staged), paths }
64287
64720
  }
64288
64721
  });
64289
64722
  }
@@ -64453,10 +64886,24 @@ var executeConcreteExternalAction = async (request, root, marker, dateMs, step,
64453
64886
  sdd_dir: request.payload.sdd_dir ?? "docs",
64454
64887
  workspace_root: root
64455
64888
  });
64456
- return result.error ? unknown2(request.operation) : success(null, null, result);
64889
+ if ("error" in result) {
64890
+ if (result.phase === "preflight")
64891
+ return failure("invalid_input", result.error, {
64892
+ outcome: "not_started",
64893
+ operation: request.operation
64894
+ });
64895
+ return unknown2(request.operation);
64896
+ }
64897
+ return success(null, null, result);
64457
64898
  }
64458
64899
  case "git.commit": {
64459
- const result = run3(root, ["commit", "-m", request.payload.message]);
64900
+ const message = request.payload.message;
64901
+ if (typeof message !== "string" || !message)
64902
+ return failure("invalid_input", "git.commit requires a commit message", {
64903
+ outcome: "not_started",
64904
+ fields: [{ path: "message", reason: "required" }]
64905
+ });
64906
+ const result = run3(root, ["commit", "-m", message]);
64460
64907
  return result.exitCode === 0 ? success(null, null, { stdout: result.stdout.trim() }) : unknown2(request.operation);
64461
64908
  }
64462
64909
  case "git.push": {
@@ -64862,7 +65309,29 @@ async function runTaskCommand(argv, deps = {}) {
64862
65309
  }
64863
65310
  const core = new WorkitCore(new TaskStore(root), contextFor(root, parsed.parsed.actor !== undefined ? { ...deps, actor: parsed.parsed.actor } : deps, observedConfirmation ? "host_observed" : "agent_reported"));
64864
65311
  let result;
64865
- if (parsed.parsed.handoff) {
65312
+ const decisionBudgetIssue = (() => {
65313
+ if (parsed.parsed.family !== "decision" || parsed.parsed.action !== "record")
65314
+ return null;
65315
+ const request = parsed.parsed.request;
65316
+ if (typeof request.purpose !== "string" || !request.binding)
65317
+ return null;
65318
+ return workitBindingQuestionIssue([
65319
+ {
65320
+ header: `Workit decision: ${request.purpose}`,
65321
+ question: typeof request.binding.presented === "string" ? request.binding.presented : "",
65322
+ options: [
65323
+ {
65324
+ label: "approved",
65325
+ description: typeof request.binding.displayed === "string" ? request.binding.displayed : typeof request.binding.approvedContent === "string" ? request.binding.approvedContent : ""
65326
+ },
65327
+ { label: "rejected", description: "Reject this decision" }
65328
+ ]
65329
+ }
65330
+ ]);
65331
+ })();
65332
+ if (decisionBudgetIssue)
65333
+ result = failure("invalid_input", decisionBudgetIssue);
65334
+ else if (parsed.parsed.handoff) {
64866
65335
  const exported = dispatch(core, "state", parsed.parsed.request);
64867
65336
  if (!exported.ok)
64868
65337
  result = exported;
@@ -64926,8 +65395,34 @@ var cliActionAuthority = (actor, reconciliationTokens = new WeakSet) => ({
64926
65395
  var nativeExternalActionRunner = (root, actor, core, step) => createAuthorizedExternalActionRunner(core, (operationValue) => {
64927
65396
  const store = new TaskStore(root);
64928
65397
  const approved = approvedExternalAction(store, "workit_cli", actor, operationValue);
64929
- if (!approved.ok)
65398
+ if (!approved.ok) {
65399
+ const plan = planCommitBinding(store, "workit_cli", actor, operationValue);
65400
+ if (plan) {
65401
+ const actionRef = externalActionRef("workit_cli", actor, operationValue);
65402
+ return {
65403
+ taskId: plan.taskId,
65404
+ decisionId: plan.decisionId,
65405
+ actionRef,
65406
+ expectedRevision: plan.expectedRevision,
65407
+ expectedWorkspaceRevision: plan.expectedWorkspaceRevision,
65408
+ binding: plan.binding,
65409
+ step: plan.step,
65410
+ refresh: () => {
65411
+ const task = store.readTask(plan.taskId);
65412
+ const freshWorkspace = store.readWorkspace();
65413
+ if (!task.ok || !freshWorkspace.ok || !freshWorkspace.data)
65414
+ throw new Error("external action state changed");
65415
+ return {
65416
+ expectedRevision: task.data.revision,
65417
+ expectedWorkspaceRevision: freshWorkspace.data.revision
65418
+ };
65419
+ },
65420
+ reserveObservation: nativeExternalActionObservation(actor, actionRef, "reserve"),
65421
+ settleObservation: (outcome, revisions) => nativeExternalActionObservation(actor, actionRef, outcome, actionRef.kind === "host" ? actionRef.handle : "external-action", revisions)
65422
+ };
65423
+ }
64930
65424
  return approved;
65425
+ }
64931
65426
  const actionRef = externalActionRef("workit_cli", actor, operationValue);
64932
65427
  return {
64933
65428
  taskId: approved.data.task.id,
@@ -65124,8 +65619,56 @@ async function runActionCommand(argv, deps = {}) {
65124
65619
  printHuman(result, deps);
65125
65620
  return 1;
65126
65621
  }
65622
+ const drift = normalized.operation === "git.branch_setup" ? priorResolvedDrift(store, "workit_cli", actor, normalized.operation, normalized.payload, resolved.data.descriptorPayload.resolved) : success(null, null, null);
65623
+ if (!drift.ok) {
65624
+ if (json)
65625
+ jsonResult(outOf(deps), drift);
65626
+ else
65627
+ printHuman(drift, deps);
65628
+ return 1;
65629
+ }
65630
+ const localOperation = [
65631
+ "git.branch_setup",
65632
+ "git.commit",
65633
+ "git.push",
65634
+ "hosting.pull_request",
65635
+ "changelog.apply"
65636
+ ].includes(normalized.operation);
65637
+ if (localOperation) {
65638
+ const writer = assertLocalExternalActionWriter(root, { host: "workit_cli", actor });
65639
+ if (!writer.ok) {
65640
+ const result = failure("needs_input", "writer ownership is required before this action", {
65641
+ outcome: "not_started",
65642
+ operation: normalized.operation,
65643
+ guidance: "Acquire checkout writer ownership first (writer.acquire) and retry the action."
65644
+ });
65645
+ if (json)
65646
+ jsonResult(outOf(deps), result);
65647
+ else
65648
+ printHuman(result, deps);
65649
+ return 1;
65650
+ }
65651
+ }
65652
+ const question = actionProposalQuestion(normalized, resolved.data.descriptorPayload);
65653
+ const commitMessage = normalized.operation === "git.commit" ? normalized.payload.message : undefined;
65654
+ const currentBranch = resolved.data.descriptorPayload.resolved?.branch;
65655
+ const plan = typeof commitMessage === "string" && commitMessage ? approvedPlanCommit(store, "workit_cli", actor, commitMessage) : null;
65656
+ if (plan?.ok && plan.data.branch === currentBranch) {
65657
+ const core = new WorkitCore(store, {
65658
+ ...contextFor(root, { ...deps, actor }, "host_observed"),
65659
+ nativeAuthority: cliActionAuthority(actor),
65660
+ workerId: null
65661
+ });
65662
+ const runner = nativeExternalActionRunner(root, actor, core);
65663
+ const result = await runner(descriptor, (step) => executeResolvedExternalAction(resolved.data, root, step, { host: "workit_cli", actor }));
65664
+ if (json)
65665
+ jsonResult(outOf(deps), result);
65666
+ else
65667
+ printHuman(result, deps);
65668
+ return result.ok ? 0 : 1;
65669
+ }
65127
65670
  if (!json)
65128
- write2(outOf(deps), `External action preview: ${descriptor}`);
65671
+ write2(outOf(deps), question.presented);
65129
65672
  const accepted = await observeConsent(deps);
65130
65673
  if (!accepted.accepted || !accepted.observed) {
65131
65674
  const result = failure("permission_denied", "external action was not confirmed by the terminal");
@@ -65151,8 +65694,9 @@ async function runActionCommand(argv, deps = {}) {
65151
65694
  taskId: task.id,
65152
65695
  workspaceId: workspace.data.id,
65153
65696
  scope: task.intent.data.scope,
65154
- presented: `Approve ${descriptor}`,
65697
+ presented: question.presented,
65155
65698
  approvedContent: descriptor,
65699
+ displayed: question.approvedText,
65156
65700
  contentRefs: []
65157
65701
  },
65158
65702
  response: "approved",
@@ -65169,6 +65713,15 @@ async function runActionCommand(argv, deps = {}) {
65169
65713
  printHuman(decision, deps);
65170
65714
  return 1;
65171
65715
  }
65716
+ const planSteps = resolved.data.descriptorPayload.plan_steps;
65717
+ if (normalized.operation === "git.commit" && Array.isArray(planSteps)) {
65718
+ const result = success(null, null, { plan_commits: planSteps.length });
65719
+ if (json)
65720
+ jsonResult(outOf(deps), result);
65721
+ else
65722
+ printHuman(result, deps);
65723
+ return 0;
65724
+ }
65172
65725
  const runner = nativeExternalActionRunner(root, actor, core);
65173
65726
  const result = await runner(descriptor, (step) => executeResolvedExternalAction(resolved.data, root, step, { host: "workit_cli", actor }));
65174
65727
  if (json)