@brainervirus/workit-core 4.0.0 → 6.0.0

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 (60) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/skills/workit-bdd/SKILL.md +35 -29
  4. package/skills/workit-continue/SKILL.md +53 -0
  5. package/skills/workit-debug/SKILL.md +41 -36
  6. package/skills/workit-deslop/SKILL.md +35 -25
  7. package/skills/workit-fanout/SKILL.md +62 -0
  8. package/skills/workit-fanout/references/brief.md +56 -0
  9. package/skills/workit-implement/SKILL.md +48 -44
  10. package/skills/workit-review/SKILL.md +42 -51
  11. package/skills/workit-review/references/impact.md +24 -0
  12. package/skills/workit-shape/SKILL.md +71 -0
  13. package/skills/workit-shape/references/diagrams.md +17 -0
  14. package/skills/workit-shape/references/knowledge.md +58 -0
  15. package/skills/workit-shape/references/mockups.md +15 -0
  16. package/skills/workit-shape/references/slicing.md +42 -0
  17. package/skills/workit-ship/SKILL.md +52 -0
  18. package/skills/workit-test-audit/SKILL.md +10 -2
  19. package/skills/workit-verify-app/SKILL.md +63 -0
  20. package/skills/workit-verify-app/references/template.md +49 -0
  21. package/src/autonomy.ts +309 -44
  22. package/src/core/context-read.ts +144 -0
  23. package/src/core/methods.ts +71 -98
  24. package/src/core/skill-manifests.ts +49 -25
  25. package/src/core/store-lock.ts +1 -1
  26. package/src/core/task-context.ts +1 -1
  27. package/src/core/task-contract.ts +8 -130
  28. package/src/core/task-engine.ts +57 -879
  29. package/src/core/task-evaluation.ts +63 -5
  30. package/src/core/task-store.ts +2 -107
  31. package/src/core/workers.ts +12 -158
  32. package/src/core/workspaces.ts +15 -0
  33. package/src/core/youtrack-writes.ts +92 -0
  34. package/src/core.ts +2 -79
  35. package/src/forge/pr-ops.ts +2 -2
  36. package/src/hooks/handle.ts +29 -5
  37. package/src/hooks/hosts/claude-code.ts +0 -8
  38. package/src/hooks/hosts/codex.ts +2 -11
  39. package/src/hooks/hosts/cursor.ts +0 -8
  40. package/src/hooks/hosts/opencode.ts +0 -8
  41. package/src/hooks/hosts/pi.ts +0 -10
  42. package/src/ledger.ts +19 -10
  43. package/templates/plan-template.md +17 -18
  44. package/templates/spec-template.md +4 -3
  45. package/templates/workit-contract.md +3 -2
  46. package/skills/workit-babysit/SKILL.md +0 -37
  47. package/skills/workit-behavioral-tdd/SKILL.md +0 -56
  48. package/skills/workit-blast-radius/SKILL.md +0 -26
  49. package/skills/workit-challenge/SKILL.md +0 -47
  50. package/skills/workit-diagram/SKILL.md +0 -27
  51. package/skills/workit-green-run/SKILL.md +0 -24
  52. package/skills/workit-handoff/SKILL.md +0 -37
  53. package/skills/workit-mockup/SKILL.md +0 -23
  54. package/skills/workit-plan/SKILL.md +0 -45
  55. package/skills/workit-steer/SKILL.md +0 -39
  56. package/src/core/authority.ts +0 -1033
  57. package/src/core/auto-approval.ts +0 -338
  58. package/src/core/external-action-effects.ts +0 -2441
  59. package/src/core/external-action.ts +0 -1204
  60. package/templates/execution-contract.md +0 -40
@@ -42,27 +42,9 @@ import {
42
42
  type OperationFamily,
43
43
  type WorkspaceRecord,
44
44
  taskRecordSchema,
45
+ scopeCovers as bindingCovers,
45
46
  } from "./task-contract";
46
- import {
47
- bindingCovers,
48
- applicableDecision,
49
- storedDecisionApplicable,
50
- reserveAction as reserveBoundedAction,
51
- settleAction as settleBoundedAction,
52
- reconcileAction as reconcileBoundedAction,
53
- verifyNativeAction,
54
- verifyNativeReconciliation,
55
- verifyNativeDecision,
56
- verifyDecisionContent,
57
- retireNativeAuthority,
58
- type ActionReservation,
59
- type NativeAuthorityVerifier,
60
- type ReserveActionInput,
61
- type SettleActionInput,
62
- type ReconcileActionInput,
63
- } from "./authority";
64
47
  import { diffPolicy, resolvePolicy } from "./policy-resolver";
65
- import { verifyStandingApproval } from "./auto-approval";
66
48
  import { sameDirectoryIdentity, TaskStore } from "./task-store";
67
49
  import { checkoutRootOf } from "../store/paths";
68
50
  import { defaultLockTimeout } from "./store-lock";
@@ -73,17 +55,11 @@ import {
73
55
  type ResumeReconciliation,
74
56
  } from "./task-context";
75
57
  import {
76
- assertProductWriteAllowed,
77
58
  isUncertainWorker,
78
59
  workerBlocksTransition,
79
- type CallerContext,
80
60
  type NativeWorkerObservation,
81
61
  type NativeWorkerVerification,
82
62
  type NativeWorkerVerifier,
83
- type WorkerDispatch,
84
- type WorkerDispatchCommit,
85
- type WorkerDispatchRequest,
86
- type WorkerDispatchStage,
87
63
  } from "./workers";
88
64
  import {
89
65
  captureCandidate,
@@ -96,6 +72,7 @@ import {
96
72
  evaluateRequirements,
97
73
  findingVerificationPasses,
98
74
  resolveBinding,
75
+ verifyDecisionContentAtRoot,
99
76
  type CandidateEnvironment,
100
77
  } from "./task-evaluation";
101
78
 
@@ -108,7 +85,6 @@ export type OperationContext = {
108
85
  capabilities: Capability[];
109
86
  constraints: Constraint[];
110
87
  now: Utc | (() => Utc);
111
- nativeAuthority?: NativeAuthorityVerifier;
112
88
  nativeWorker?: NativeWorkerVerifier;
113
89
  workerId?: string | null;
114
90
  };
@@ -128,22 +104,8 @@ const provenance = (
128
104
  host: context.caller.host,
129
105
  session: { kind: "host" as const, host: context.caller.host, handle: context.caller.actor },
130
106
  workerId: context.workerId ?? null,
131
- receipts: [],
132
107
  });
133
108
  const environment = (): CandidateEnvironment => [];
134
- /**
135
- * The operation a decision authorizes: canonical descriptors carry it as JSON,
136
- * legacy single-operation approvals are the bare operation string.
137
- */
138
- const decisionOperation = (approvedContent: string): string | null => {
139
- try {
140
- const parsed = JSON.parse(approvedContent) as { operation?: unknown };
141
- if (typeof parsed?.operation === "string") return parsed.operation;
142
- } catch {
143
- // a bare operation string is not JSON
144
- }
145
- return /^[a-z][a-z_]*\.[a-z_]+$/.test(approvedContent) ? approvedContent : null;
146
- };
147
109
  const sameSession = (value: unknown, context: OperationContext): boolean =>
148
110
  typeof value === "object" &&
149
111
  value !== null &&
