@brainervirus/workit-core 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.
@@ -58,6 +58,7 @@ import {
58
58
  } from "./task-context";
59
59
  import {
60
60
  assertProductWriteAllowed,
61
+ isUncertainWorker,
61
62
  type CallerContext,
62
63
  type NativeWorkerObservation,
63
64
  type NativeWorkerVerification,
@@ -360,11 +361,16 @@ const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<
360
361
  return failure("invalid_transition", "worker has not started");
361
362
  if (input.state === "running" && entry.data.state === "stopped")
362
363
  return failure("invalid_transition", "stopped worker cannot run again");
364
+ const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
363
365
  const nextEntry = {
364
366
  ...entry,
365
367
  recordedAt: input.now ?? entry.recordedAt,
366
368
  provenance: authority.provenance,
367
- data: { ...entry.data, state: input.state, session: input.session },
369
+ data: {
370
+ ...entry.data,
371
+ state: terminalCancel ? ("cancelling" as const) : input.state,
372
+ session: input.session,
373
+ },
368
374
  } satisfies Entry<Worker>;
369
375
  const shouldClear =
370
376
  input.state === "stopped" &&
@@ -381,7 +387,7 @@ const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<
381
387
  // revision bump per event livelocks worker sessions — each call would
382
388
  // invalidate the revision the previous call returned.
383
389
  if (
384
- entry.data.state === input.state &&
390
+ entry.data.state === nextEntry.data.state &&
385
391
  sameValue(entry.data.session, input.session) &&
386
392
  !shouldClear &&
387
393
  !shouldUncertain
@@ -823,11 +829,7 @@ export class WorkitCore {
823
829
  ) {
824
830
  if (workspace.writer)
825
831
  return failure("recovery_required", "writer ownership must be released first");
826
- if (
827
- task.workers.some((entry) =>
828
- ["dispatching", "running", "cancelling", "unknown"].includes(entry.data.state),
829
- )
830
- )
832
+ if (task.workers.some((entry) => isUncertainWorker(entry.data.state)))
831
833
  return failure("recovery_required", "worker state requires reconciliation");
832
834
  return null;
833
835
  }
@@ -1063,18 +1065,6 @@ export class WorkitCore {
1063
1065
  (this.context.workerId ?? null) === null ? null : this.helperEntry(task.data, true, true);
1064
1066
  if (helper && !helper.ok) return helper as Result<never>;
1065
1067
  const evidence = input.evidence as Evidence;
1066
- if (helper?.ok) {
1067
- const assignment = helper.data.data.assignment;
1068
- if (
1069
- evidence.requirementIds.some((id: string) => !assignment.requirementIds.includes(id)) ||
1070
- (assignment.candidateId !== null &&
1071
- evidence.candidateId !== assignment.candidateId &&
1072
- evidence.beforeCandidateId !== assignment.candidateId) ||
1073
- !refsWithinScope(evidence.refs, assignment.scope) ||
1074
- !refsWithinScope(evidence.refs, task.data.intent.data.scope)
1075
- )
1076
- return failure("permission_denied", "evidence is outside the worker assignment");
1077
- }
1078
1068
  if (
1079
1069
  (evidence.result === "missing" || evidence.result === "skipped") &&
1080
1070
  !evidence.summary.trim()
@@ -1100,6 +1090,18 @@ export class WorkitCore {
1100
1090
  evidence.beforeCandidateId ??= null;
1101
1091
  evidence.candidateId ??= null;
1102
1092
  }
1093
+ if (helper?.ok) {
1094
+ const assignment = helper.data.data.assignment;
1095
+ if (
1096
+ evidence.requirementIds.some((id: string) => !assignment.requirementIds.includes(id)) ||
1097
+ (assignment.candidateId !== null &&
1098
+ evidence.candidateId !== assignment.candidateId &&
1099
+ evidence.beforeCandidateId !== assignment.candidateId) ||
1100
+ !refsWithinScope(evidence.refs, assignment.scope) ||
1101
+ !refsWithinScope(evidence.refs, task.data.intent.data.scope)
1102
+ )
1103
+ return failure("permission_denied", "evidence is outside the worker assignment");
1104
+ }
1103
1105
  const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
1104
1106
  for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
1105
1107
  if (
@@ -1187,7 +1189,15 @@ export class WorkitCore {
1187
1189
  "permission_denied",
1188
1190
  "native observation is only valid for recording decisions",
1189
1191
  );
1190
- if (nativeRequired && nativeObservation === undefined)
1192
+ // Stated choices carry user-settled decisions without a native receipt.
1193
+ // They never authorize mutating actions; native verification is skipped
1194
+ // and provenance stays agent-reported.
1195
+ const stated = input.action === "record" && input.response === "stated";
1196
+ if (stated && input.purpose === "action")
1197
+ return failure("permission_denied", "stated choices cannot authorize mutating actions");
1198
+ if (stated && !input.binding?.statedChoice)
1199
+ return failure("invalid_input", "stated choices require binding.statedChoice");
1200
+ if (nativeRequired && nativeObservation === undefined && !stated)
1191
1201
  return failure("permission_denied", "native decision observation is required");
1192
1202
  const task = this.store.readTask(input.taskId);
1193
1203
  if (!task.ok) return task as Result<never>;
@@ -1218,26 +1228,27 @@ export class WorkitCore {
1218
1228
  consumption: null,
1219
1229
  } satisfies Omit<Decision, "digest">;
1220
1230
  const data: Decision = { ...base, digest: decisionDigest(base) };
1221
- const native = nativeRequired
1222
- ? verifyNativeDecision(
1223
- this.context.nativeAuthority,
1224
- {
1225
- observation: nativeObservation,
1226
- expected: {
1227
- taskId: task.data.id,
1228
- workspaceId: workspace.data.id,
1229
- purpose: input.purpose,
1230
- response: input.response,
1231
- binding: input.binding,
1232
- bindingBytes: canonicalJson(input.binding),
1233
- digest: data.digest,
1234
- requirementIds: [...input.requirementIds],
1231
+ const native =
1232
+ nativeRequired && !stated
1233
+ ? verifyNativeDecision(
1234
+ this.context.nativeAuthority,
1235
+ {
1236
+ observation: nativeObservation,
1237
+ expected: {
1238
+ taskId: task.data.id,
1239
+ workspaceId: workspace.data.id,
1240
+ purpose: input.purpose,
1241
+ response: input.response,
1242
+ binding: input.binding,
1243
+ bindingBytes: canonicalJson(input.binding),
1244
+ digest: data.digest,
1245
+ requirementIds: [...input.requirementIds],
1246
+ },
1247
+ caller: this.context.caller,
1235
1248
  },
1236
- caller: this.context.caller,
1237
- },
1238
- { owner: this.authorityOwner, store: this.store, root: this.store.root },
1239
- )
1240
- : null;
1249
+ { owner: this.authorityOwner, store: this.store, root: this.store.root },
1250
+ )
1251
+ : null;
1241
1252
  if (native && !native.ok) return native as Result<never>;
1242
1253
  const nativeProvenance = native?.ok ? retireNativeAuthority(native.data) : null;
1243
1254
  if (native?.ok && !nativeProvenance)
@@ -2242,6 +2253,9 @@ export class WorkitCore {
2242
2253
  id: task.id,
2243
2254
  revision: task.revision,
2244
2255
  workspaceRevision: workspace.data.revision,
2256
+ createdAt: task.createdAt,
2257
+ updatedAt: task.updatedAt,
2258
+ runtime: task.runtime ?? null,
2245
2259
  objective: task.intent.data.objective,
2246
2260
  status: task.status,
2247
2261
  closure: task.closure,
@@ -2344,9 +2358,7 @@ export class WorkitCore {
2344
2358
  const base = reconcileResumeContext(effectiveView);
2345
2359
  if (!base.ok) return base;
2346
2360
  const needsReconciliation = view.task.workers.some((entry) =>
2347
- ["dispatching", "running", "cancelling", "unknown"].includes(
2348
- observedStates.get(entry.id) ?? entry.data.state,
2349
- ),
2361
+ isUncertainWorker(observedStates.get(entry.id) ?? entry.data.state),
2350
2362
  );
2351
2363
  const blockers = [...base.data.blockers];
2352
2364
  if (
@@ -2438,8 +2450,8 @@ export class WorkitCore {
2438
2450
  !current.candidates.some((candidate) => candidate.id === resumeCandidate!.id)
2439
2451
  ? [...current.candidates, resumeCandidate]
2440
2452
  : current.candidates,
2441
- progress:
2442
- status === "paused" ? { ...current.progress, summary: input.reason } : current.progress,
2453
+ progress: current.progress,
2454
+ pauseReason: status === "paused" ? (input.reason ?? null) : null,
2443
2455
  }),
2444
2456
  });
2445
2457
  if (!changed.ok) return changed as Result<never>;
@@ -2475,7 +2487,10 @@ export class WorkitCore {
2475
2487
  const workspace = this.store.readWorkspace();
2476
2488
  if (!workspace.ok) return workspace as Result<never>;
2477
2489
  if (!workspace.data) return failure("not_found", "workspace not found");
2478
- if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
2490
+ if (
2491
+ workspace.data.writer ||
2492
+ task.data.workers.some((entry) => isUncertainWorker(entry.data.state))
2493
+ )
2479
2494
  return failure(
2480
2495
  "recovery_required",
2481
2496
  "scope revision requires worker ownership reconciliation",
@@ -154,14 +154,23 @@ const scopeRoots = (root: string, scope: Scope): Result<string[]> => {
154
154
  const roots: string[] = [];
155
155
  for (const item of scope.paths.length ? scope.paths : ["."]) {
156
156
  const target = path.resolve(root, item);
157
- if (!inside(root, target)) return failure("invalid_input", "candidate scope escapes checkout");
157
+ if (!inside(root, target))
158
+ return failure(
159
+ "invalid_input",
160
+ `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`,
161
+ { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] },
162
+ );
158
163
  let ancestor = target;
159
164
  while (!fs.existsSync(ancestor) && ancestor !== root) ancestor = path.dirname(ancestor);
160
165
  try {
161
166
  if (!inside(root, fs.realpathSync(ancestor)))
162
- return failure("invalid_input", "candidate scope escapes checkout");
167
+ return failure(
168
+ "invalid_input",
169
+ `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`,
170
+ { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] },
171
+ );
163
172
  } catch {
164
- return failure("invalid_input", "candidate scope cannot be inspected");
173
+ return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
165
174
  }
166
175
  roots.push(target);
167
176
  }
@@ -180,7 +189,12 @@ export function captureCandidate(
180
189
  return failure("invalid_input", "candidate root does not exist");
181
190
  }
182
191
  const normalizedScope = canonicalScope(scope);
183
- if (!normalizedScope) return failure("invalid_input", "candidate scope is invalid");
192
+ if (!normalizedScope)
193
+ return failure(
194
+ "invalid_input",
195
+ "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",
196
+ { fields: [{ path: "scope.paths", reason: "must be checkout-relative" }] },
197
+ );
184
198
  const roots = scopeRoots(checkout, normalizedScope);
185
199
  if (!roots.ok) return roots;
186
200
  const git = gitPaths(checkout);
@@ -399,7 +413,7 @@ const applicableRequirementDecision = (
399
413
  ({ data, provenance }) =>
400
414
  provenance.kind !== "imported" &&
401
415
  data.purpose !== "limitation" &&
402
- data.response === "approved" &&
416
+ (data.response === "approved" || data.response === "stated") &&
403
417
  data.revoked === null &&
404
418
  data.binding.taskId === task.id &&
405
419
  data.binding.workspaceId === workspace.id &&
@@ -461,7 +475,10 @@ export function evaluateRequirements(
461
475
  const reviewSession = sessionFromRef(entry.data.reviewContext);
462
476
  const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
463
477
  const sameEvidenceSession = task.evidence.some(
464
- (other) => other.id !== entry.id && sameSession(reviewSession, other.provenance.session),
478
+ (other) =>
479
+ other.id !== entry.id &&
480
+ other.data.kind === "review" &&
481
+ sameSession(reviewSession, other.provenance.session),
465
482
  );
466
483
  return (
467
484
  entry.data.kind === "review" &&
@@ -536,7 +553,7 @@ export function evaluateRequirements(
536
553
  status: "satisfied" as const,
537
554
  evidenceIds: [],
538
555
  decisionIds: decisions.map((decision) => decision.id),
539
- reason: "an applicable approved decision satisfies the requirement",
556
+ reason: "an applicable approved or stated decision satisfies the requirement",
540
557
  };
541
558
  const limitations = requirement.acceptanceAllowed
542
559
  ? applicableDecision(task, workspace, requirement, checkoutRoot)
@@ -641,6 +658,18 @@ export function evaluateClosure(
641
658
  if (requestedOutcome !== "stopped" && blocking.length)
642
659
  return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
643
660
  requirementIds: blocking.map((item) => item.requirementId),
661
+ requirements: blocking.map((item) => {
662
+ const requirement = view.task.policy?.requirements.find(
663
+ (candidate) => candidate.id === item.requirementId,
664
+ );
665
+ return {
666
+ requirementId: item.requirementId,
667
+ ruleId: requirement?.ruleId ?? "unknown",
668
+ reason: item.reason,
669
+ satisfaction: requirement?.satisfaction ?? "",
670
+ dependentAction: requirement?.dependentAction ?? null,
671
+ };
672
+ }),
644
673
  });
645
674
  const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
646
675
  if (requestedOutcome === "verified" && accepted.length)
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import { hostname } from "node:os";
4
4
  import path from "node:path";
5
5
  import * as z from "zod";
6
+ import { packageRoot } from "./package-root";
6
7
  import {
7
8
  acquireFileLockSync,
8
9
  type FileLockSyncHandle,
@@ -109,6 +110,43 @@ export type RecoveryCandidate = {
109
110
  };
110
111
 
111
112
  const now = (): Utc => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
113
+
114
+ let cachedRuntimeVersion: string | null = null;
115
+ /** Version of the package that hosts this runtime; recorded with state so a
116
+ * bug report can identify the writer version without reading raw .workit. */
117
+ export const runtimeVersion = (): string => {
118
+ if (cachedRuntimeVersion === null) {
119
+ try {
120
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot(), "package.json"), "utf8")) as {
121
+ version?: unknown;
122
+ };
123
+ cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
124
+ } catch {
125
+ cachedRuntimeVersion = "0.0.0";
126
+ }
127
+ }
128
+ return cachedRuntimeVersion;
129
+ };
130
+
131
+ const stampNew = (record: { runtime?: { createdWith: string | null } }) => ({
132
+ createdWith: record.runtime?.createdWith ?? null,
133
+ updatedWith: runtimeVersion(),
134
+ });
135
+
136
+ const isNewerVersion = (candidate: string, current: string): boolean => {
137
+ const parts = (value: string) =>
138
+ value
139
+ .split("-")[0]
140
+ .split(".")
141
+ .map((item) => Number.parseInt(item, 10) || 0);
142
+ const [next, now] = [parts(candidate), parts(current)];
143
+ for (let index = 0; index < 3; index += 1) {
144
+ const left = next[index] ?? 0;
145
+ const right = now[index] ?? 0;
146
+ if (left !== right) return left > right;
147
+ }
148
+ return false;
149
+ };
112
150
  const jsonBytes = (value: unknown): string => `${canonicalJson(value)}\n`;
113
151
  const digestBytes = (value: string | Buffer): string =>
114
152
  createHash("sha256").update(value).digest("hex");
@@ -221,10 +259,14 @@ export class TaskStore {
221
259
  ? value.expectedWorkspaceRevision !== current.data.revision
222
260
  : value.expectedWorkspaceRevision !== null
223
261
  ) {
224
- return failure("revision_conflict", "workspace revision does not match", {
225
- expectedWorkspaceRevision: value.expectedWorkspaceRevision,
226
- actualWorkspaceRevision: current.data?.revision ?? null,
227
- });
262
+ return failure(
263
+ "revision_conflict",
264
+ "workspace revision does not match; omit expectedWorkspaceRevision to use the current record",
265
+ {
266
+ expectedWorkspaceRevision: value.expectedWorkspaceRevision,
267
+ actualWorkspaceRevision: current.data?.revision ?? null,
268
+ },
269
+ );
228
270
  }
229
271
  if (!value.provenance) return failure("invalid_input", "provenance is required");
230
272
  if (
@@ -236,12 +278,18 @@ export class TaskStore {
236
278
  if (current.data && !previousWorkspaceBytes)
237
279
  return failure("storage_error", "workspace snapshot disappeared during creation");
238
280
  const workspace: WorkspaceRecord = current.data
239
- ? { ...current.data, revision: newRevision(), root: this.root }
281
+ ? {
282
+ ...current.data,
283
+ revision: newRevision(),
284
+ root: this.root,
285
+ runtime: stampNew(current.data),
286
+ }
240
287
  : {
241
288
  schemaVersion: SCHEMA_VERSION,
242
289
  id: newId(),
243
290
  revision: newRevision(),
244
291
  root: this.root,
292
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
245
293
  writer: null,
246
294
  };
247
295
  const timestamp = value.now ?? now();
@@ -263,6 +311,7 @@ export class TaskStore {
263
311
  status: "active",
264
312
  closure: null,
265
313
  progress: { summary: "", nextAction: null, blockers: [] },
314
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
266
315
  assessments: [],
267
316
  policy: null,
268
317
  policyChanges: [],
@@ -312,21 +361,31 @@ export class TaskStore {
312
361
  ? input.expectedWorkspaceRevision !== current.data.revision
313
362
  : input.expectedWorkspaceRevision !== null
314
363
  )
315
- return failure("revision_conflict", "workspace revision does not match", {
316
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
317
- actualWorkspaceRevision: current.data?.revision ?? null,
318
- });
364
+ return failure(
365
+ "revision_conflict",
366
+ "workspace revision does not match; omit expectedWorkspaceRevision to use the current record",
367
+ {
368
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
369
+ actualWorkspaceRevision: current.data?.revision ?? null,
370
+ },
371
+ );
319
372
  const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
320
373
  if (current.data && !previousWorkspaceBytes)
321
374
  return failure("storage_error", "workspace snapshot disappeared during import");
322
375
  const timestamp = input.now ?? now();
323
376
  const workspace: WorkspaceRecord = current.data
324
- ? { ...current.data, revision: newRevision(), root: this.root }
377
+ ? {
378
+ ...current.data,
379
+ revision: newRevision(),
380
+ root: this.root,
381
+ runtime: stampNew(current.data),
382
+ }
325
383
  : {
326
384
  schemaVersion: SCHEMA_VERSION,
327
385
  id: input.workspaceId ?? newId(),
328
386
  revision: newRevision(),
329
387
  root: this.root,
388
+ runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
330
389
  writer: null,
331
390
  };
332
391
  const task = {
@@ -335,6 +394,10 @@ export class TaskStore {
335
394
  revision: newRevision(),
336
395
  createdAt: timestamp,
337
396
  updatedAt: timestamp,
397
+ runtime: {
398
+ createdWith: input.task.runtime?.createdWith ?? null,
399
+ updatedWith: runtimeVersion(),
400
+ },
338
401
  };
339
402
  const validTask = taskRecordSchema.safeParse(task);
340
403
  if (!validTask.success)
@@ -402,6 +465,7 @@ export class TaskStore {
402
465
  createdAt: current.data.createdAt,
403
466
  revision: context.revision,
404
467
  updatedAt: context.now,
468
+ runtime: stampNew(current.data),
405
469
  };
406
470
  const valid = taskRecordSchema.safeParse(record);
407
471
  if (!valid.success)
@@ -433,6 +497,7 @@ export class TaskStore {
433
497
  id: current.data.id,
434
498
  root: this.root,
435
499
  revision: context.revision,
500
+ runtime: stampNew(current.data),
436
501
  };
437
502
  const valid = workspaceRecordSchema.safeParse(record);
438
503
  if (!valid.success)
@@ -472,6 +537,7 @@ export class TaskStore {
472
537
  id: workspace.data.id,
473
538
  root: this.root,
474
539
  revision: workspaceContext.revision,
540
+ runtime: stampNew(workspace.data),
475
541
  });
476
542
  if (!nextWorkspace.success)
477
543
  return failure("invalid_input", "workspace mutation produced an invalid record");
@@ -508,6 +574,7 @@ export class TaskStore {
508
574
  createdAt: task.data.createdAt,
509
575
  revision: taskContext.revision,
510
576
  updatedAt: taskContext.now,
577
+ runtime: stampNew(task.data),
511
578
  });
512
579
  if (!nextTask.success) {
513
580
  this.markUncertain(nextWorkspace.data);
@@ -785,6 +852,7 @@ export class TaskStore {
785
852
  ...workspace,
786
853
  writer: { ...workspace.writer, state: "uncertain" as const },
787
854
  revision: newRevision(),
855
+ runtime: stampNew(workspace),
788
856
  };
789
857
  this.replaceSnapshot(this.workspacePath, value, workspace);
790
858
  }
@@ -1027,9 +1095,17 @@ export class TaskStore {
1027
1095
  if (isObject(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
1028
1096
  return failure("unsupported_version", "unsupported snapshot schema version");
1029
1097
  const parsed = schema.safeParse(value);
1030
- return parsed.success
1031
- ? success(null, null, parsed.data)
1032
- : failure("recovery_required", "snapshot does not satisfy its schema");
1098
+ if (parsed.success) return success(null, null, parsed.data);
1099
+ const writerVersion =
1100
+ isObject(value) && isObject((value as { runtime?: unknown }).runtime)
1101
+ ? (value as { runtime: { updatedWith?: unknown } }).runtime.updatedWith
1102
+ : null;
1103
+ if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
1104
+ return failure(
1105
+ "recovery_required",
1106
+ `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`,
1107
+ );
1108
+ return failure("recovery_required", "snapshot does not satisfy its schema");
1033
1109
  }
1034
1110
 
1035
1111
  private processStart(pid: number): string | null {
@@ -1041,10 +1117,14 @@ export class TaskStore {
1041
1117
  }
1042
1118
 
1043
1119
  private conflict(expected: Revision, actual: Revision): Result<never> {
1044
- return failure("revision_conflict", "snapshot revision does not match", {
1045
- expectedRevision: expected,
1046
- actualRevision: actual,
1047
- });
1120
+ return failure(
1121
+ "revision_conflict",
1122
+ "snapshot revision does not match; omit expectedRevision to use the current record",
1123
+ {
1124
+ expectedRevision: expected,
1125
+ actualRevision: actual,
1126
+ },
1127
+ );
1048
1128
  }
1049
1129
 
1050
1130
  private taskPath(taskId: Id) {
@@ -93,6 +93,12 @@ const same = (left: unknown, right: unknown): boolean => {
93
93
  }
94
94
  };
95
95
 
96
+ /** The one shared definition of active or uncertain worker state used by
97
+ * close, revise, resume, and replacement-launch checks. */
98
+ export const UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"] as const;
99
+ export const isUncertainWorker = (state: string): boolean =>
100
+ (UNCERTAIN_WORKER_STATES as readonly string[]).includes(state);
101
+
96
102
  const callerSession = (caller: CallerContext): HostSession =>
97
103
  caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
98
104
 
package/src/core.ts CHANGED
@@ -22,6 +22,8 @@ export {
22
22
  decisionDigest,
23
23
  candidateDigest,
24
24
  requirementId,
25
+ BINDING_QUESTION_BUDGET,
26
+ workitBindingQuestionIssue,
25
27
  } from "./core/task-contract";
26
28
  export type {
27
29
  OperationFamily,
@@ -55,7 +57,7 @@ export type {
55
57
  ExportBundle,
56
58
  Result as ContractResult,
57
59
  } from "./core/task-contract";
58
- export { TaskStore } from "./core/task-store";
60
+ export { TaskStore, runtimeVersion } from "./core/task-store";
59
61
  export type { MetadataLock, ProcessEvidence, RecoveryInput } from "./core/task-store";
60
62
  export { compactTaskContext, reconcileResume } from "./core/task-context";
61
63
  export type {
@@ -66,7 +68,7 @@ export type {
66
68
  } from "./core/task-context";
67
69
  export { METHODS, invariantBootstrap, selectMethods } from "./core/methods";
68
70
  export type { MethodId, SelectedMethod } from "./core/methods";
69
- export { shellRouteIntent } from "./core/route-intent";
71
+ export { hasLiveWorkitTask, shellRouteIntent, shouldDenyShellRoute } from "./core/route-intent";
70
72
  export type { ShellRouteIntent } from "./core/route-intent";
71
73
  export { applicableDecision, reserveAction, settleAction, reconcileAction } from "./core/authority";
72
74
  export {
@@ -84,9 +86,16 @@ export type {
84
86
  ExternalActionOperation,
85
87
  } from "./core/external-action";
86
88
  export {
89
+ approvedBranchSetupIntent,
87
90
  approvedExternalAction,
91
+ approvedPlanCommit,
92
+ branchSetupIntent,
93
+ planCommitDescriptor,
94
+ planCommitBinding,
95
+ commitMessageFromDescriptor,
88
96
  externalActionState,
89
97
  priorExternalAction,
98
+ priorResolvedDrift,
90
99
  externalActionRef,
91
100
  externalActionDescriptor,
92
101
  externalActionHelp,