@@ -175,22 +137,6 @@ type WorkerAuthority = {
175
137
  type VerifiedWorker = object;
176
138
  const verifiedWorkers = new WeakMap<object, WorkerAuthority>();
177
139
 
178
- type DispatchReservation = {
179
- owner: object;
180
- store: TaskStore;
181
- root: string;
182
- caller: Caller;
183
- taskId: string;
184
- workspaceId: string;
185
- workerId: string;
186
- };
187
- /**
188
- * Live launch reservations. Only this module can create one, only the adapter that
189
- * received it holds it, and a process restart loses it: a worker left `assigned`
190
- * with no session and no live reservation can never be resolved by inference.
191
- */
192
- const dispatchReservations = new WeakMap<object, DispatchReservation>();
193
-
194
140
  const sameValue = (left: unknown, right: unknown): boolean => {
195
141
  try {
196
142
  return canonicalJson(left) === canonicalJson(right);
@@ -210,66 +156,10 @@ const validNativeProvenance = (
210
156
  observed.kind === "host_observed" &&
211
157
  observed.host === caller.host &&
212
158
  observed.workerId === expected.workerId &&
213
- sameValue(observed.session, expected.session) &&
214
- observed.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host)
215
- );
216
- };
217
-
218
- /** A dispatch has no worker session yet, so it is bound to the attesting host session. */
219
- const validDispatchProvenance = (
220
- value: unknown,
221
- caller: Caller,
222
- workerId: string,
223
- ): value is Provenance => {
224
- if (!provenanceSchema.safeParse(value).success) return false;
225
- const observed = value as Provenance;
226
- return (
227
- observed.kind === "host_observed" &&
228
- observed.host === caller.host &&
229
- observed.workerId === workerId &&
230
- observed.session?.kind === "host" &&
231
- observed.session.host === caller.host &&
232
- observed.session.handle === caller.actor &&
233
- observed.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host)
159
+ sameValue(observed.session, expected.session)
234
160
  );
235
161
  };
236
162
 
237
- const validResumeApproval = (
238
- task: TaskRecord,
239
- workspace: WorkspaceRecord,
240
- authorityRefs: Ref[],
241
- context: OperationContext,
242
- ): boolean =>
243
- authorityRefs.some((ref) => {
244
- if (ref.kind !== "record" || ref.collection !== "decisions") return false;
245
- const entry = task.decisions.find((candidate) => candidate.id === ref.id);
246
- if (!entry || entry.provenance.kind !== "host_observed") return false;
247
- if (
248
- entry.provenance.host !== context.caller.host ||
249
- !sameSession(entry.provenance.session, context) ||
250
- !entry.provenance.receipts.some(
251
- (receipt) => receipt.kind === "host" && receipt.host === context.caller.host,
252
- )
253
- )
254
- return false;
255
- const decision = entry.data;
256
- if (
257
- !["design", "action"].includes(decision.purpose) ||
258
- decision.response !== "approved" ||
259
- decision.revoked !== null ||
260
- decision.binding.approvedContent !== "resume" ||
261
- decision.binding.taskId !== task.id ||
262
- decision.binding.workspaceId !== workspace.id ||
263
- canonicalJson(decision.binding.scope) !== canonicalJson(task.intent.data.scope)
264
- )
265
- return false;
266
- return (["design", "action"] as const).some((purpose) =>
267
- applicableDecision(task, purpose, decision.binding, workspace.root).some(
268
- (candidate) => candidate.digest === decision.digest,
269
- ),
270
- );
271
- });
272
-
273
163
  const verifyNativeWorker = (
274
164
  verifier: NativeWorkerVerifier | undefined,
275
165
  input: NativeWorkerVerification,
@@ -333,8 +223,6 @@ type ObserveWorkerLifecycleInput = NativeWorkerObservation & {
333
223
  authorityOwner: object;
334
224
  caller: Caller;
335
225
  now?: Utc;
336
- /** Set only by commitWorkerDispatch for a reservation that produced no child. */
337
- dispatch?: boolean;
338
226
  };
339
227
 
340
228
  const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<Worker>> => {
@@ -368,62 +256,62 @@ const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<
368
256
  return failure("invalid_transition", "closed task cannot observe workers");
369
257
  if (task.data.status === "paused" && (input.state === "running" || input.state === "cancelling"))
370
258
  return failure("invalid_transition", "paused task cannot run a worker");
371
- const notStarted = input.dispatch === true;
372
- if (notStarted && entry.data.state === "stopped" && input.session === null)
373
- return success(task.data.revision, workspace.data.revision, entry);
374
- if (notStarted && (input.state !== "stopped" || input.session !== null))
375
- return failure("invalid_input", "a never-dispatched worker stops with no session");
376
- if (notStarted && entry.data.session !== null)
377
- return failure("invalid_transition", "worker already has an observed session");
378
- if (notStarted && !["dispatching", "cancelling"].includes(entry.data.state))
379
- return failure("invalid_transition", "worker launch was not claimed");
259
+ // Compare-and-swap first: a stale observation (another process launched
260
+ // and observed this worker meanwhile) must not pass as a no-op below.
261
+ if (input.expectedRevision !== task.data.revision)
262
+ return failure("revision_conflict", "worker observation is stale; re-read the task", {
263
+ expectedRevision: input.expectedRevision,
264
+ actualRevision: task.data.revision,
265
+ });
380
266
  if (input.report && input.state !== "stopped")
381
267
  return failure("invalid_input", "worker reports require a stopped worker");
382
268
  if (
383
- (input.state === "running" ||
384
- input.state === "cancelling" ||
385
- (input.state === "stopped" && !notStarted)) &&
269
+ (input.state === "running" || input.state === "cancelling" || input.state === "stopped") &&
386
270
  !input.session
387
271
  )
388
272
  return failure("invalid_input", "running worker observations require a session");
389
273
  if (entry.data.session && !sameValue(entry.data.session, input.session))
390
274
  return failure("permission_denied", "worker session does not match its assignment");
391
- if (input.state !== "running" && entry.data.state === "assigned" && input.state !== "stopped")
275
+ // `unknown` from `assigned` records a launch whose outcome was never
276
+ // observed (its launcher died); the lead must cancel before a relaunch.
277
+ if (
278
+ entry.data.state === "assigned" &&
279
+ input.state !== "running" &&
280
+ input.state !== "stopped" &&
281
+ input.state !== "unknown"
282
+ )
392
283
  return failure("invalid_transition", "worker has not started");
393
284
  if (input.state === "running" && entry.data.state === "stopped")
394
285
  return failure("invalid_transition", "stopped worker cannot run again");
395
286
  const terminalCancel = entry.data.state === "cancelling" && input.state === "running";
287
+ // The first observation replaces the assigning provenance with the child's,
288
+ // so the assigning coordinator session is kept on the worker: a restarted
289
+ // host still attributes the running child to its coordinator.
290
+ const coordinator =
291
+ entry.data.coordinator ??
292
+ (entry.data.state === "assigned" && entry.provenance.session?.kind === "host"
293
+ ? entry.provenance.session
294
+ : undefined);
396
295
  const nextEntry = {
397
296
  ...entry,
398
297
  recordedAt: input.now ?? entry.recordedAt,
399
298
  provenance: authority.provenance,
400
299
  data: {
401
300
  ...entry.data,
301
+ ...(coordinator ? { coordinator } : {}),
402
302
  state: terminalCancel ? ("cancelling" as const) : input.state,
403
303
  session: input.session,
404
304
  report: entry.data.report ?? input.report ?? null,
405
305
  },
406
306
  } satisfies Entry<Worker>;
407
- const shouldClear =
408
- input.state === "stopped" &&
409
- workspace.data.writer !== null &&
410
- workspace.data.writer.owner.taskId === task.data.id &&
411
- workspace.data.writer.owner.workerId === input.workerId;
412
- const shouldUncertain =
413
- input.state === "unknown" &&
414
- workspace.data.writer !== null &&
415
- workspace.data.writer.owner.taskId === task.data.id &&
416
- workspace.data.writer.owner.workerId === input.workerId;
417
- // A no-op observation (same state, same session, no writer side-effect)
307
+ // A no-op observation (same state, same session)
418
308
  // must not rewrite the entry: hosts observe on every session event, and a
419
309
  // revision bump per event livelocks worker sessions — each call would
420
310
  // invalidate the revision the previous call returned.
421
311
  if (
422
312
  entry.data.state === nextEntry.data.state &&
423
313
  sameValue(entry.data.session, input.session) &&
424
- sameValue(entry.data.report, nextEntry.data.report) &&
425
- !shouldClear &&
426
- !shouldUncertain
314
+ sameValue(entry.data.report, nextEntry.data.report)
427
315
  )
428
316
  return success(task.data.revision, workspace.data.revision, entry);
429
317
  const changed = input.store.mutateTaskAndWorkspace({
@@ -431,15 +319,7 @@ const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<
431
319
  expectedRevision: input.expectedRevision,
432
320
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
433
321
  now: input.now,
434
- workspace: (current, mutation) =>
435
- success(mutation.revision, mutation.revision, {
436
- ...current,
437
- writer: shouldClear
438
- ? null
439
- : shouldUncertain && current.writer
440
- ? { ...current.writer, state: "uncertain" as const }
441
- : current.writer,
442
- }),
322
+ workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
443
323
  task: (current, mutation) =>
444
324
  success(mutation.revision, null, {
445
325
  ...current,
@@ -481,8 +361,6 @@ const TASK_SCOPED_ACTIONS: ReadonlySet<string> = new Set([
481
361
  "worker.assign",
482
362
  "worker.report",
483
363
  "worker.cancel",
484
- "writer.acquire",
485
- "writer.release",
486
364
  "state.export",
487
365
  ]);
488
366
  /** Recording actions that create the implicit task on first use. */
@@ -494,7 +372,6 @@ const CREATING_ACTIONS: ReadonlySet<string> = new Set([
494
372
  "finding.record",
495
373
  "decision.record",
496
374
  "worker.assign",
497
- "writer.acquire",
498
375
  ]);
499
376
  /** Commit attempts for a call whose revisions the engine filled (see retryFilledRevisions). */
500
377
  const REVISION_RETRY_ATTEMPTS = 8;
@@ -580,7 +457,6 @@ const importedProvenance = (context: OperationContext): Provenance => ({
580
457
  host: context.caller.host,
581
458
  session: null,
582
459
  workerId: null,
583
- receipts: [],
584
460
  });
585
461
 
586
462
  const portableTask = (task: TaskRecord): TaskRecord => {
@@ -606,21 +482,6 @@ const portableTask = (task: TaskRecord): TaskRecord => {
606
482
  };
607
483
  for (const entry of clone.assessments)
608
484
  demote(entry.data.facts, entry.data.signals, entry.data.consequences);
609
- const sourceDecisions = new Map(task.decisions.map((entry) => [entry.id, entry]));
610
- for (const entry of clone.decisions) {
611
- const consumption = sourceDecisions.get(entry.id)?.data.consumption;
612
- if (!consumption) continue;
613
- entry.data.consumption = {
614
- ...consumption,
615
- state: consumption.state === "reserved" ? "uncertain" : consumption.state,
616
- actionRef: { kind: "record", collection: "decisions", id: entry.id },
617
- ...(consumption.historicalActionId
618
- ? { historicalActionId: consumption.historicalActionId }
619
- : consumption.actionRef.kind === "host"
620
- ? { historicalActionId: consumption.actionRef.handle }
621
- : {}),
622
- };
623
- }
624
485
  // Candidate metadata can contain environment-derived paths and digests. The destination
625
486
  // must recapture its own candidate instead of receiving source checkout material.
626
487
  clone.candidates = [];
@@ -725,10 +586,6 @@ const importedTask = (
725
586
  : null,
726
587
  },
727
588
  }));
728
- const actionProgress = source.actionProgress?.map((entry) => ({
729
- ...entry,
730
- decisionId: ids.get(entry.decisionId) ?? entry.decisionId,
731
- }));
732
589
  const task: TaskRecord = {
733
590
  ...source,
734
591
  id: newId(),
@@ -758,17 +615,9 @@ const importedTask = (
758
615
  taskId: "",
759
616
  workspaceId: destinationWorkspaceId,
760
617
  },
761
- consumption: data.consumption
762
- ? {
763
- ...data.consumption,
764
- state: data.consumption.state === "reserved" ? "uncertain" : data.consumption.state,
765
- actionRef: { kind: "record", collection: "decisions", id: entry.id },
766
- }
767
- : null,
768
618
  },
769
619
  };
770
620
  }),
771
- ...(actionProgress ? { actionProgress } : {}),
772
621
  findings,
773
622
  workers,
774
623
  candidates: [],
@@ -821,13 +670,6 @@ export class WorkitCore {
821
670
  return success(null, null, null);
822
671
  }
823
672
 
824
- private callerContext(): CallerContext {
825
- return {
826
- ...this.context.caller,
827
- workerId: this.context.workerId ?? null,
828
- };
829
- }
830
-
831
673
  /**
832
674
  * Omitted revisions default to the records read for this call; explicit values
833
675
  * stay CAS-enforced downstream. Call after loading task/workspace records.
@@ -910,12 +752,7 @@ export class WorkitCore {
910
752
  : failure("permission_denied", "helpers cannot control task lifecycle or scope");
911
753
  }
912
754
 
913
- private activeWorkerBlocker(
914
- task: TaskRecord,
915
- workspace: import("./task-contract").WorkspaceRecord,
916
- ) {
917
- if (workspace.writer)
918
- return failure("recovery_required", "writer ownership must be released first");
755
+ private activeWorkerBlocker(task: TaskRecord) {
919
756
  if (task.workers.some((entry) => workerBlocksTransition("close", entry.data.state)))
920
757
  return failure("recovery_required", "worker state requires reconciliation");
921
758
  return null;
@@ -1083,7 +920,6 @@ export class WorkitCore {
1083
920
  status: task.status,
1084
921
  closure: task.closure,
1085
922
  progress: task.progress,
1086
- writer: task.status === "closed" ? null : workspace.data!.writer,
1087
923
  source: {
1088
924
  host: task.intent.provenance.host,
1089
925
  kind: task.intent.provenance.kind,
@@ -1381,29 +1217,10 @@ export class WorkitCore {
1381
1217
  return resolved.ok ? this.recordDecision(resolved.data) : resolved;
1382
1218
  }
1383
1219
 
1384
- observeDecision(request: unknown, observation: unknown): Result<Entry<Decision>> {
1385
- const resolved = this.implicitTask("decision", request);
1386
- return resolved.ok ? this.recordDecision(resolved.data, observation, true) : resolved;
1387
- }
1388
-
1389
- /**
1390
- * Record a standing auto-approval: no live question, authorized by the
1391
- * workspace rule named in binding.standing while it covers the operation.
1392
- * Lead-only, task-scoped, verified live here — revocation is config
1393
- * removal, which fails the next reserve closed.
1394
- */
1395
- observeStandingDecision(request: unknown): Result<Entry<Decision>> {
1396
- const resolved = this.implicitTask("decision", request);
1397
- return resolved.ok ? this.recordDecision(resolved.data, undefined, true, true) : resolved;
1398
- }
1399
-
1400
1220
  /**
1401
1221
  * Commit a task mutation whose update re-validates everything it depends on
1402
- * under the lock. Decisions use this instead of a whole-operation retry
1403
- * because a native receipt is retired before the commit and cannot be
1404
- * verified twice: a CAS loss on an engine-filled revision re-reads the
1405
- * record, repeats `recheck` against it and retries the commit; the update's
1406
- * in-lock receipt check keeps a receipt from being consumed twice.
1222
+ * under the lock: a CAS loss on an engine-filled revision re-reads the
1223
+ * record, repeats `recheck` against it and retries the commit.
1407
1224
  */
1408
1225
  private commitTask(
1409
1226
  taskId: string,
@@ -1427,40 +1244,20 @@ export class WorkitCore {
1427
1244
  });
1428
1245
  }
1429
1246
 
1430
- private recordDecision(
1431
- request: unknown,
1432
- nativeObservation?: unknown,
1433
- nativeRequired = false,
1434
- standing = false,
1435
- ): Result<Entry<Decision>> {
1247
+ /**
1248
+ * A decision is a durable, agent-asserted record (D2, D18). It satisfies
1249
+ * `decisions` requirements and accepted limitations; it never authorizes an
1250
+ * external effect, so no host attestation is minted or checked.
1251
+ */
1252
+ private recordDecision(request: unknown): Result<Entry<Decision>> {
1436
1253
  const root = this.contextRootError();
1437
1254
  if (!root.ok) return root;
1438
1255
  const parsed = parseOperation("decision", request);
1439
1256
  if (!parsed.ok) return parsed;
1440
1257
  const input = parsed.data;
1441
- if (nativeRequired && input.action !== "record")
1442
- return failure(
1443
- "permission_denied",
1444
- "native observation is only valid for recording decisions",
1445
- );
1446
- // Stated choices carry user-settled decisions without a native receipt.
1447
- // They never authorize mutating actions; native verification is skipped
1448
- // and provenance stays agent-reported.
1449
1258
  const stated = input.action === "record" && input.response === "stated";
1450
- if (stated && input.purpose === "action")
1451
- return failure("permission_denied", "stated choices cannot authorize mutating actions");
1452
1259
  if (stated && !input.binding?.statedChoice)
1453
1260
  return failure("invalid_input", "stated choices require binding.statedChoice");
1454
- if (nativeRequired && nativeObservation === undefined && !stated && !standing)
1455
- return failure("permission_denied", "native decision observation is required");
1456
- if (
1457
- standing &&
1458
- (!input.binding?.standing || input.purpose !== "action" || input.response !== "approved")
1459
- )
1460
- return failure(
1461
- "invalid_input",
1462
- "standing approvals need an approved action binding with a standing rule",
1463
- );
1464
1261
  const task = this.store.readTask(input.taskId);
1465
1262
  if (!task.ok) return task;
1466
1263
  if ((this.context.workerId ?? null) !== null)
@@ -1474,8 +1271,8 @@ export class WorkitCore {
1474
1271
  if (!workspace.ok) return workspace;
1475
1272
  if (!workspace.data) return failure("not_found", "workspace not found");
1476
1273
  if (input.binding.taskId !== task.data.id || input.binding.workspaceId !== workspace.data.id)
1477
- return failure("permission_denied", "decision task or workspace binding is invalid");
1478
- const content = verifyDecisionContent(this.store, input.binding);
1274
+ return failure("invalid_input", "decision task or workspace binding is invalid");
1275
+ const content = verifyDecisionContentAtRoot(this.store.root, input.binding);
1479
1276
  if (!content.ok) return content;
1480
1277
  const knownRequirements = new Set(
1481
1278
  task.data.policy?.requirements.map((item) => item.id) ?? [],
@@ -1488,85 +1285,26 @@ export class WorkitCore {
1488
1285
  response: input.response,
1489
1286
  requirementIds: input.requirementIds,
1490
1287
  revoked: null,
1491
- consumption: null,
1492
1288
  } satisfies Omit<Decision, "digest">;
1493
1289
  const data: Decision = { ...base, digest: decisionDigest(base) };
1494
- const standingApproval =
1495
- standing && !stated
1496
- ? verifyStandingApproval(this.store.root, task.data, this.context.caller, input.binding)
1497
- : null;
1498
- if (standingApproval && !standingApproval.ok) return standingApproval;
1499
- const native =
1500
- nativeRequired && !stated && !standingApproval
1501
- ? verifyNativeDecision(
1502
- this.context.nativeAuthority,
1503
- {
1504
- observation: nativeObservation,
1505
- expected: {
1506
- taskId: task.data.id,
1507
- workspaceId: workspace.data.id,
1508
- purpose: input.purpose,
1509
- response: input.response,
1510
- binding: input.binding,
1511
- bindingBytes: canonicalJson(input.binding),
1512
- digest: data.digest,
1513
- requirementIds: [...input.requirementIds],
1514
- },
1515
- caller: this.context.caller,
1516
- },
1517
- { owner: this.authorityOwner, store: this.store, root: this.store.root },
1518
- )
1519
- : null;
1520
- if (native && !native.ok) return native;
1521
- const nativeProvenance = native?.ok ? retireNativeAuthority(native.data) : null;
1522
- if (native?.ok && !nativeProvenance)
1523
- return failure("permission_denied", "native decision authority was retired");
1524
- // A retry repeats the pre-lock checks that depend on the task record and
1525
- // records the provenance of the standing re-verification it passed;
1526
- // closure, receipt reuse, content and requirements are re-checked in the lock.
1527
- let standingProvenance = standingApproval?.ok === true ? standingApproval.data : null;
1528
- const recheck = (fresh: TaskRecord): Result<unknown> => {
1529
- if (fresh.status === "closed")
1530
- return failure("invalid_transition", "closed task cannot record a decision");
1531
- if (!standingProvenance) return success(null, null, null);
1532
- const again = verifyStandingApproval(
1533
- this.store.root,
1534
- fresh,
1535
- this.context.caller,
1536
- input.binding,
1537
- );
1538
- if (again.ok) standingProvenance = again.data;
1539
- return again;
1540
- };
1541
1290
  const changed = this.commitTask(
1542
1291
  task.data.id,
1543
1292
  input.expectedRevision,
1544
1293
  filled,
1545
- recheck,
1294
+ (fresh) =>
1295
+ fresh.status === "closed"
1296
+ ? failure("invalid_transition", "closed task cannot record a decision")
1297
+ : success(null, null, null),
1546
1298
  (current, mutation) => {
1547
1299
  if (current.status === "closed")
1548
1300
  return failure("invalid_transition", "closed task cannot record a decision");
1549
- const latestContent = verifyDecisionContent(this.store, input.binding);
1550
- if (!latestContent.ok) return latestContent;
1551
1301
  const known = new Set(current.policy?.requirements.map((item) => item.id) ?? []);
1552
1302
  if (input.requirementIds.some((id: string) => !known.has(id)))
1553
1303
  return failure("invalid_input", "decision references an unknown requirement");
1554
- if (
1555
- nativeProvenance &&
1556
- current.decisions.some((entry) =>
1557
- entry.provenance.receipts.some((existingReceipt) =>
1558
- nativeProvenance.receipts.some(
1559
- (receipt) => canonicalJson(receipt) === canonicalJson(existingReceipt),
1560
- ),
1561
- ),
1562
- )
1563
- )
1564
- return failure("permission_denied", "native decision receipt was already consumed");
1565
1304
  const entry: Entry<Decision> = {
1566
1305
  id: newId(),
1567
1306
  recordedAt: mutation.now,
1568
- provenance:
1569
- standingProvenance ?? nativeProvenance ?? provenance(this.context, "agent_reported"),
1307
+ provenance: provenance(this.context, "agent_reported"),
1570
1308
  data,
1571
1309
  };
1572
1310
  return success(mutation.revision, null, {
@@ -1583,8 +1321,6 @@ export class WorkitCore {
1583
1321
  if (!input.reason.trim())
1584
1322
  return failure("invalid_input", "decision revocation requires a reason");
1585
1323
  if (decision.data.revoked) return failure("invalid_transition", "decision is already revoked");
1586
- if (decision.data.consumption)
1587
- return failure("permission_denied", "consumed decision cannot be revoked");
1588
1324
  const changed = this.commitTask(
1589
1325
  task.data.id,
1590
1326
  input.expectedRevision,
@@ -1594,8 +1330,6 @@ export class WorkitCore {
1594
1330
  const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
1595
1331
  if (!entry) return failure("not_found", "decision not found");
1596
1332
  if (entry.data.revoked) return failure("invalid_transition", "decision is already revoked");
1597
- if (entry.data.consumption)
1598
- return failure("permission_denied", "consumed decision cannot be revoked");
1599
1333
  const data = {
1600
1334
  ...entry.data,
1601
1335
  revoked: { at: mutation.now, reason: input.reason },
@@ -1740,8 +1474,7 @@ export class WorkitCore {
1740
1474
  entry.data.purpose === "limitation" &&
1741
1475
  entry.data.response === "approved" &&
1742
1476
  entry.data.revoked === null &&
1743
- entry.data.digest === decisionDigest(entry.data) &&
1744
- verifyDecisionContent(this.store, entry.data.binding).ok &&
1477
+ verifyDecisionContentAtRoot(this.store.root, entry.data.binding).ok &&
1745
1478
  entry.data.binding.taskId === task.data.id &&
1746
1479
  entry.data.binding.workspaceId === workspace.data!.id &&
1747
1480
  bindingCovers(entry.data.binding.scope, finding.data.scope) &&
@@ -1955,30 +1688,22 @@ export class WorkitCore {
1955
1688
  return failure("permission_denied", "helpers cannot cancel or control workers");
1956
1689
  if (task.data.status === "closed")
1957
1690
  return failure("invalid_transition", "closed task cannot cancel workers");
1958
- if (entry.data.state === "unknown")
1959
- return failure("recovery_required", "worker state requires recovery");
1960
1691
  if (entry.data.state === "stopped") {
1961
1692
  // Idempotent cancel: repeating a settled stop succeeds with the
1962
1693
  // current entry instead of failing the retry.
1963
1694
  const stopped = task.data.workers.find((candidate) => candidate.id === input.workerId)!;
1964
1695
  return success(task.data.revision, workspace.data.revision, stopped);
1965
1696
  }
1966
- // Lead-attested cancel is terminal for every live state: the lead saw
1967
- // the run end, so no host observation is awaited. A later live-child
1697
+ // Lead-attested cancel is terminal for every live or unknown state: the lead
1698
+ // saw the run end (or chose to abandon it), so no host observation is awaited. A later live-child
1968
1699
  // sighting against a stopped worker is a new anomaly for the host to
1969
- // flag, not a reason to strand the worker short of stopped. A writer
1970
- // held by the cancelled worker dies with it; anything else would brick
1971
- // the checkout behind a stopped owner.
1700
+ // flag, not a reason to strand the worker short of stopped.
1972
1701
  const changed = this.store.mutateTaskAndWorkspace({
1973
1702
  taskId: task.data.id,
1974
1703
  expectedRevision: input.expectedRevision,
1975
1704
  expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1976
1705
  now: trustedNow(this.context),
1977
- workspace: (current, mutation) =>
1978
- success(mutation.revision, mutation.revision, {
1979
- ...current,
1980
- writer: current.writer?.owner.workerId === input.workerId ? null : current.writer,
1981
- }),
1706
+ workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
1982
1707
  task: (current, mutation) =>
1983
1708
  success(mutation.revision, null, {
1984
1709
  ...current,
@@ -2032,7 +1757,6 @@ export class WorkitCore {
2032
1757
  if (!authority.ok) return authority;
2033
1758
  return applyWorkerLifecycle({
2034
1759
  ...input,
2035
- dispatch: false,
2036
1760
  store: this.store,
2037
1761
  authority: authority.data,
2038
1762
  authorityOwner: this.authorityOwner,
@@ -2041,542 +1765,6 @@ export class WorkitCore {
2041
1765
  });
2042
1766
  }
2043
1767
 
2044
- /** Host-only launch bookkeeping; no operation family exposes these two methods. */
2045
- private verifyDispatch(
2046
- stage: WorkerDispatchStage,
2047
- input: WorkerDispatchRequest,
2048
- workspaceId: string,
2049
- ): Result<Provenance> {
2050
- const verifier = this.context.nativeWorker?.verifyDispatch;
2051
- if (!verifier)
2052
- return failure("permission_denied", "native worker dispatch attestation is unavailable");
2053
- let result: Result<Provenance>;
2054
- try {
2055
- result = verifier({
2056
- observation: input.observation,
2057
- expected: {
2058
- taskId: input.taskId,
2059
- workspaceId,
2060
- workerId: input.workerId,
2061
- expectedRevision: input.expectedRevision,
2062
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
2063
- stage,
2064
- },
2065
- caller: this.context.caller,
2066
- });
2067
- } catch (error) {
2068
- return failure("permission_denied", `native worker dispatch failed: ${String(error)}`);
2069
- }
2070
- if (!result.ok || !validDispatchProvenance(result.data, this.context.caller, input.workerId))
2071
- return failure("permission_denied", "worker dispatch was not attested by the host");
2072
- return result;
2073
- }
2074
-
2075
- /**
2076
- * Claim the launch slot of an exactly-`assigned` worker before the host spawns it.
2077
- * The returned reservation is the only thing that can later record a truthful
2078
- * "never started" stop, so it stays in the adapter process and is never persisted.
2079
- */
2080
- prepareWorkerDispatch(input: WorkerDispatchRequest): Result<WorkerDispatch> {
2081
- const root = this.contextRootError();
2082
- if (!root.ok) return root;
2083
- if ((this.context.workerId ?? null) !== null)
2084
- return failure("permission_denied", "helpers cannot dispatch workers");
2085
- const task = this.store.readTask(input.taskId);
2086
- if (!task.ok) return task;
2087
- const workspace = this.store.readWorkspace();
2088
- if (!workspace.ok) return workspace;
2089
- if (!workspace.data) return failure("not_found", "workspace not found");
2090
- if (task.data.workspaceId !== workspace.data.id)
2091
- return failure("recovery_required", "worker workspace binding is invalid");
2092
- if (task.data.status !== "active")
2093
- return failure("invalid_transition", "paused or closed tasks cannot dispatch workers");
2094
- const entry = task.data.workers.find((candidate) => candidate.id === input.workerId);
2095
- if (!entry) return failure("not_found", "worker not found");
2096
- if (entry.data.state !== "assigned" || entry.data.session !== null)
2097
- return failure("invalid_transition", "worker is not awaiting dispatch");
2098
- const attested = this.verifyDispatch("prepare", input, workspace.data.id);
2099
- if (!attested.ok) return attested;
2100
- const changed = this.store.mutateTaskAndWorkspace({
2101
- taskId: task.data.id,
2102
- expectedRevision: input.expectedRevision,
2103
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
2104
- now: trustedNow(this.context),
2105
- workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
2106
- task: (current, mutation) => {
2107
- const candidate = current.workers.find((item) => item.id === input.workerId);
2108
- if (!candidate) return failure("not_found", "worker not found");
2109
- if (candidate.data.state !== "assigned" || candidate.data.session !== null)
2110
- return failure("invalid_transition", "worker is not awaiting dispatch");
2111
- return success(mutation.revision, null, {
2112
- ...current,
2113
- workers: current.workers.map((item) =>
2114
- item.id === input.workerId
2115
- ? {
2116
- ...item,
2117
- recordedAt: mutation.now,
2118
- provenance: attested.data,
2119
- data: {
2120
- ...item.data,
2121
- state: "dispatching" as const,
2122
- coordinator: {
2123
- kind: "host" as const,
2124
- host: this.context.caller.host,
2125
- handle: this.context.caller.actor,
2126
- },
2127
- },
2128
- }
2129
- : item,
2130
- ),
2131
- });
2132
- },
2133
- });
2134
- if (!changed.ok) return changed;
2135
- const dispatch = {} as WorkerDispatch;
2136
- dispatchReservations.set(dispatch, {
2137
- owner: this.authorityOwner,
2138
- store: this.store,
2139
- root: this.store.root,
2140
- caller: this.context.caller,
2141
- taskId: task.data.id,
2142
- workspaceId: workspace.data.id,
2143
- workerId: input.workerId,
2144
- });
2145
- return success(changed.data.task.revision, changed.data.workspace.revision, dispatch);
2146
- }
2147
-
2148
- /**
2149
- * Settle a live reservation exactly once: `started` binds the observed child session,
2150
- * `not_started` records a host-attested stop with no session. Whichever lands first
2151
- * consumes the reservation, so a launch and a cancellation cannot both win.
2152
- */
2153
- commitWorkerDispatch(input: WorkerDispatchCommit): Result<Entry<Worker>> {
2154
- const root = this.contextRootError();
2155
- if (!root.ok) return root;
2156
- if ((this.context.workerId ?? null) !== null)
2157
- return failure("permission_denied", "helpers cannot dispatch workers");
2158
- const reservation = dispatchReservations.get(input.dispatch);
2159
- if (
2160
- !reservation ||
2161
- reservation.owner !== this.authorityOwner ||
2162
- reservation.store !== this.store ||
2163
- reservation.root !== this.store.root ||
2164
- !sameValue(reservation.caller, this.context.caller) ||
2165
- reservation.taskId !== input.taskId ||
2166
- reservation.workerId !== input.workerId
2167
- )
2168
- return failure("permission_denied", "worker dispatch reservation is not live");
2169
- const workspace = this.store.readWorkspace();
2170
- if (!workspace.ok) return workspace;
2171
- if (!workspace.data) return failure("not_found", "workspace not found");
2172
- if (workspace.data.id !== reservation.workspaceId)
2173
- return failure("recovery_required", "worker workspace binding is invalid");
2174
- const observation: NativeWorkerObservation = {
2175
- taskId: input.taskId,
2176
- workerId: input.workerId,
2177
- expectedRevision: input.expectedRevision,
2178
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
2179
- state: input.outcome === "started" ? "running" : "stopped",
2180
- session: input.session,
2181
- observation: input.observation,
2182
- };
2183
- let authority: VerifiedWorker;
2184
- if (input.outcome === "started") {
2185
- if (!input.session) return failure("invalid_input", "a started worker requires a session");
2186
- const verified = verifyNativeWorker(
2187
- this.context.nativeWorker,
2188
- {
2189
- observation: input.observation,
2190
- expected: { ...observation, workspaceId: workspace.data.id },
2191
- caller: this.context.caller,
2192
- },
2193
- {
2194
- owner: this.authorityOwner,
2195
- store: this.store,
2196
- root: this.store.root,
2197
- caller: this.context.caller,
2198
- },
2199
- );
2200
- if (!verified.ok) return verified;
2201
- authority = verified.data;
2202
- } else {
2203
- if (input.session !== null)
2204
- return failure("invalid_input", "a never-dispatched worker stops with no session");
2205
- const attested = this.verifyDispatch("not_started", input, workspace.data.id);
2206
- if (!attested.ok) return attested;
2207
- authority = {};
2208
- verifiedWorkers.set(authority, {
2209
- taskId: input.taskId,
2210
- workspaceId: workspace.data.id,
2211
- workerId: input.workerId,
2212
- expectedRevision: input.expectedRevision,
2213
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
2214
- state: "stopped",
2215
- session: null,
2216
- report: null,
2217
- provenance: attested.data,
2218
- owner: this.authorityOwner,
2219
- store: this.store,
2220
- root: this.store.root,
2221
- caller: this.context.caller,
2222
- });
2223
- }
2224
- const applied = applyWorkerLifecycle({
2225
- ...observation,
2226
- dispatch: input.outcome === "not_started",
2227
- store: this.store,
2228
- authority,
2229
- authorityOwner: this.authorityOwner,
2230
- caller: this.context.caller,
2231
- now: trustedNow(this.context),
2232
- });
2233
- if (applied.ok) dispatchReservations.delete(input.dispatch);
2234
- return applied;
2235
- }
2236
-
2237
- writer(request: unknown): Result<WorkspaceRecord> {
2238
- const resolved = this.implicitTask("writer", request);
2239
- if (!resolved.ok) return resolved;
2240
- const value = resolved.data;
2241
- return this.retryOmittedRevisions(value, () => this.writerOnce(value));
2242
- }
2243
-
2244
- private writerOnce(request: unknown): Result<WorkspaceRecord> {
2245
- const root = this.contextRootError();
2246
- if (!root.ok) return root;
2247
- const parsed = parseOperation("writer", request);
2248
- if (!parsed.ok) return parsed;
2249
- const input = parsed.data;
2250
- const task = this.store.readTask(input.taskId);
2251
- if (!task.ok) return task;
2252
- const workspace = this.store.readWorkspace();
2253
- if (!workspace.ok) return workspace;
2254
- if (!workspace.data) return failure("not_found", "workspace not found");
2255
- if (task.data.status !== "active")
2256
- return failure("invalid_transition", "paused or closed tasks cannot own product writes");
2257
- this.fillRevisions(input, task.data, workspace.data);
2258
- const helperId = this.context.workerId ?? null;
2259
- if (input.action === "acquire") {
2260
- if (input.workerId === undefined) input.workerId = helperId;
2261
- if (input.workerId !== helperId)
2262
- return failure("permission_denied", "writer identity does not match the caller");
2263
- if (workspace.data.writer) {
2264
- if (workspace.data.writer.state === "uncertain")
2265
- return failure("recovery_required", "checkout writer requires recovery", {
2266
- owner: workspace.data.writer.owner,
2267
- });
2268
- const currentOwner = workspace.data.writer.owner;
2269
- if (currentOwner.workerId) {
2270
- const ownerWorker = task.data.workers.find(
2271
- (candidate) => candidate.id === currentOwner.workerId,
2272
- );
2273
- if (ownerWorker?.data.state === "cancelling" || ownerWorker?.data.state === "unknown")
2274
- return failure("recovery_required", "current worker has not stopped", {
2275
- owner: currentOwner,
2276
- });
2277
- }
2278
- // A lead-held writer transfers across lead sessions of the same
2279
- // checkout: restarts end the owning session, and per-write ownership
2280
- // checks keep concurrent sessions from both believing they hold it.
2281
- // Worker-held writers never transfer this way.
2282
- if (currentOwner.workerId !== null || helperId !== null)
2283
- return failure("writer_conflict", "checkout already has a writer", {
2284
- owner: currentOwner,
2285
- });
2286
- }
2287
- if (helperId !== null) {
2288
- const helper = this.helperEntry(task.data);
2289
- if (!helper.ok) return helper;
2290
- if (helper.data.data.assignment.role !== "implementer")
2291
- return failure("permission_denied", "only implementers can own product writes");
2292
- if (helper.data.data.state !== "running" || !helper.data.data.session)
2293
- return failure(
2294
- "permission_denied",
2295
- "writer ownership requires an observed running worker",
2296
- );
2297
- if (!sameSession(helper.data.data.session, this.context))
2298
- return failure("permission_denied", "worker session does not match the caller");
2299
- }
2300
- // Write-timed requirements gate acquisition, not just close: a spec
2301
- // gate is enforced where writes begin, never as advisory prose.
2302
- const gate = this.view(task.data);
2303
- if (gate.ok) {
2304
- const writeGates = (task.data.policy?.requirements ?? []).filter(
2305
- (requirement) =>
2306
- requirement.before === "write" &&
2307
- gate.data.requirements.some(
2308
- (evaluation) =>
2309
- evaluation.requirementId === requirement.id &&
2310
- (evaluation.status === "unsatisfied" || evaluation.status === "unavailable"),
2311
- ),
2312
- );
2313
- if (writeGates.length)
2314
- return failure(
2315
- "requirements_unsatisfied",
2316
- "writer ownership is gated by unsatisfied requirements; record their evidence or an approved limitation waiver first",
2317
- { requirementIds: writeGates.map((requirement) => requirement.id) },
2318
- );
2319
- }
2320
- const owner = {
2321
- taskId: task.data.id,
2322
- workerId: helperId,
2323
- session: {
2324
- kind: "host" as const,
2325
- host: this.context.caller.host,
2326
- handle: this.context.caller.actor,
2327
- },
2328
- };
2329
- const changed = this.store.mutateTaskAndWorkspace({
2330
- taskId: task.data.id,
2331
- expectedRevision: input.expectedRevision,
2332
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
2333
- now: trustedNow(this.context),
2334
- workspace: (current, mutation) =>
2335
- success(mutation.revision, mutation.revision, {
2336
- ...current,
2337
- writer: { state: "held" as const, owner, acquiredAt: mutation.now },
2338
- }),
2339
- task: (current) => success(null, null, current),
2340
- });
2341
- if (!changed.ok) return changed;
2342
- return success(
2343
- changed.data.task.revision,
2344
- changed.data.workspace.revision,
2345
- changed.data.workspace,
2346
- );
2347
- }
2348
- const current = workspace.data.writer;
2349
- if (!current) return failure("permission_denied", "checkout has no writer owner");
2350
- if (current.state === "uncertain")
2351
- return failure("recovery_required", "checkout writer requires recovery", {
2352
- owner: current.owner,
2353
- });
2354
- if (
2355
- current.owner.taskId !== task.data.id ||
2356
- current.owner.workerId !== helperId ||
2357
- !sameSession(current.owner.session, this.context)
2358
- )
2359
- return failure("permission_denied", "only the current writer can release ownership");
2360
- if (helperId !== null) {
2361
- const helper = this.helperEntry(task.data);
2362
- if (!helper.ok) return helper;
2363
- if (helper.data.data.state !== "running")
2364
- return failure("recovery_required", "worker must be observed stopped before release");
2365
- }
2366
- const changed = this.store.mutateTaskAndWorkspace({
2367
- taskId: task.data.id,
2368
- expectedRevision: input.expectedRevision,
2369
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
2370
- now: trustedNow(this.context),
2371
- workspace: (currentWorkspace, mutation) =>
2372
- success(mutation.revision, mutation.revision, { ...currentWorkspace, writer: null }),
2373
- task: (currentTask) => success(null, null, currentTask),
2374
- });
2375
- if (!changed.ok) return changed;
2376
- return success(
2377
- changed.data.task.revision,
2378
- changed.data.workspace.revision,
2379
- changed.data.workspace,
2380
- );
2381
- }
2382
-
2383
- assertProductWriteAllowed(input: {
2384
- task: TaskRecord;
2385
- workspace: WorkspaceRecord;
2386
- paths: string[];
2387
- }) {
2388
- return assertProductWriteAllowed({ ...input, caller: this.callerContext(), store: this.store });
2389
- }
2390
-
2391
- applicableDecision(
2392
- taskId: string,
2393
- purpose: Decision["purpose"],
2394
- binding: Decision["binding"],
2395
- ): Result<Entry<Decision>[]> {
2396
- const root = this.contextRootError();
2397
- if (!root.ok) return root;
2398
- const task = this.store.readTask(taskId);
2399
- if (!task.ok) return task;
2400
- const entries = storedDecisionApplicable(this.store, task.data, purpose, binding);
2401
- return success(task.data.revision, null, entries);
2402
- }
2403
-
2404
- reserveAction(
2405
- input: Omit<
2406
- ReserveActionInput,
2407
- "store" | "native" | "authority" | "authorityOwner" | "authorityCaller"
2408
- > & { observation: unknown },
2409
- ): Result<ActionReservation> {
2410
- const root = this.contextRootError();
2411
- if (!root.ok) return root;
2412
- if ((this.context.workerId ?? null) !== null)
2413
- return failure("permission_denied", "helpers cannot reserve external actions");
2414
- if (input.observation === undefined)
2415
- return failure("invalid_input", "native action observation is required");
2416
- const task = this.store.readTask(input.taskId);
2417
- if (!task.ok) return task;
2418
- const workspace = this.store.readWorkspace();
2419
- if (!workspace.ok) return workspace;
2420
- if (!workspace.data) return failure("not_found", "workspace not found");
2421
- const decision = task.data.decisions.find((entry) => entry.id === input.decisionId);
2422
- if (!decision) return failure("not_found", "decision not found");
2423
- const authority = verifyNativeAction(
2424
- this.context.nativeAuthority,
2425
- {
2426
- observation: input.observation,
2427
- expected: {
2428
- taskId: task.data.id,
2429
- workspaceId: workspace.data.id,
2430
- decisionId: input.decisionId,
2431
- actionRef: input.actionRef,
2432
- taskRevision: input.expectedRevision,
2433
- workspaceRevision: input.expectedWorkspaceRevision,
2434
- outcome: "reserve",
2435
- decision: decision.data,
2436
- },
2437
- caller: this.context.caller,
2438
- },
2439
- { owner: this.authorityOwner, store: this.store, root: this.store.root },
2440
- );
2441
- if (!authority.ok) return authority;
2442
- const operation = decisionOperation(decision.data.binding.approvedContent);
2443
- if (operation) {
2444
- const view = this.view(task.data);
2445
- if (!view.ok) return view;
2446
- const evaluations = new Map(
2447
- view.data.requirements.map((entry) => [entry.requirementId, entry.status]),
2448
- );
2449
- const blocked = (view.data.task.policy?.requirements ?? []).filter(
2450
- (requirement) =>
2451
- requirement.dependentAction === operation &&
2452
- (evaluations.get(requirement.id) === "unsatisfied" ||
2453
- evaluations.get(requirement.id) === "unavailable"),
2454
- );
2455
- if (blocked.length)
2456
- return failure(
2457
- "requirements_unsatisfied",
2458
- `${operation} is gated by unsatisfied requirements; record their evidence or an approved limitation waiver first`,
2459
- { operation, requirementIds: blocked.map((requirement) => requirement.id) },
2460
- );
2461
- }
2462
- return reserveBoundedAction({
2463
- ...input,
2464
- store: this.store,
2465
- authority: authority.data,
2466
- authorityOwner: this.authorityOwner,
2467
- authorityCaller: this.context.caller,
2468
- native: { now: trustedNow(this.context) },
2469
- });
2470
- }
2471
-
2472
- settleAction(
2473
- input: Omit<
2474
- SettleActionInput,
2475
- "store" | "native" | "authority" | "authorityOwner" | "authorityCaller"
2476
- > & { observation: unknown },
2477
- ): Result<Entry<Decision>> {
2478
- const root = this.contextRootError();
2479
- if (!root.ok) return root;
2480
- if ((this.context.workerId ?? null) !== null)
2481
- return failure("permission_denied", "helpers cannot settle external actions");
2482
- if (input.observation === undefined)
2483
- return failure("invalid_input", "native action observation is required");
2484
- const task = this.store.readTask(input.taskId);
2485
- if (!task.ok) return task;
2486
- const workspace = this.store.readWorkspace();
2487
- if (!workspace.ok) return workspace;
2488
- if (!workspace.data) return failure("not_found", "workspace not found");
2489
- const decision = task.data.decisions.find((entry) => entry.id === input.decisionId);
2490
- if (!decision) return failure("not_found", "decision not found");
2491
- const authority = verifyNativeAction(
2492
- this.context.nativeAuthority,
2493
- {
2494
- observation: input.observation,
2495
- expected: {
2496
- taskId: task.data.id,
2497
- workspaceId: workspace.data.id,
2498
- decisionId: input.decisionId,
2499
- actionRef: input.actionRef,
2500
- taskRevision: input.taskRevision,
2501
- workspaceRevision: input.workspaceRevision,
2502
- outcome: input.outcome,
2503
- decision: decision.data,
2504
- },
2505
- caller: this.context.caller,
2506
- },
2507
- { owner: this.authorityOwner, store: this.store, root: this.store.root },
2508
- );
2509
- if (!authority.ok) return authority;
2510
- return settleBoundedAction({
2511
- ...input,
2512
- store: this.store,
2513
- authority: authority.data,
2514
- authorityOwner: this.authorityOwner,
2515
- authorityCaller: this.context.caller,
2516
- native: { now: trustedNow(this.context) },
2517
- });
2518
- }
2519
-
2520
- reconcileAction(
2521
- input: Omit<
2522
- ReconcileActionInput,
2523
- | "store"
2524
- | "native"
2525
- | "authority"
2526
- | "authorityOwner"
2527
- | "authorityCaller"
2528
- | "taskRevision"
2529
- | "workspaceRevision"
2530
- > & {
2531
- expectedRevision: string;
2532
- expectedWorkspaceRevision: string;
2533
- observation: unknown;
2534
- },
2535
- ): Result<Entry<Decision>> {
2536
- const root = this.contextRootError();
2537
- if (!root.ok) return root;
2538
- if ((this.context.workerId ?? null) !== null)
2539
- return failure("permission_denied", "helpers cannot reconcile external actions");
2540
- const task = this.store.readTask(input.taskId);
2541
- if (!task.ok) return task;
2542
- const workspace = this.store.readWorkspace();
2543
- if (!workspace.ok) return workspace;
2544
- if (!workspace.data) return failure("not_found", "workspace not found");
2545
- const decision = task.data.decisions.find((entry) => entry.id === input.decisionId);
2546
- if (!decision) return failure("not_found", "decision not found");
2547
- const authority = verifyNativeReconciliation(
2548
- this.context.nativeAuthority,
2549
- {
2550
- observation: input.observation,
2551
- expected: {
2552
- taskId: task.data.id,
2553
- workspaceId: workspace.data.id,
2554
- decisionId: input.decisionId,
2555
- actionRef: input.actionRef,
2556
- taskRevision: input.expectedRevision,
2557
- workspaceRevision: input.expectedWorkspaceRevision,
2558
- outcome: input.outcome,
2559
- evidenceDigest: input.evidenceDigest,
2560
- ...(input.step ? { step: input.step } : {}),
2561
- decision: decision.data,
2562
- },
2563
- caller: this.context.caller,
2564
- },
2565
- { owner: this.authorityOwner, store: this.store, root: this.store.root },
2566
- );
2567
- if (!authority.ok) return authority;
2568
- return reconcileBoundedAction({
2569
- ...input,
2570
- taskRevision: input.expectedRevision,
2571
- workspaceRevision: input.expectedWorkspaceRevision,
2572
- store: this.store,
2573
- authority: authority.data,
2574
- authorityOwner: this.authorityOwner,
2575
- authorityCaller: this.context.caller,
2576
- native: { now: trustedNow(this.context) },
2577
- });
2578
- }
2579
-
2580
1768
  private resolve(task: TaskRecord, assessment: Assessment): Result<Policy> {
2581
1769
  return resolvePolicy({
2582
1770
  intent: task.intent.data,
@@ -2603,8 +1791,7 @@ export class WorkitCore {
2603
1791
  ? success(null, null, historical)
2604
1792
  : captureCandidate(this.store.root, task.intent.data.scope, environment());
2605
1793
  if (!current.ok) return current;
2606
- const evaluationWorkspace =
2607
- task.status === "closed" ? { ...workspace.data, writer: null } : workspace.data;
1794
+ const evaluationWorkspace = workspace.data;
2608
1795
  const tree = treeFreshness(this.store.root);
2609
1796
  const requirements = evaluateRequirements(
2610
1797
  task,
@@ -2628,7 +1815,6 @@ export class WorkitCore {
2628
1815
  progress: task.progress,
2629
1816
  policy: task.policy,
2630
1817
  requirements,
2631
- writer: evaluationWorkspace.writer,
2632
1818
  });
2633
1819
  }
2634
1820
 
@@ -2666,8 +1852,7 @@ export class WorkitCore {
2666
1852
  ? captureCandidate(this.store.root, task.intent.data.scope, environment())
2667
1853
  : success<Candidate | null>(null, null, task.candidates.at(-1) ?? null);
2668
1854
  if (!current.ok) return current;
2669
- const evaluationWorkspace =
2670
- task.status === "closed" ? { ...workspace.data, writer: null } : workspace.data;
1855
+ const evaluationWorkspace = workspace.data;
2671
1856
  const tree = freshness;
2672
1857
  const requirements = evaluateRequirements(
2673
1858
  task,
@@ -2833,16 +2018,9 @@ export class WorkitCore {
2833
2018
  )
2834
2019
  )
2835
2020
  return failure("recovery_required", "imported task requires resume reconciliation");
2836
- if (!validResumeApproval(task.data, workspace.data, input.authorityRefs ?? [], this.context))
2837
- return failure("permission_denied", "imported resume requires native destination approval");
2838
2021
  resumeCandidate = current.data.candidate;
2839
2022
  }
2840
- const blocker =
2841
- status === "paused"
2842
- ? workspace.data.writer
2843
- ? failure("recovery_required", "writer ownership must be released first")
2844
- : null
2845
- : this.activeWorkerBlocker(task.data, workspace.data);
2023
+ const blocker = status === "paused" ? null : this.activeWorkerBlocker(task.data);
2846
2024
  if (blocker) return blocker;
2847
2025
  let pauseCandidate: import("./task-contract").Candidate | null = null;
2848
2026
  if (status === "paused") {
@@ -2909,7 +2087,7 @@ export class WorkitCore {
2909
2087
  const workspace = this.store.readWorkspace();
2910
2088
  if (!workspace.ok) return workspace;
2911
2089
  if (!workspace.data) return failure("not_found", "workspace not found");
2912
- const revisionBlocker = this.activeWorkerBlocker(task.data, workspace.data);
2090
+ const revisionBlocker = this.activeWorkerBlocker(task.data);
2913
2091
  if (revisionBlocker) return revisionBlocker;
2914
2092
  this.fillRevisions(input, task.data, workspace.data);
2915
2093
  const changed = this.store.mutateTaskAndWorkspace({
@@ -2945,7 +2123,7 @@ export class WorkitCore {
2945
2123
  const workspace = this.store.readWorkspace();
2946
2124
  if (!workspace.ok) return workspace;
2947
2125
  if (!workspace.data) return failure("not_found", "workspace not found");
2948
- const blocker = this.activeWorkerBlocker(task.data, workspace.data);
2126
+ const blocker = this.activeWorkerBlocker(task.data);
2949
2127
  if (blocker) return blocker;
2950
2128
  this.fillRevisions(input, task.data, workspace.data);
2951
2129
  const current = captureCandidate(this.store.root, task.data.intent.data.scope, environment());