@brainervirus/workit-core 0.11.0 → 1.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 (150) hide show
  1. package/README.md +5 -3
  2. package/package.json +8 -3
  3. package/scripts/analyze-release-scope.ts +4 -1
  4. package/scripts/doctor-check.ts +3 -1
  5. package/scripts/install-codex-plugin.sh +28 -0
  6. package/scripts/install-cursor-plugin.sh +1 -0
  7. package/scripts/install-opencode-plugin.sh +1 -0
  8. package/scripts/install-pi-package.sh +56 -0
  9. package/scripts/rewrite-workspace-deps.ts +22 -5
  10. package/scripts/sync-release-manifests.ts +8 -5
  11. package/scripts/sync-runtime.sh +7 -3
  12. package/scripts/validate-cursor-marketplace.ts +16 -62
  13. package/skills/workit-babysit/SKILL.md +33 -0
  14. package/skills/workit-behavioral-tdd/SKILL.md +53 -0
  15. package/skills/workit-blast-radius/SKILL.md +31 -0
  16. package/skills/workit-challenge/SKILL.md +62 -0
  17. package/skills/workit-debug/SKILL.md +61 -0
  18. package/skills/workit-deslop/SKILL.md +36 -0
  19. package/skills/workit-diagram/SKILL.md +32 -0
  20. package/skills/workit-green-run/SKILL.md +29 -0
  21. package/skills/workit-handoff/SKILL.md +43 -0
  22. package/skills/workit-implement/SKILL.md +46 -0
  23. package/skills/workit-mockup/SKILL.md +28 -0
  24. package/skills/workit-plan/SKILL.md +66 -0
  25. package/skills/workit-review/SKILL.md +60 -0
  26. package/skills/workit-steer/SKILL.md +32 -0
  27. package/src/core/authority.ts +918 -0
  28. package/src/core/boundary.ts +29 -0
  29. package/src/core/branch.ts +26 -255
  30. package/src/core/changelog.ts +95 -18
  31. package/src/core/commit-flavors.ts +65 -0
  32. package/src/core/config-conversion.ts +183 -0
  33. package/src/core/config.ts +42 -0
  34. package/src/core/cutover.ts +721 -0
  35. package/src/core/detect-hosts.ts +107 -0
  36. package/src/core/doctor.ts +428 -18
  37. package/src/core/external-action-effects.ts +1171 -0
  38. package/src/core/external-action.ts +541 -0
  39. package/src/core/init.ts +83 -32
  40. package/src/core/methods.ts +125 -0
  41. package/src/core/policy-resolver.ts +523 -0
  42. package/src/core/pr-create.ts +7 -1
  43. package/src/core/registration.ts +69 -4
  44. package/src/core/repo-context.ts +59 -1
  45. package/src/core/setup.ts +28 -0
  46. package/src/core/skill-manifests.ts +40 -42
  47. package/src/core/support-matrix.ts +8 -3
  48. package/src/core/sync-runtime.ts +15 -3
  49. package/src/core/task-context.ts +179 -0
  50. package/src/core/task-contract.ts +1136 -0
  51. package/src/core/task-engine.ts +2562 -0
  52. package/src/core/task-evaluation.ts +679 -0
  53. package/src/core/task-store.ts +1071 -0
  54. package/src/core/tracker-issues.ts +237 -0
  55. package/src/core/uninstall.ts +67 -3
  56. package/src/core/vcs-config.ts +53 -22
  57. package/src/core/workers.ts +166 -0
  58. package/src/core/workspaces.ts +3 -1
  59. package/src/core/youtrack-tools.ts +2 -0
  60. package/src/core/youtrack.ts +145 -10
  61. package/src/core.ts +126 -0
  62. package/templates/execution-contract.md +17 -50
  63. package/templates/plan-template.md +3 -3
  64. package/templates/spec-template.md +15 -3
  65. package/templates/workit-contract.md +12 -0
  66. package/commands/wk-changelog.md +0 -2
  67. package/commands/wk-commit.md +0 -2
  68. package/commands/wk-docs-refresh.md +0 -2
  69. package/commands/wk-handoff.md +0 -2
  70. package/commands/wk-implement.md +0 -2
  71. package/commands/wk-init.md +0 -2
  72. package/commands/wk-issue-update.md +0 -2
  73. package/commands/wk-meetings.md +0 -2
  74. package/commands/wk-pr.md +0 -2
  75. package/commands/wk-release-notes.md +0 -2
  76. package/commands/wk-status.md +0 -2
  77. package/commands/wk-verify.md +0 -2
  78. package/scripts/update-superpowers.sh +0 -82
  79. package/scripts/vendor-assets.ts +0 -37
  80. package/skills/wk-changelog/SKILL.md +0 -15
  81. package/skills/wk-commit/SKILL.md +0 -16
  82. package/skills/wk-docs-refresh/SKILL.md +0 -15
  83. package/skills/wk-handoff/SKILL.md +0 -20
  84. package/skills/wk-implement/SKILL.md +0 -47
  85. package/skills/wk-init/SKILL.md +0 -31
  86. package/skills/wk-issue-update/SKILL.md +0 -27
  87. package/skills/wk-issue-update/references/youtrack-update-style.md +0 -81
  88. package/skills/wk-meetings/SKILL.md +0 -17
  89. package/skills/wk-pr/SKILL.md +0 -27
  90. package/skills/wk-release-notes/SKILL.md +0 -15
  91. package/skills/wk-status/SKILL.md +0 -16
  92. package/skills/wk-verify/SKILL.md +0 -16
  93. package/src/core/detector.ts +0 -239
  94. package/src/core/flow-state.ts +0 -3308
  95. package/src/core/handoff-context.ts +0 -136
  96. package/src/core/handoff-tools.ts +0 -133
  97. package/src/core/menu.ts +0 -70
  98. package/src/core/plan-tasks.ts +0 -43
  99. package/src/core/reminder.ts +0 -124
  100. package/src/core/sdd.ts +0 -353
  101. package/src/state.ts +0 -22
  102. package/templates/superpowers-doc-contract.md +0 -75
  103. package/vendor/superpowers/skills/brainstorming/SKILL.md +0 -159
  104. package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +0 -213
  105. package/vendor/superpowers/skills/brainstorming/scripts/helper.js +0 -167
  106. package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +0 -723
  107. package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +0 -209
  108. package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +0 -120
  109. package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
  110. package/vendor/superpowers/skills/brainstorming/visual-companion.md +0 -291
  111. package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +0 -185
  112. package/vendor/superpowers/skills/executing-plans/SKILL.md +0 -70
  113. package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +0 -241
  114. package/vendor/superpowers/skills/receiving-code-review/SKILL.md +0 -213
  115. package/vendor/superpowers/skills/requesting-code-review/SKILL.md +0 -103
  116. package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +0 -172
  117. package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +0 -428
  118. package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +0 -139
  119. package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +0 -44
  120. package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +0 -22
  121. package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +0 -40
  122. package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +0 -188
  123. package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +0 -119
  124. package/vendor/superpowers/skills/systematic-debugging/SKILL.md +0 -296
  125. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
  126. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +0 -115
  127. package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +0 -122
  128. package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +0 -63
  129. package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +0 -169
  130. package/vendor/superpowers/skills/systematic-debugging/test-academic.md +0 -14
  131. package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +0 -58
  132. package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +0 -68
  133. package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +0 -69
  134. package/vendor/superpowers/skills/test-driven-development/SKILL.md +0 -371
  135. package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +0 -299
  136. package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +0 -202
  137. package/vendor/superpowers/skills/using-superpowers/SKILL.md +0 -62
  138. package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +0 -23
  139. package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +0 -39
  140. package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +0 -16
  141. package/vendor/superpowers/skills/verification-before-completion/SKILL.md +0 -139
  142. package/vendor/superpowers/skills/writing-plans/SKILL.md +0 -174
  143. package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
  144. package/vendor/superpowers/skills/writing-skills/SKILL.md +0 -689
  145. package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +0 -1150
  146. package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -189
  147. package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +0 -172
  148. package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +0 -187
  149. package/vendor/superpowers/skills/writing-skills/render-graphs.js +0 -168
  150. package/vendor/superpowers/skills/writing-skills/testing-skills-with-subagents.md +0 -384
@@ -0,0 +1,2562 @@
1
+ import { realpathSync } from "node:fs";
2
+ import {
3
+ POLICY_VERSION,
4
+ canonicalJson,
5
+ failure,
6
+ decisionDigest,
7
+ exportBundleSchema,
8
+ newId,
9
+ parseOperation,
10
+ success,
11
+ type Assessment,
12
+ type Capability,
13
+ type Caller,
14
+ type Constraint,
15
+ type Decision,
16
+ type Entry,
17
+ type Evidence,
18
+ type ExportBundle,
19
+ type Finding,
20
+ type Policy,
21
+ provenanceSchema,
22
+ type Ref,
23
+ type Result,
24
+ type Scope,
25
+ type TaskRecord,
26
+ type TaskSummary,
27
+ type TaskView,
28
+ type Utc,
29
+ type Provenance,
30
+ type Worker,
31
+ type WorkspaceRecord,
32
+ taskRecordSchema,
33
+ } from "./task-contract";
34
+ import {
35
+ bindingCovers,
36
+ applicableDecision,
37
+ storedDecisionApplicable,
38
+ reserveAction as reserveBoundedAction,
39
+ settleAction as settleBoundedAction,
40
+ reconcileAction as reconcileBoundedAction,
41
+ verifyNativeAction,
42
+ verifyNativeReconciliation,
43
+ verifyNativeDecision,
44
+ verifyDecisionContent,
45
+ retireNativeAuthority,
46
+ type ActionReservation,
47
+ type NativeAuthorityVerifier,
48
+ type ReserveActionInput,
49
+ type SettleActionInput,
50
+ type ReconcileActionInput,
51
+ } from "./authority";
52
+ import { diffPolicy, resolvePolicy } from "./policy-resolver";
53
+ import { TaskStore, type MetadataLock, type ProcessEvidence } from "./task-store";
54
+ import {
55
+ exportDigest,
56
+ reconcileResume as reconcileResumeContext,
57
+ type ResumeReconciliation,
58
+ } from "./task-context";
59
+ import {
60
+ assertProductWriteAllowed,
61
+ type CallerContext,
62
+ type NativeWorkerObservation,
63
+ type NativeWorkerVerification,
64
+ type NativeWorkerVerifier,
65
+ type WorkerDispatch,
66
+ type WorkerDispatchCommit,
67
+ type WorkerDispatchRequest,
68
+ type WorkerDispatchStage,
69
+ } from "./workers";
70
+ import {
71
+ captureCandidate,
72
+ evaluateClosure,
73
+ evaluateEvidence,
74
+ evaluateRequirements,
75
+ type CandidateEnvironment,
76
+ } from "./task-evaluation";
77
+
78
+ export type OperationContext = {
79
+ root: string;
80
+ caller: Caller;
81
+ /** Whether the host supplied a trusted per-session caller identity. */
82
+ callerAttested?: boolean;
83
+ provenanceKind?: "host_observed" | "agent_reported";
84
+ capabilities: Capability[];
85
+ constraints: Constraint[];
86
+ now: Utc | (() => Utc);
87
+ nativeAuthority?: NativeAuthorityVerifier;
88
+ nativeWorker?: NativeWorkerVerifier;
89
+ nativeRecovery?: (input: {
90
+ lock: MetadataLock | null;
91
+ writer: WorkspaceRecord["writer"];
92
+ reason: string;
93
+ authorityRefs: Ref[];
94
+ }) => Result<ProcessEvidence>;
95
+ workerId?: string | null;
96
+ };
97
+
98
+ const provenance = (
99
+ context: OperationContext,
100
+ kind: "host_observed" | "agent_reported" = context.provenanceKind ?? "host_observed",
101
+ ) => ({
102
+ kind,
103
+ host: context.caller.host,
104
+ session: { kind: "host" as const, host: context.caller.host, handle: context.caller.actor },
105
+ workerId: context.workerId ?? null,
106
+ receipts: [],
107
+ });
108
+ const environment = (): CandidateEnvironment => [];
109
+ /**
110
+ * The operation a decision authorizes: canonical descriptors carry it as JSON,
111
+ * legacy single-operation approvals are the bare operation string.
112
+ */
113
+ const decisionOperation = (approvedContent: string): string | null => {
114
+ try {
115
+ const parsed = JSON.parse(approvedContent) as { operation?: unknown };
116
+ if (typeof parsed?.operation === "string") return parsed.operation;
117
+ } catch {
118
+ // a bare operation string is not JSON
119
+ }
120
+ return /^[a-z][a-z_]*\.[a-z_]+$/.test(approvedContent) ? approvedContent : null;
121
+ };
122
+ const sameSession = (value: unknown, context: OperationContext): boolean =>
123
+ typeof value === "object" &&
124
+ value !== null &&
125
+ (value as any).kind === "host" &&
126
+ (value as any).host === context.caller.host &&
127
+ (value as any).handle === context.caller.actor;
128
+
129
+ type WorkerBinding = {
130
+ owner: object;
131
+ store: TaskStore;
132
+ root: string;
133
+ caller: Caller;
134
+ };
135
+ type WorkerAuthority = {
136
+ taskId: string;
137
+ workspaceId: string;
138
+ workerId: string;
139
+ expectedRevision: string;
140
+ expectedWorkspaceRevision: string;
141
+ state: NativeWorkerObservation["state"];
142
+ session: NativeWorkerObservation["session"];
143
+ provenance: Provenance;
144
+ owner: object;
145
+ store: TaskStore;
146
+ root: string;
147
+ caller: Caller;
148
+ };
149
+ type VerifiedWorker = object;
150
+ const verifiedWorkers = new WeakMap<object, WorkerAuthority>();
151
+
152
+ type DispatchReservation = {
153
+ owner: object;
154
+ store: TaskStore;
155
+ root: string;
156
+ caller: Caller;
157
+ taskId: string;
158
+ workspaceId: string;
159
+ workerId: string;
160
+ };
161
+ /**
162
+ * Live launch reservations. Only this module can create one, only the adapter that
163
+ * received it holds it, and a process restart loses it: a worker left `assigned`
164
+ * with no session and no live reservation can never be resolved by inference.
165
+ */
166
+ const dispatchReservations = new WeakMap<object, DispatchReservation>();
167
+
168
+ const sameValue = (left: unknown, right: unknown): boolean => {
169
+ try {
170
+ return canonicalJson(left) === canonicalJson(right);
171
+ } catch {
172
+ return false;
173
+ }
174
+ };
175
+
176
+ const validNativeProvenance = (
177
+ value: unknown,
178
+ caller: Caller,
179
+ expected: Pick<NativeWorkerObservation, "workerId" | "session">,
180
+ ): value is Provenance => {
181
+ if (!provenanceSchema.safeParse(value).success) return false;
182
+ const provenance = value as Provenance;
183
+ return (
184
+ provenance.kind === "host_observed" &&
185
+ provenance.host === caller.host &&
186
+ provenance.workerId === expected.workerId &&
187
+ sameValue(provenance.session, expected.session) &&
188
+ provenance.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host)
189
+ );
190
+ };
191
+
192
+ /** A dispatch has no worker session yet, so it is bound to the attesting host session. */
193
+ const validDispatchProvenance = (
194
+ value: unknown,
195
+ caller: Caller,
196
+ workerId: string,
197
+ ): value is Provenance => {
198
+ if (!provenanceSchema.safeParse(value).success) return false;
199
+ const provenance = value as Provenance;
200
+ return (
201
+ provenance.kind === "host_observed" &&
202
+ provenance.host === caller.host &&
203
+ provenance.workerId === workerId &&
204
+ provenance.session?.kind === "host" &&
205
+ provenance.session.host === caller.host &&
206
+ provenance.session.handle === caller.actor &&
207
+ provenance.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host)
208
+ );
209
+ };
210
+
211
+ const validResumeApproval = (
212
+ task: TaskRecord,
213
+ workspace: WorkspaceRecord,
214
+ authorityRefs: Ref[],
215
+ context: OperationContext,
216
+ ): boolean =>
217
+ authorityRefs.some((ref) => {
218
+ if (ref.kind !== "record" || ref.collection !== "decisions") return false;
219
+ const entry = task.decisions.find((candidate) => candidate.id === ref.id);
220
+ if (!entry || entry.provenance.kind !== "host_observed") return false;
221
+ if (
222
+ entry.provenance.host !== context.caller.host ||
223
+ !sameSession(entry.provenance.session, context) ||
224
+ !entry.provenance.receipts.some(
225
+ (receipt) => receipt.kind === "host" && receipt.host === context.caller.host,
226
+ )
227
+ )
228
+ return false;
229
+ const decision = entry.data;
230
+ if (
231
+ !["design", "action"].includes(decision.purpose) ||
232
+ decision.response !== "approved" ||
233
+ decision.revoked !== null ||
234
+ decision.binding.approvedContent !== "resume" ||
235
+ decision.binding.taskId !== task.id ||
236
+ decision.binding.workspaceId !== workspace.id ||
237
+ canonicalJson(decision.binding.scope) !== canonicalJson(task.intent.data.scope)
238
+ )
239
+ return false;
240
+ return (["design", "action"] as const).some((purpose) =>
241
+ applicableDecision(task, purpose, decision.binding, workspace.root).some(
242
+ (candidate) => candidate.digest === decision.digest,
243
+ ),
244
+ );
245
+ });
246
+
247
+ const verifyNativeWorker = (
248
+ verifier: NativeWorkerVerifier | undefined,
249
+ input: NativeWorkerVerification,
250
+ binding: WorkerBinding,
251
+ ): Result<VerifiedWorker> => {
252
+ if (!verifier) return failure("permission_denied", "native worker verifier is unavailable");
253
+ let result: Result<Provenance>;
254
+ try {
255
+ result = verifier.verifyWorker(input);
256
+ } catch (error) {
257
+ return failure("permission_denied", `native worker observation failed: ${String(error)}`);
258
+ }
259
+ const expected = input.expected;
260
+ if (!result.ok || !validNativeProvenance(result.data, input.caller, expected))
261
+ return failure("permission_denied", "native worker observation was not attested");
262
+ const token = {};
263
+ verifiedWorkers.set(token, {
264
+ taskId: expected.taskId,
265
+ workspaceId: expected.workspaceId,
266
+ workerId: expected.workerId,
267
+ expectedRevision: expected.expectedRevision,
268
+ expectedWorkspaceRevision: expected.expectedWorkspaceRevision,
269
+ state: expected.state,
270
+ session: expected.session,
271
+ provenance: result.data,
272
+ ...binding,
273
+ });
274
+ return success(null, null, token);
275
+ };
276
+
277
+ const takeWorker = (
278
+ token: VerifiedWorker,
279
+ binding: WorkerBinding,
280
+ expected: NativeWorkerObservation,
281
+ ): WorkerAuthority | null => {
282
+ const value = verifiedWorkers.get(token);
283
+ verifiedWorkers.delete(token);
284
+ if (!value) return null;
285
+ if (
286
+ value.owner !== binding.owner ||
287
+ value.store !== binding.store ||
288
+ value.root !== binding.root ||
289
+ value.root !== value.store.root ||
290
+ !sameValue(value.caller, binding.caller) ||
291
+ value.taskId !== expected.taskId ||
292
+ value.workerId !== expected.workerId ||
293
+ value.expectedRevision !== expected.expectedRevision ||
294
+ value.expectedWorkspaceRevision !== expected.expectedWorkspaceRevision ||
295
+ value.state !== expected.state ||
296
+ !sameValue(value.session, expected.session)
297
+ )
298
+ return null;
299
+ return value;
300
+ };
301
+
302
+ type ObserveWorkerLifecycleInput = NativeWorkerObservation & {
303
+ store: TaskStore;
304
+ authority: VerifiedWorker;
305
+ authorityOwner: object;
306
+ caller: Caller;
307
+ now?: Utc;
308
+ /** Set only by commitWorkerDispatch for a reservation that produced no child. */
309
+ dispatch?: boolean;
310
+ };
311
+
312
+ const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<Worker>> => {
313
+ if (!input.store || !input.authority || !input.authorityOwner)
314
+ return failure("invalid_input", "native worker observation preconditions are required");
315
+ const authority = takeWorker(
316
+ input.authority,
317
+ {
318
+ owner: input.authorityOwner,
319
+ store: input.store,
320
+ root: input.store.root,
321
+ caller: input.caller,
322
+ },
323
+ input,
324
+ );
325
+ if (!authority) return failure("permission_denied", "native worker observation is not verified");
326
+
327
+ const task = input.store.readTask(input.taskId);
328
+ if (!task.ok) return task as Result<never>;
329
+ const workspace = input.store.readWorkspace();
330
+ if (!workspace.ok) return workspace as Result<never>;
331
+ if (!workspace.data) return failure("not_found", "workspace not found");
332
+ if (
333
+ task.data.workspaceId !== authority.workspaceId ||
334
+ workspace.data.id !== authority.workspaceId
335
+ )
336
+ return failure("recovery_required", "worker workspace binding is invalid");
337
+ const entry = task.data.workers.find((candidate) => candidate.id === input.workerId);
338
+ if (!entry) return failure("not_found", "worker not found");
339
+ if (task.data.status === "closed")
340
+ return failure("invalid_transition", "closed task cannot observe workers");
341
+ if (task.data.status === "paused" && (input.state === "running" || input.state === "cancelling"))
342
+ return failure("invalid_transition", "paused task cannot run a worker");
343
+ const notStarted = input.dispatch === true;
344
+ if (notStarted && (input.state !== "stopped" || input.session !== null))
345
+ return failure("invalid_input", "a never-dispatched worker stops with no session");
346
+ if (notStarted && (entry.data.state === "stopped" || entry.data.state === "unknown"))
347
+ return failure("invalid_transition", "worker is no longer awaiting dispatch");
348
+ if (notStarted && (entry.data.session !== null || entry.data.state === "running"))
349
+ return failure("invalid_transition", "worker already has an observed session");
350
+ if (
351
+ (input.state === "running" ||
352
+ input.state === "cancelling" ||
353
+ (input.state === "stopped" && !notStarted)) &&
354
+ !input.session
355
+ )
356
+ return failure("invalid_input", "running worker observations require a session");
357
+ if (entry.data.session && !sameValue(entry.data.session, input.session))
358
+ return failure("permission_denied", "worker session does not match its assignment");
359
+ if (input.state !== "running" && entry.data.state === "assigned" && input.state !== "stopped")
360
+ return failure("invalid_transition", "worker has not started");
361
+ if (input.state === "running" && entry.data.state === "stopped")
362
+ return failure("invalid_transition", "stopped worker cannot run again");
363
+ const nextEntry = {
364
+ ...entry,
365
+ recordedAt: input.now ?? entry.recordedAt,
366
+ provenance: authority.provenance,
367
+ data: { ...entry.data, state: input.state, session: input.session },
368
+ } satisfies Entry<Worker>;
369
+ const shouldClear =
370
+ input.state === "stopped" &&
371
+ workspace.data.writer !== null &&
372
+ workspace.data.writer.owner.taskId === task.data.id &&
373
+ workspace.data.writer.owner.workerId === input.workerId;
374
+ const shouldUncertain =
375
+ input.state === "unknown" &&
376
+ workspace.data.writer !== null &&
377
+ workspace.data.writer.owner.taskId === task.data.id &&
378
+ workspace.data.writer.owner.workerId === input.workerId;
379
+ // A no-op observation (same state, same session, no writer side-effect)
380
+ // must not rewrite the entry: hosts observe on every session event, and a
381
+ // revision bump per event livelocks worker sessions — each call would
382
+ // invalidate the revision the previous call returned.
383
+ if (
384
+ entry.data.state === input.state &&
385
+ sameValue(entry.data.session, input.session) &&
386
+ !shouldClear &&
387
+ !shouldUncertain
388
+ )
389
+ return success(task.data.revision, workspace.data.revision, entry);
390
+ const changed = input.store.mutateTaskAndWorkspace({
391
+ taskId: task.data.id,
392
+ expectedTaskRevision: input.expectedRevision,
393
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
394
+ now: input.now,
395
+ workspace: (current, mutation) =>
396
+ success(mutation.revision, mutation.revision, {
397
+ ...current,
398
+ writer: shouldClear
399
+ ? null
400
+ : shouldUncertain && current.writer
401
+ ? { ...current.writer, state: "uncertain" as const }
402
+ : current.writer,
403
+ }),
404
+ task: (current, mutation) =>
405
+ success(mutation.revision, null, {
406
+ ...current,
407
+ workers: current.workers.map((candidate) =>
408
+ candidate.id === nextEntry.id ? { ...nextEntry, recordedAt: mutation.now } : candidate,
409
+ ),
410
+ }),
411
+ });
412
+ if (!changed.ok) return changed as Result<never>;
413
+ const updated = changed.data.task.workers.find((candidate) => candidate.id === input.workerId);
414
+ return updated
415
+ ? success(changed.data.task.revision, changed.data.workspace.revision, updated)
416
+ : failure("recovery_required", "worker disappeared during lifecycle observation");
417
+ };
418
+
419
+ const refsWithinScope = (refs: Ref[], scope: Scope): boolean =>
420
+ refs.every(
421
+ (ref) =>
422
+ ref.kind !== "file" ||
423
+ bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }),
424
+ );
425
+
426
+ const trustedNow = (context: OperationContext): Utc =>
427
+ typeof context.now === "function" ? context.now() : context.now;
428
+
429
+ const mapRef = (ref: Ref, ids: Map<string, string>): Ref => {
430
+ if (ref.kind !== "record") return ref;
431
+ return { ...ref, id: ids.get(ref.id) ?? ref.id };
432
+ };
433
+
434
+ const portableRefs = (refs: Ref[]): Ref[] =>
435
+ refs.filter((ref) => ref.kind === "file" || ref.kind === "record");
436
+ const portableRef = (ref: Ref | null): Ref | null =>
437
+ ref && (ref.kind === "file" || ref.kind === "record") ? ref : null;
438
+ const portableFact = (fact: Assessment["facts"][number]) => {
439
+ const refs = portableRefs(fact.refs);
440
+ return fact.basis === "observed" && refs.length === 0
441
+ ? { ...fact, basis: "unknown" as const, refs }
442
+ : { ...fact, refs };
443
+ };
444
+ const portableSignal = (signal: Assessment["signals"][keyof Assessment["signals"]]) => {
445
+ const refs = portableRefs(signal.refs);
446
+ return signal.basis === "observed" && refs.length === 0
447
+ ? {
448
+ ...signal,
449
+ value: "unknown" as const,
450
+ basis: "unknown" as const,
451
+ reason: "portable supporting reference was removed",
452
+ refs,
453
+ }
454
+ : { ...signal, refs };
455
+ };
456
+
457
+ const importedProvenance = (context: OperationContext): Provenance => ({
458
+ kind: "imported",
459
+ host: context.caller.host,
460
+ session: null,
461
+ workerId: null,
462
+ receipts: [],
463
+ });
464
+
465
+ const portableTask = (task: TaskRecord): TaskRecord => {
466
+ const clone = structuredClone(task);
467
+ const portableProvenance = (value: Provenance): Provenance => ({
468
+ ...value,
469
+ session: null,
470
+ receipts: [],
471
+ });
472
+ const entries = [
473
+ clone.intent,
474
+ ...clone.assessments,
475
+ ...clone.evidence,
476
+ ...clone.decisions,
477
+ ...clone.findings,
478
+ ...clone.workers,
479
+ ];
480
+ for (const entry of entries) entry.provenance = portableProvenance(entry.provenance);
481
+ for (const worker of clone.workers) worker.data.session = null;
482
+ clone.intent = {
483
+ ...clone.intent,
484
+ data: { ...clone.intent.data, authorityRefs: portableRefs(clone.intent.data.authorityRefs) },
485
+ };
486
+ clone.constraints = clone.constraints.flatMap((constraint) => {
487
+ const source = portableRef(constraint.source);
488
+ return source
489
+ ? [
490
+ {
491
+ ...constraint,
492
+ source,
493
+ requires: constraint.requires.map((requirement) => ({
494
+ ...requirement,
495
+ refs: portableRefs(requirement.refs),
496
+ })),
497
+ },
498
+ ]
499
+ : [];
500
+ });
501
+ clone.assessments = clone.assessments.map((entry) => ({
502
+ ...entry,
503
+ data: {
504
+ ...entry.data,
505
+ facts: entry.data.facts.map(portableFact),
506
+ signals: Object.fromEntries(
507
+ Object.entries(entry.data.signals).map(([name, signal]) => [name, portableSignal(signal)]),
508
+ ) as typeof entry.data.signals,
509
+ consequences: entry.data.consequences.map((consequence) => ({
510
+ ...consequence,
511
+ fact: portableFact(consequence.fact),
512
+ })),
513
+ verification: entry.data.verification.map((verification) => ({
514
+ ...verification,
515
+ availableChecks: portableRefs(verification.availableChecks),
516
+ })),
517
+ },
518
+ }));
519
+ clone.progress = {
520
+ ...clone.progress,
521
+ blockers: clone.progress.blockers.map((blocker) => ({
522
+ ...blocker,
523
+ refs: portableRefs(blocker.refs),
524
+ })),
525
+ };
526
+ clone.evidence = clone.evidence.map((entry) => ({
527
+ ...entry,
528
+ data: {
529
+ ...entry.data,
530
+ refs: portableRefs(entry.data.refs),
531
+ reviewContext: portableRef(entry.data.reviewContext),
532
+ },
533
+ }));
534
+ clone.decisions = clone.decisions.map((entry) => ({
535
+ ...entry,
536
+ data: {
537
+ ...entry.data,
538
+ consumption: entry.data.consumption
539
+ ? portableRef(entry.data.consumption.actionRef)
540
+ ? {
541
+ ...entry.data.consumption,
542
+ actionRef: portableRef(entry.data.consumption.actionRef)!,
543
+ }
544
+ : null
545
+ : null,
546
+ binding: {
547
+ ...entry.data.binding,
548
+ contentRefs: portableRefs(entry.data.binding.contentRefs),
549
+ },
550
+ },
551
+ }));
552
+ clone.findings = clone.findings.map((entry) => ({
553
+ ...entry,
554
+ data: { ...entry.data, refs: portableRefs(entry.data.refs) },
555
+ }));
556
+ // Candidate metadata can contain environment-derived paths and digests. The destination
557
+ // must recapture its own candidate instead of receiving source checkout material.
558
+ clone.candidates = [];
559
+ return clone;
560
+ };
561
+
562
+ const importedTask = (
563
+ source: TaskRecord,
564
+ bundle: ExportBundle,
565
+ destinationWorkspaceId: string,
566
+ context: OperationContext,
567
+ timestamp: Utc,
568
+ ): TaskRecord => {
569
+ const ids = new Map<string, string>();
570
+ const allocate = (id: string) => {
571
+ const existing = ids.get(id);
572
+ if (existing) return existing;
573
+ const value = newId();
574
+ ids.set(id, value);
575
+ return value;
576
+ };
577
+ for (const entry of [
578
+ source.intent,
579
+ ...source.assessments,
580
+ ...source.evidence,
581
+ ...source.decisions,
582
+ ...source.findings,
583
+ ...source.workers,
584
+ ])
585
+ allocate(entry.id);
586
+ const fresh = (id: string) => allocate(id);
587
+ const provenance = importedProvenance(context);
588
+ const intent = {
589
+ ...source.intent,
590
+ id: fresh(source.intent.id),
591
+ recordedAt: timestamp,
592
+ provenance,
593
+ data: {
594
+ ...source.intent.data,
595
+ authorityRefs: source.intent.data.authorityRefs.map((ref) => mapRef(ref, ids)),
596
+ },
597
+ };
598
+ const assessments = source.assessments.map((entry) => {
599
+ const data = entry.data;
600
+ return {
601
+ ...entry,
602
+ id: fresh(entry.id),
603
+ recordedAt: timestamp,
604
+ provenance,
605
+ data: {
606
+ ...data,
607
+ facts: data.facts.map((fact) => ({
608
+ ...fact,
609
+ refs: fact.refs.map((ref) => mapRef(ref, ids)),
610
+ })),
611
+ signals: Object.fromEntries(
612
+ Object.entries(data.signals).map(([name, signal]) => [
613
+ name,
614
+ { ...signal, refs: signal.refs.map((ref) => mapRef(ref, ids)) },
615
+ ]),
616
+ ) as typeof data.signals,
617
+ consequences: data.consequences.map((consequence) => ({
618
+ ...consequence,
619
+ fact: {
620
+ ...consequence.fact,
621
+ refs: consequence.fact.refs.map((ref) => mapRef(ref, ids)),
622
+ },
623
+ })),
624
+ verification: data.verification.map((verification) => ({
625
+ ...verification,
626
+ availableChecks: verification.availableChecks.map((ref) => mapRef(ref, ids)),
627
+ })),
628
+ },
629
+ };
630
+ });
631
+ const evidence = source.evidence.map((entry) => ({
632
+ ...entry,
633
+ id: fresh(entry.id),
634
+ recordedAt: timestamp,
635
+ provenance,
636
+ data: {
637
+ ...entry.data,
638
+ refs: entry.data.refs.map((ref) => mapRef(ref, ids)),
639
+ reviewContext: entry.data.reviewContext ? mapRef(entry.data.reviewContext, ids) : null,
640
+ },
641
+ }));
642
+ const decisions = source.decisions.map((entry) => ({
643
+ ...entry,
644
+ id: fresh(entry.id),
645
+ recordedAt: timestamp,
646
+ provenance,
647
+ }));
648
+ const findings = source.findings.map((entry) => ({
649
+ ...entry,
650
+ id: fresh(entry.id),
651
+ recordedAt: timestamp,
652
+ provenance,
653
+ data: {
654
+ ...entry.data,
655
+ refs: entry.data.refs.map((ref) => mapRef(ref, ids)),
656
+ resolution: entry.data.resolution
657
+ ? {
658
+ ...entry.data.resolution,
659
+ evidenceIds: entry.data.resolution.evidenceIds.map((id) => ids.get(id) ?? id),
660
+ decisionIds: entry.data.resolution.decisionIds.map((id) => ids.get(id) ?? id),
661
+ }
662
+ : null,
663
+ },
664
+ }));
665
+ const workers = source.workers.map((entry) => ({
666
+ ...entry,
667
+ id: fresh(entry.id),
668
+ recordedAt: timestamp,
669
+ provenance,
670
+ data: {
671
+ ...entry.data,
672
+ state: "stopped" as const,
673
+ session: null,
674
+ assignment: {
675
+ ...entry.data.assignment,
676
+ decisionIds: entry.data.assignment.decisionIds.map((id) => ids.get(id) ?? id),
677
+ },
678
+ report: entry.data.report
679
+ ? {
680
+ ...entry.data.report,
681
+ evidenceIds: entry.data.report.evidenceIds.map((id) => ids.get(id) ?? id),
682
+ findingIds: entry.data.report.findingIds.map((id) => ids.get(id) ?? id),
683
+ }
684
+ : null,
685
+ },
686
+ }));
687
+ const actionProgress = source.actionProgress?.map((entry) => ({
688
+ ...entry,
689
+ decisionId: ids.get(entry.decisionId) ?? entry.decisionId,
690
+ }));
691
+ const task: TaskRecord = {
692
+ ...source,
693
+ id: newId(),
694
+ workspaceId: destinationWorkspaceId,
695
+ revision: newId(),
696
+ createdAt: timestamp,
697
+ updatedAt: timestamp,
698
+ origin: {
699
+ workspaceId: bundle.sourceWorkspaceId,
700
+ taskId: bundle.task.id,
701
+ exportDigest: bundle.digest,
702
+ },
703
+ intent,
704
+ constraints: context.constraints,
705
+ status: "paused",
706
+ closure: null,
707
+ assessments,
708
+ evidence,
709
+ decisions: decisions.map((entry) => ({
710
+ ...entry,
711
+ data: {
712
+ ...entry.data,
713
+ binding: {
714
+ ...entry.data.binding,
715
+ taskId: "",
716
+ workspaceId: destinationWorkspaceId,
717
+ contentRefs: entry.data.binding.contentRefs.map((ref) => mapRef(ref, ids)),
718
+ },
719
+ consumption: null,
720
+ },
721
+ })),
722
+ ...(actionProgress ? { actionProgress } : {}),
723
+ findings,
724
+ workers,
725
+ candidates: [],
726
+ policy: null,
727
+ progress: {
728
+ ...source.progress,
729
+ blockers: source.progress.blockers.map((blocker) => ({
730
+ ...blocker,
731
+ refs: blocker.refs.map((ref) => mapRef(ref, ids)),
732
+ })),
733
+ },
734
+ };
735
+ task.decisions = task.decisions.map((entry) => ({
736
+ ...entry,
737
+ data: {
738
+ ...entry.data,
739
+ binding: { ...entry.data.binding, taskId: task.id },
740
+ digest: decisionDigest(entry.data),
741
+ },
742
+ }));
743
+ return task;
744
+ };
745
+
746
+ export class WorkitCore {
747
+ private readonly authorityOwner = {};
748
+
749
+ constructor(
750
+ private readonly store: TaskStore,
751
+ private readonly context: OperationContext,
752
+ ) {}
753
+
754
+ /** Optional host actions are coordinator-owned; supervised worker contexts cannot invoke them. */
755
+ public isDelegatedCaller(): boolean {
756
+ return this.context.workerId !== null && this.context.workerId !== undefined;
757
+ }
758
+
759
+ private contextRootError(): Result<null> {
760
+ try {
761
+ if (realpathSync(this.context.root) !== this.store.root)
762
+ return failure(
763
+ "invalid_input",
764
+ "operation context root does not match the task store root",
765
+ );
766
+ } catch {
767
+ return failure("invalid_input", "operation context root cannot be resolved");
768
+ }
769
+ return success(null, null, null);
770
+ }
771
+
772
+ private callerContext(): CallerContext {
773
+ return {
774
+ ...this.context.caller,
775
+ workerId: this.context.workerId ?? null,
776
+ };
777
+ }
778
+
779
+ /**
780
+ * Omitted revisions default to the records read for this call; explicit values
781
+ * stay CAS-enforced downstream. Call after loading task/workspace records.
782
+ */
783
+ private fillRevisions(
784
+ input: { expectedRevision?: string; expectedWorkspaceRevision?: string | null },
785
+ task?: { revision: string },
786
+ workspace?: { revision: string } | null,
787
+ ): void {
788
+ if (input.expectedRevision === undefined && task) input.expectedRevision = task.revision;
789
+ if (input.expectedWorkspaceRevision === undefined)
790
+ input.expectedWorkspaceRevision = workspace ? workspace.revision : null;
791
+ }
792
+
793
+ private helperEntry(
794
+ task: TaskRecord,
795
+ requireSession = false,
796
+ requireActive = false,
797
+ ): Result<Entry<import("./task-contract").Worker>> {
798
+ const workerId = this.context.workerId ?? null;
799
+ if (workerId === null) return failure("permission_denied", "operation is lead-only");
800
+ const worker = task.workers.find((entry) => entry.id === workerId);
801
+ if (!worker) return failure("permission_denied", "worker is not assigned to this task");
802
+ if (worker.data.session && !sameSession(worker.data.session, this.context))
803
+ return failure("permission_denied", "worker session does not match the caller");
804
+ if (requireSession && (!worker.data.session || !sameSession(worker.data.session, this.context)))
805
+ return failure("permission_denied", "worker session has not been observed");
806
+ if (requireActive && worker.data.state !== "running")
807
+ return failure("permission_denied", "worker is not active");
808
+ return success(null, null, worker);
809
+ }
810
+
811
+ private helperTaskGuard(task: TaskRecord, allowRead = false): Result<null> {
812
+ if ((this.context.workerId ?? null) === null) return success(null, null, null);
813
+ const worker = this.helperEntry(task);
814
+ if (!worker.ok) return worker as Result<never>;
815
+ return allowRead
816
+ ? success(null, null, null)
817
+ : failure("permission_denied", "helpers cannot control task lifecycle or scope");
818
+ }
819
+
820
+ private activeWorkerBlocker(
821
+ task: TaskRecord,
822
+ workspace: import("./task-contract").WorkspaceRecord,
823
+ ) {
824
+ if (workspace.writer)
825
+ return failure("recovery_required", "writer ownership must be released first");
826
+ if (
827
+ task.workers.some((entry) => ["running", "cancelling", "unknown"].includes(entry.data.state))
828
+ )
829
+ return failure("recovery_required", "worker state requires reconciliation");
830
+ return null;
831
+ }
832
+
833
+ state(request: unknown): Result<ExportBundle | TaskSummary | TaskRecord | WorkspaceRecord> {
834
+ const root = this.contextRootError();
835
+ if (!root.ok) return root as Result<never>;
836
+ if ((this.context.workerId ?? null) !== null)
837
+ return failure("permission_denied", "helpers cannot control task state");
838
+ const parsed = parseOperation("state", request);
839
+ if (!parsed.ok) return parsed as Result<never>;
840
+ const input = parsed.data as any;
841
+ if (input.action === "export") {
842
+ const task = this.store.readTask(input.taskId);
843
+ if (!task.ok) return task as Result<never>;
844
+ const workspace = this.store.readWorkspace();
845
+ if (!workspace.ok) return workspace as Result<never>;
846
+ if (!workspace.data) return failure("not_found", "workspace not found");
847
+ const portable = taskRecordSchema.safeParse(portableTask(task.data));
848
+ if (!portable.success)
849
+ return failure("storage_error", "portable task is invalid after sanitization");
850
+ const bundle = {
851
+ schemaVersion: 1 as const,
852
+ exportedAt: trustedNow(this.context),
853
+ sourceWorkspaceId: workspace.data.id,
854
+ task: portable.data,
855
+ };
856
+ const checked = exportBundleSchema.safeParse({
857
+ ...bundle,
858
+ digest: exportDigest(bundle),
859
+ });
860
+ if (!checked.success) return failure("storage_error", "portable export bundle is invalid");
861
+ return success(task.data.revision, workspace.data.revision, checked.data);
862
+ }
863
+ if (input.action === "import") {
864
+ const checked = exportBundleSchema.safeParse(input.bundle);
865
+ if (!checked.success) return failure("invalid_input", "export bundle is invalid");
866
+ const bundle = checked.data;
867
+ if (
868
+ bundle.sourceWorkspaceId !== bundle.task.workspaceId ||
869
+ exportDigest({
870
+ schemaVersion: bundle.schemaVersion,
871
+ exportedAt: bundle.exportedAt,
872
+ sourceWorkspaceId: bundle.sourceWorkspaceId,
873
+ task: bundle.task,
874
+ }) !== bundle.digest
875
+ )
876
+ return failure("invalid_input", "export bundle digest is invalid");
877
+ const workspace = this.store.readWorkspace();
878
+ if (!workspace.ok) return workspace as Result<never>;
879
+ const destinationWorkspaceId = workspace.data?.id ?? newId();
880
+ this.fillRevisions(input, undefined, workspace.data ?? null);
881
+ const timestamp = trustedNow(this.context);
882
+ const task = importedTask(
883
+ bundle.task,
884
+ bundle,
885
+ destinationWorkspaceId,
886
+ this.context,
887
+ timestamp,
888
+ );
889
+ const imported = this.store.importTask({
890
+ task,
891
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
892
+ workspaceId: destinationWorkspaceId,
893
+ now: timestamp,
894
+ });
895
+ if (!imported.ok) return imported as Result<never>;
896
+ return this.summary(imported.data);
897
+ }
898
+ if (!this.context.nativeRecovery)
899
+ return failure("permission_denied", "native recovery authority is unavailable");
900
+ const processEvidence = (
901
+ lock: MetadataLock | null,
902
+ writer: WorkspaceRecord["writer"],
903
+ ): Result<ProcessEvidence> =>
904
+ this.context.nativeRecovery!({
905
+ lock,
906
+ writer,
907
+ reason: input.reason,
908
+ authorityRefs: input.authorityRefs,
909
+ });
910
+ if (input.target === "workspace")
911
+ return this.store.recoverWorkspace({
912
+ expectedBytes: input.expectedBytes,
913
+ snapshotDigest: input.snapshotDigest,
914
+ reason: input.reason,
915
+ authorityRefs: input.authorityRefs,
916
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
917
+ processEvidence,
918
+ });
919
+ const workspace = this.store.readWorkspace();
920
+ if (!workspace.ok) return workspace as Result<never>;
921
+ if (!workspace.data) return failure("not_found", "workspace not found");
922
+ this.fillRevisions(input, undefined, workspace.data);
923
+ if (input.target === "workspace" && input.taskId !== undefined)
924
+ return failure("invalid_input", "workspace recovery takes no task ID");
925
+ if (input.target === "task" && input.taskId === undefined)
926
+ return failure("invalid_input", "task recovery requires a task ID");
927
+ return this.store.recoverTask(input.taskId, {
928
+ expectedBytes: input.expectedBytes,
929
+ snapshotDigest: input.snapshotDigest,
930
+ reason: input.reason,
931
+ authorityRefs: input.authorityRefs,
932
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
933
+ processEvidence,
934
+ });
935
+ }
936
+
937
+ task(request: unknown): Result<TaskSummary | TaskSummary[] | TaskView> {
938
+ const root = this.contextRootError();
939
+ if (!root.ok) return root as Result<never>;
940
+ const parsed = parseOperation("task", request);
941
+ if (!parsed.ok) return parsed as Result<never>;
942
+ const input = parsed.data as any;
943
+ if ((this.context.workerId ?? null) !== null) {
944
+ if (input.action !== "inspect")
945
+ return failure("permission_denied", "helpers cannot control task lifecycle or scope");
946
+ }
947
+ switch (input.action) {
948
+ case "start": {
949
+ if (input.expectedWorkspaceRevision === undefined) {
950
+ const workspace = this.store.readWorkspace();
951
+ if (!workspace.ok) return workspace as Result<never>;
952
+ input.expectedWorkspaceRevision = workspace.data?.revision ?? null;
953
+ }
954
+ const created = this.store.create({
955
+ intent: input.intent,
956
+ provenance: provenance(this.context),
957
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
958
+ now: trustedNow(this.context),
959
+ });
960
+ if (!created.ok) return created as Result<never>;
961
+ return this.summary(created.data);
962
+ }
963
+ case "list": {
964
+ const listed = this.store.listTasks();
965
+ if (!listed.ok) return listed as Result<never>;
966
+ const summaries: TaskSummary[] = [];
967
+ for (const task of listed.data) {
968
+ const summary = this.summary(task);
969
+ if (!summary.ok) return summary;
970
+ summaries.push(summary.data);
971
+ }
972
+ return success(null, null, summaries);
973
+ }
974
+ case "inspect": {
975
+ const task = this.store.readTask(input.taskId);
976
+ if (!task.ok) return task as Result<never>;
977
+ const helper = this.helperTaskGuard(task.data, true);
978
+ if (!helper.ok) return helper as Result<never>;
979
+ if (input.view === "summary") return this.summary(task.data);
980
+ return this.view(task.data);
981
+ }
982
+ case "pause":
983
+ return this.transition(input, "paused");
984
+ case "resume":
985
+ return this.transition(input, "active");
986
+ case "progress":
987
+ return this.progress(input);
988
+ case "revise":
989
+ return this.revise(input);
990
+ case "close":
991
+ return this.close(input);
992
+ default:
993
+ return failure("invalid_transition", `task action ${input.action} is not implemented`);
994
+ }
995
+ }
996
+
997
+ policy(request: unknown): Result<Policy | null> {
998
+ const root = this.contextRootError();
999
+ if (!root.ok) return root as Result<never>;
1000
+ const parsed = parseOperation("policy", request);
1001
+ if (!parsed.ok) return parsed as Result<never>;
1002
+ const input = parsed.data as any;
1003
+ const task = this.store.readTask(input.taskId);
1004
+ if (!task.ok) return task as Result<never>;
1005
+ if (input.action === "explain") return success(task.data.revision, null, task.data.policy);
1006
+ if ((this.context.workerId ?? null) !== null)
1007
+ return failure("permission_denied", "helpers cannot change task requirements");
1008
+ this.fillRevisions(input, task.data);
1009
+ const resolved = this.resolve(task.data, input.assessment);
1010
+ if (!resolved.ok) return resolved;
1011
+ if (input.action === "preview") return success(null, null, resolved.data);
1012
+ if (input.action !== "assess")
1013
+ return failure("invalid_transition", "unsupported policy action");
1014
+ const changed = this.store.mutateTask(
1015
+ task.data.id,
1016
+ input.expectedRevision,
1017
+ (current, mutation) => {
1018
+ if (current.status === "closed")
1019
+ return failure("invalid_transition", "closed task cannot be assessed");
1020
+ const nextChange = diffPolicy(
1021
+ current.policy,
1022
+ resolved.data,
1023
+ "policy reassessed",
1024
+ mutation.now,
1025
+ );
1026
+ const assessment = {
1027
+ id: newId(),
1028
+ recordedAt: mutation.now,
1029
+ provenance: provenance(this.context),
1030
+ data: input.assessment as Assessment,
1031
+ };
1032
+ const next = {
1033
+ ...current,
1034
+ constraints: this.context.constraints,
1035
+ assessments: [...current.assessments, assessment],
1036
+ policy: resolved.data,
1037
+ policyChanges: nextChange
1038
+ ? [...current.policyChanges, nextChange]
1039
+ : current.policyChanges,
1040
+ };
1041
+ return success(mutation.revision, null, next);
1042
+ },
1043
+ trustedNow(this.context),
1044
+ );
1045
+ if (!changed.ok) return changed as Result<never>;
1046
+ return success(changed.data.revision, null, changed.data.policy);
1047
+ }
1048
+
1049
+ evidence(request: unknown): Result<Entry<Evidence>> {
1050
+ const root = this.contextRootError();
1051
+ if (!root.ok) return root as Result<never>;
1052
+ const parsed = parseOperation("evidence", request);
1053
+ if (!parsed.ok) return parsed as Result<never>;
1054
+ const input = parsed.data as any;
1055
+ const task = this.store.readTask(input.taskId);
1056
+ if (!task.ok) return task as Result<never>;
1057
+ if (task.data.status === "closed")
1058
+ return failure("invalid_transition", "closed task cannot record evidence");
1059
+ this.fillRevisions(input, task.data);
1060
+ const helper =
1061
+ (this.context.workerId ?? null) === null ? null : this.helperEntry(task.data, true, true);
1062
+ if (helper && !helper.ok) return helper as Result<never>;
1063
+ const evidence = input.evidence as Evidence;
1064
+ if (helper?.ok) {
1065
+ const assignment = helper.data.data.assignment;
1066
+ if (
1067
+ evidence.requirementIds.some((id: string) => !assignment.requirementIds.includes(id)) ||
1068
+ (assignment.candidateId !== null &&
1069
+ evidence.candidateId !== assignment.candidateId &&
1070
+ evidence.beforeCandidateId !== assignment.candidateId) ||
1071
+ !refsWithinScope(evidence.refs, assignment.scope) ||
1072
+ !refsWithinScope(evidence.refs, task.data.intent.data.scope)
1073
+ )
1074
+ return failure("permission_denied", "evidence is outside the worker assignment");
1075
+ }
1076
+ if (
1077
+ (evidence.result === "missing" || evidence.result === "skipped") &&
1078
+ !evidence.summary.trim()
1079
+ )
1080
+ return failure("invalid_input", "missing or skipped evidence requires a reason");
1081
+ if (evidence.kind === "review") {
1082
+ if (!sameSession(evidence.reviewContext, this.context))
1083
+ return failure("invalid_input", "review context must match the trusted caller session");
1084
+ }
1085
+ const currentCandidate = captureCandidate(
1086
+ this.store.root,
1087
+ task.data.intent.data.scope,
1088
+ environment(),
1089
+ );
1090
+ if (!currentCandidate.ok) return currentCandidate as Result<never>;
1091
+ if (evidence.kind === "check" || evidence.kind === "review") {
1092
+ // Checks run against the tree as observed now; bind them to the captured
1093
+ // candidate so callers never hand-compute digests. Later tree changes
1094
+ // still mark this evidence stale through the normal evaluation.
1095
+ evidence.beforeCandidateId ??= currentCandidate.data.id;
1096
+ evidence.candidateId ??= currentCandidate.data.id;
1097
+ } else {
1098
+ evidence.beforeCandidateId ??= null;
1099
+ evidence.candidateId ??= null;
1100
+ }
1101
+ const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
1102
+ for (const candidateId of [evidence.beforeCandidateId, evidence.candidateId]) {
1103
+ if (
1104
+ candidateId &&
1105
+ candidateId !== currentCandidate.data.id &&
1106
+ !knownCandidates.has(candidateId)
1107
+ )
1108
+ return failure("invalid_input", "evidence candidate is not a captured candidate");
1109
+ }
1110
+ const changed = this.store.mutateTask(
1111
+ task.data.id,
1112
+ input.expectedRevision,
1113
+ (current, mutation) => {
1114
+ const candidates = current.candidates.some((item) => item.id === currentCandidate.data.id)
1115
+ ? current.candidates
1116
+ : [...current.candidates, currentCandidate.data];
1117
+ const entry = {
1118
+ id: newId(),
1119
+ recordedAt: mutation.now,
1120
+ provenance: provenance(this.context, "agent_reported"),
1121
+ data: evidence,
1122
+ };
1123
+ return success(mutation.revision, null, {
1124
+ ...current,
1125
+ candidates,
1126
+ evidence: [...current.evidence, entry],
1127
+ findings: current.findings.map((finding) => {
1128
+ if (finding.data.disposition === "open" || evidence.result === "skipped")
1129
+ return finding;
1130
+ if (finding.data.disposition !== "fixed") return finding;
1131
+ // A fix stands while its verification still passes on the current
1132
+ // tree; it reopens only on lapse or direct contradiction. Judged
1133
+ // dispositions never auto-reopen.
1134
+ const withEntry = { ...current, candidates, evidence: [...current.evidence, entry] };
1135
+ const statuses = new Map(
1136
+ evaluateEvidence(withEntry, currentCandidate.data).map((item) => [
1137
+ item.evidenceId,
1138
+ item.status,
1139
+ ]),
1140
+ );
1141
+ const verified = withEntry.evidence.some(
1142
+ (item) =>
1143
+ statuses.get(item.id) === "passed" &&
1144
+ (item.data.kind === "check" || item.data.kind === "review") &&
1145
+ (finding.data.candidateId === null ||
1146
+ item.data.candidateId === finding.data.candidateId),
1147
+ );
1148
+ const contradicted =
1149
+ evidence.result === "failed" &&
1150
+ finding.data.refs.some((left: Ref) =>
1151
+ evidence.refs.some((right: Ref) => JSON.stringify(left) === JSON.stringify(right)),
1152
+ );
1153
+ return verified && !contradicted
1154
+ ? finding
1155
+ : { ...finding, data: { ...finding.data, disposition: "open", resolution: null } };
1156
+ }),
1157
+ });
1158
+ },
1159
+ trustedNow(this.context),
1160
+ );
1161
+ if (!changed.ok) return changed as Result<never>;
1162
+ return success(changed.data.revision, null, changed.data.evidence.at(-1)!);
1163
+ }
1164
+
1165
+ decision(request: unknown): Result<Entry<Decision>> {
1166
+ return this.recordDecision(request);
1167
+ }
1168
+
1169
+ observeDecision(request: unknown, observation: unknown): Result<Entry<Decision>> {
1170
+ return this.recordDecision(request, observation, true);
1171
+ }
1172
+
1173
+ private recordDecision(
1174
+ request: unknown,
1175
+ nativeObservation?: unknown,
1176
+ nativeRequired = false,
1177
+ ): Result<Entry<Decision>> {
1178
+ const root = this.contextRootError();
1179
+ if (!root.ok) return root as Result<never>;
1180
+ const parsed = parseOperation("decision", request);
1181
+ if (!parsed.ok) return parsed as Result<never>;
1182
+ const input = parsed.data as any;
1183
+ if (nativeRequired && input.action !== "record")
1184
+ return failure(
1185
+ "permission_denied",
1186
+ "native observation is only valid for recording decisions",
1187
+ );
1188
+ if (nativeRequired && nativeObservation === undefined)
1189
+ return failure("permission_denied", "native decision observation is required");
1190
+ const task = this.store.readTask(input.taskId);
1191
+ if (!task.ok) return task as Result<never>;
1192
+ if ((this.context.workerId ?? null) !== null)
1193
+ return failure("permission_denied", "helpers cannot record or revoke decisions");
1194
+ this.fillRevisions(input, task.data);
1195
+ if (input.action === "record") {
1196
+ if (task.data.status === "closed")
1197
+ return failure("invalid_transition", "closed task cannot record a decision");
1198
+ const workspace = this.store.readWorkspace();
1199
+ if (!workspace.ok) return workspace as Result<never>;
1200
+ if (!workspace.data) return failure("not_found", "workspace not found");
1201
+ if (input.binding.taskId !== task.data.id || input.binding.workspaceId !== workspace.data.id)
1202
+ return failure("permission_denied", "decision task or workspace binding is invalid");
1203
+ const content = verifyDecisionContent(this.store, input.binding);
1204
+ if (!content.ok) return content as Result<never>;
1205
+ const knownRequirements = new Set(
1206
+ task.data.policy?.requirements.map((item) => item.id) ?? [],
1207
+ );
1208
+ if (input.requirementIds.some((id: string) => !knownRequirements.has(id)))
1209
+ return failure("invalid_input", "decision references an unknown requirement");
1210
+ const base = {
1211
+ purpose: input.purpose,
1212
+ binding: input.binding,
1213
+ response: input.response,
1214
+ requirementIds: input.requirementIds,
1215
+ revoked: null,
1216
+ consumption: null,
1217
+ } satisfies Omit<Decision, "digest">;
1218
+ const data: Decision = { ...base, digest: decisionDigest(base) };
1219
+ const native = nativeRequired
1220
+ ? verifyNativeDecision(
1221
+ this.context.nativeAuthority,
1222
+ {
1223
+ observation: nativeObservation,
1224
+ expected: {
1225
+ taskId: task.data.id,
1226
+ workspaceId: workspace.data.id,
1227
+ purpose: input.purpose,
1228
+ response: input.response,
1229
+ binding: input.binding,
1230
+ bindingBytes: canonicalJson(input.binding),
1231
+ digest: data.digest,
1232
+ requirementIds: [...input.requirementIds],
1233
+ },
1234
+ caller: this.context.caller,
1235
+ },
1236
+ { owner: this.authorityOwner, store: this.store, root: this.store.root },
1237
+ )
1238
+ : null;
1239
+ if (native && !native.ok) return native as Result<never>;
1240
+ const nativeProvenance = native?.ok ? retireNativeAuthority(native.data) : null;
1241
+ if (native?.ok && !nativeProvenance)
1242
+ return failure("permission_denied", "native decision authority was retired");
1243
+ const changed = this.store.mutateTask(
1244
+ task.data.id,
1245
+ input.expectedRevision,
1246
+ (current, mutation) => {
1247
+ const content = verifyDecisionContent(this.store, input.binding);
1248
+ if (!content.ok) return content as Result<never>;
1249
+ const known = new Set(current.policy?.requirements.map((item) => item.id) ?? []);
1250
+ if (input.requirementIds.some((id: string) => !known.has(id)))
1251
+ return failure("invalid_input", "decision references an unknown requirement");
1252
+ if (
1253
+ nativeProvenance &&
1254
+ current.decisions.some((entry) =>
1255
+ entry.provenance.receipts.some((existingReceipt) =>
1256
+ nativeProvenance.receipts.some(
1257
+ (receipt) => canonicalJson(receipt) === canonicalJson(existingReceipt),
1258
+ ),
1259
+ ),
1260
+ )
1261
+ )
1262
+ return failure("permission_denied", "native decision receipt was already consumed");
1263
+ const entry: Entry<Decision> = {
1264
+ id: newId(),
1265
+ recordedAt: mutation.now,
1266
+ provenance: nativeProvenance ?? provenance(this.context, "agent_reported"),
1267
+ data,
1268
+ };
1269
+ return success(mutation.revision, null, {
1270
+ ...current,
1271
+ decisions: [...current.decisions, entry],
1272
+ });
1273
+ },
1274
+ trustedNow(this.context),
1275
+ );
1276
+ if (!changed.ok) return changed as Result<never>;
1277
+ return success(changed.data.revision, null, changed.data.decisions.at(-1)!);
1278
+ }
1279
+ const decision = task.data.decisions.find((entry) => entry.id === input.decisionId);
1280
+ if (!decision) return failure("not_found", "decision not found");
1281
+ if (!input.reason.trim())
1282
+ return failure("invalid_input", "decision revocation requires a reason");
1283
+ if (decision.data.revoked) return failure("invalid_transition", "decision is already revoked");
1284
+ if (decision.data.consumption)
1285
+ return failure("permission_denied", "consumed decision cannot be revoked");
1286
+ const changed = this.store.mutateTask(
1287
+ task.data.id,
1288
+ input.expectedRevision,
1289
+ (current, mutation) => {
1290
+ const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
1291
+ if (!entry) return failure("not_found", "decision not found");
1292
+ if (entry.data.revoked) return failure("invalid_transition", "decision is already revoked");
1293
+ if (entry.data.consumption)
1294
+ return failure("permission_denied", "consumed decision cannot be revoked");
1295
+ const data = {
1296
+ ...entry.data,
1297
+ revoked: { at: mutation.now, reason: input.reason },
1298
+ };
1299
+ const updated = { ...entry, data };
1300
+ return success(mutation.revision, null, {
1301
+ ...current,
1302
+ decisions: current.decisions.map((candidate) =>
1303
+ candidate.id === input.decisionId ? updated : candidate,
1304
+ ),
1305
+ });
1306
+ },
1307
+ trustedNow(this.context),
1308
+ );
1309
+ if (!changed.ok) return changed as Result<never>;
1310
+ const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
1311
+ return entry
1312
+ ? success(changed.data.revision, null, entry)
1313
+ : failure("recovery_required", "decision disappeared");
1314
+ }
1315
+
1316
+ finding(request: unknown): Result<Entry<Finding>> {
1317
+ const root = this.contextRootError();
1318
+ if (!root.ok) return root as Result<never>;
1319
+ const parsed = parseOperation("finding", request);
1320
+ if (!parsed.ok) return parsed as Result<never>;
1321
+ const input = parsed.data as any;
1322
+ const task = this.store.readTask(input.taskId);
1323
+ if (!task.ok) return task as Result<never>;
1324
+ if (task.data.status === "closed")
1325
+ return failure("invalid_transition", "closed task cannot mutate findings");
1326
+ this.fillRevisions(input, task.data);
1327
+ const helper =
1328
+ (this.context.workerId ?? null) === null ? null : this.helperEntry(task.data, true, true);
1329
+ if (helper && !helper.ok) return helper as Result<never>;
1330
+ if (input.action === "record") {
1331
+ if (helper?.ok) {
1332
+ const assignment = helper.data.data.assignment;
1333
+ if (
1334
+ !bindingCovers(assignment.scope, input.scope) ||
1335
+ !bindingCovers(task.data.intent.data.scope, input.scope) ||
1336
+ (assignment.candidateId !== null && input.candidateId !== assignment.candidateId) ||
1337
+ !refsWithinScope(input.refs, assignment.scope) ||
1338
+ !refsWithinScope(input.refs, task.data.intent.data.scope)
1339
+ )
1340
+ return failure("permission_denied", "finding is outside the worker assignment");
1341
+ }
1342
+ if (
1343
+ input.candidateId &&
1344
+ !task.data.candidates.some((candidate) => candidate.id === input.candidateId)
1345
+ )
1346
+ return failure("invalid_input", "finding candidate is not a captured candidate");
1347
+ const changed = this.store.mutateTask(
1348
+ task.data.id,
1349
+ input.expectedRevision,
1350
+ (current, mutation) => {
1351
+ const entry: Entry<Finding> = {
1352
+ id: newId(),
1353
+ recordedAt: mutation.now,
1354
+ provenance: provenance(this.context, "agent_reported"),
1355
+ data: {
1356
+ claim: input.claim,
1357
+ consequence: input.consequence,
1358
+ scope: input.scope,
1359
+ candidateId: input.candidateId ?? null,
1360
+ refs: input.refs,
1361
+ disposition: "open",
1362
+ resolution: null,
1363
+ },
1364
+ };
1365
+ return success(mutation.revision, null, {
1366
+ ...current,
1367
+ findings: [...current.findings, entry],
1368
+ });
1369
+ },
1370
+ trustedNow(this.context),
1371
+ );
1372
+ if (!changed.ok) return changed as Result<never>;
1373
+ return success(changed.data.revision, null, changed.data.findings.at(-1)!);
1374
+ }
1375
+ if (helper?.ok) return failure("permission_denied", "helpers cannot resolve findings");
1376
+ const finding = task.data.findings.find((entry) => entry.id === input.findingId);
1377
+ if (!finding) return failure("not_found", "finding not found");
1378
+ if (!input.reason.trim())
1379
+ return failure("invalid_input", "finding resolution requires a reason");
1380
+ const evidence = task.data.evidence.filter((entry) => input.evidenceIds.includes(entry.id));
1381
+ const decisions = task.data.decisions.filter((entry) => input.decisionIds.includes(entry.id));
1382
+ if (
1383
+ evidence.length !== input.evidenceIds.length ||
1384
+ decisions.length !== input.decisionIds.length
1385
+ )
1386
+ return failure("invalid_input", "finding resolution references an unknown record");
1387
+ if (input.disposition === "fixed") {
1388
+ const current = captureCandidate(this.store.root, task.data.intent.data.scope, environment());
1389
+ if (!current.ok) return current as Result<never>;
1390
+ const evaluations = evaluateEvidence(task.data, current.data);
1391
+ const verified = evidence.some((entry) => {
1392
+ const evaluation = evaluations.find((item) => item.evidenceId === entry.id);
1393
+ return (
1394
+ evaluation?.status === "passed" &&
1395
+ (entry.data.kind === "check" || entry.data.kind === "review") &&
1396
+ (finding.data.candidateId === null || entry.data.candidateId === finding.data.candidateId)
1397
+ );
1398
+ });
1399
+ if (!verified)
1400
+ return failure("permission_denied", "fixed findings require passing verification evidence");
1401
+ }
1402
+ if (input.disposition === "dismissed" && evidence.length === 0)
1403
+ return failure("permission_denied", "dismissal requires supporting evidence");
1404
+ if (input.disposition === "dismissed") {
1405
+ const relevant = evidence.every((entry) => {
1406
+ if (entry.data.result === "missing" || entry.data.result === "skipped") return false;
1407
+ const candidateMatches =
1408
+ finding.data.candidateId === null
1409
+ ? entry.data.candidateId === null
1410
+ : entry.data.candidateId === finding.data.candidateId ||
1411
+ entry.data.beforeCandidateId === finding.data.candidateId;
1412
+ const referenceMatches = finding.data.refs.some((left) =>
1413
+ entry.data.refs.some((right) => JSON.stringify(left) === JSON.stringify(right)),
1414
+ );
1415
+ return candidateMatches && (referenceMatches || entry.data.claim === finding.data.claim);
1416
+ });
1417
+ if (!relevant)
1418
+ return failure("permission_denied", "dismissal evidence is unrelated to the finding");
1419
+ }
1420
+ if (input.disposition === "deferred") {
1421
+ const workspace = this.store.readWorkspace();
1422
+ if (!workspace.ok) return workspace as Result<never>;
1423
+ if (!workspace.data) return failure("not_found", "workspace not found");
1424
+ const allowed = decisions.some(
1425
+ (entry) =>
1426
+ entry.provenance.kind !== "imported" &&
1427
+ entry.data.purpose === "limitation" &&
1428
+ entry.data.response === "approved" &&
1429
+ entry.data.revoked === null &&
1430
+ entry.data.digest === decisionDigest(entry.data) &&
1431
+ verifyDecisionContent(this.store, entry.data.binding).ok &&
1432
+ entry.data.binding.taskId === task.data.id &&
1433
+ entry.data.binding.workspaceId === workspace.data!.id &&
1434
+ bindingCovers(entry.data.binding.scope, finding.data.scope) &&
1435
+ entry.data.requirementIds.some((id: string) =>
1436
+ task.data.policy?.requirements.some(
1437
+ (requirement) => requirement.id === id && requirement.acceptanceAllowed,
1438
+ ),
1439
+ ),
1440
+ );
1441
+ if (!allowed)
1442
+ return failure(
1443
+ "permission_denied",
1444
+ "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope",
1445
+ );
1446
+ }
1447
+ const changed = this.store.mutateTask(
1448
+ task.data.id,
1449
+ input.expectedRevision,
1450
+ (current, mutation) => {
1451
+ const existing = current.findings.find((entry) => entry.id === input.findingId);
1452
+ if (!existing) return failure("not_found", "finding not found");
1453
+ const data =
1454
+ input.disposition === "open"
1455
+ ? { ...existing.data, disposition: "open" as const, resolution: null }
1456
+ : {
1457
+ ...existing.data,
1458
+ disposition: input.disposition,
1459
+ resolution: {
1460
+ reason: input.reason,
1461
+ evidenceIds: input.evidenceIds,
1462
+ decisionIds: input.decisionIds,
1463
+ },
1464
+ };
1465
+ const updated = { ...existing, data };
1466
+ return success(mutation.revision, null, {
1467
+ ...current,
1468
+ findings: current.findings.map((entry) =>
1469
+ entry.id === input.findingId ? updated : entry,
1470
+ ),
1471
+ });
1472
+ },
1473
+ trustedNow(this.context),
1474
+ );
1475
+ if (!changed.ok) return changed as Result<never>;
1476
+ const entry = changed.data.findings.find((candidate) => candidate.id === input.findingId);
1477
+ return entry
1478
+ ? success(changed.data.revision, null, entry)
1479
+ : failure("recovery_required", "finding disappeared");
1480
+ }
1481
+
1482
+ worker(request: unknown): Result<Entry<Worker>> {
1483
+ const root = this.contextRootError();
1484
+ if (!root.ok) return root as Result<never>;
1485
+ const parsed = parseOperation("worker", request);
1486
+ if (!parsed.ok) return parsed as Result<never>;
1487
+ const input = parsed.data as any;
1488
+ const task = this.store.readTask(input.taskId);
1489
+ if (!task.ok) return task as Result<never>;
1490
+ const workspace = this.store.readWorkspace();
1491
+ if (!workspace.ok) return workspace as Result<never>;
1492
+ if (!workspace.data) return failure("not_found", "workspace not found");
1493
+ this.fillRevisions(input, task.data, workspace.data);
1494
+ const helperId = this.context.workerId ?? null;
1495
+ if (input.action === "assign") {
1496
+ if (helperId !== null) return failure("permission_denied", "helpers cannot assign workers");
1497
+ if (task.data.status !== "active")
1498
+ return failure("invalid_transition", "paused or closed tasks cannot assign workers");
1499
+ if (!bindingCovers(task.data.intent.data.scope, input.assignment.scope))
1500
+ return failure("permission_denied", "worker assignment is outside task scope");
1501
+ if (
1502
+ input.assignment.decisionIds.some(
1503
+ (id: string) => !task.data.decisions.some((entry) => entry.id === id),
1504
+ )
1505
+ )
1506
+ return failure("invalid_input", "worker assignment references an unknown decision");
1507
+ if (
1508
+ input.assignment.requirementIds.some(
1509
+ (id: string) =>
1510
+ !task.data.policy?.requirements.some((requirement) => requirement.id === id),
1511
+ )
1512
+ )
1513
+ return failure("invalid_input", "worker assignment references an unknown requirement");
1514
+ const reviewRequirements = (task.data.policy?.requirements ?? []).filter(
1515
+ (requirement) => requirement.dimension === "review",
1516
+ );
1517
+ if (
1518
+ input.assignment.role === "reviewer" &&
1519
+ input.assignment.requirementIds.length === 0 &&
1520
+ reviewRequirements.length
1521
+ )
1522
+ return failure(
1523
+ "invalid_input",
1524
+ "reviewer assignment needs the review requirement ids it will evidence",
1525
+ { requirementIds: reviewRequirements.map((requirement) => requirement.id) },
1526
+ );
1527
+ if (
1528
+ input.assignment.candidateId &&
1529
+ !task.data.candidates.some((candidate) => candidate.id === input.assignment.candidateId)
1530
+ )
1531
+ return failure("invalid_input", "worker assignment references an unknown candidate");
1532
+ input.assignment.candidateId ??= null;
1533
+ const workerId = newId();
1534
+ const changed = this.store.mutateTaskAndWorkspace({
1535
+ taskId: task.data.id,
1536
+ expectedTaskRevision: input.expectedRevision,
1537
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1538
+ now: trustedNow(this.context),
1539
+ workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
1540
+ task: (current, mutation) => {
1541
+ const entry: Entry<Worker> = {
1542
+ id: workerId,
1543
+ recordedAt: mutation.now,
1544
+ provenance: provenance(this.context, "agent_reported"),
1545
+ data: { assignment: input.assignment, state: "assigned", session: null, report: null },
1546
+ };
1547
+ return success(mutation.revision, null, {
1548
+ ...current,
1549
+ workers: [...current.workers, entry],
1550
+ });
1551
+ },
1552
+ });
1553
+ if (!changed.ok) return changed as Result<never>;
1554
+ return success(
1555
+ changed.data.task.revision,
1556
+ changed.data.workspace.revision,
1557
+ changed.data.task.workers.at(-1)!,
1558
+ );
1559
+ }
1560
+
1561
+ const entry = task.data.workers.find((candidate) => candidate.id === input.workerId);
1562
+ if (!entry) return failure("not_found", "worker not found");
1563
+ if (input.action === "report") {
1564
+ if (helperId === null || helperId !== input.workerId)
1565
+ return failure("permission_denied", "workers can submit only their own report");
1566
+ const helper = this.helperEntry(task.data, true, true);
1567
+ if (!helper.ok) return helper as Result<never>;
1568
+ const evidence = task.data.evidence.filter((candidate) =>
1569
+ input.report.evidenceIds.includes(candidate.id),
1570
+ );
1571
+ const findings = task.data.findings.filter((candidate) =>
1572
+ input.report.findingIds.includes(candidate.id),
1573
+ );
1574
+ if (
1575
+ evidence.length !== input.report.evidenceIds.length ||
1576
+ findings.length !== input.report.findingIds.length
1577
+ )
1578
+ return failure("invalid_input", "worker report references an unknown record");
1579
+ if (
1580
+ evidence.some((candidate) => candidate.provenance.workerId !== helperId) ||
1581
+ findings.some((candidate) => candidate.provenance.workerId !== helperId) ||
1582
+ evidence.some(
1583
+ (candidate) =>
1584
+ !refsWithinScope(candidate.data.refs, helper.data.data.assignment.scope) ||
1585
+ !refsWithinScope(candidate.data.refs, task.data.intent.data.scope),
1586
+ ) ||
1587
+ findings.some(
1588
+ (candidate) =>
1589
+ !refsWithinScope(candidate.data.refs, helper.data.data.assignment.scope) ||
1590
+ !refsWithinScope(candidate.data.refs, task.data.intent.data.scope),
1591
+ )
1592
+ )
1593
+ return failure("permission_denied", "worker report is outside the worker assignment");
1594
+ const changed = this.store.mutateTaskAndWorkspace({
1595
+ taskId: task.data.id,
1596
+ expectedTaskRevision: input.expectedRevision,
1597
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1598
+ now: trustedNow(this.context),
1599
+ workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
1600
+ task: (current, mutation) =>
1601
+ success(mutation.revision, null, {
1602
+ ...current,
1603
+ workers: current.workers.map((candidate) =>
1604
+ candidate.id === input.workerId
1605
+ ? {
1606
+ ...candidate,
1607
+ recordedAt: mutation.now,
1608
+ data: { ...candidate.data, report: input.report },
1609
+ }
1610
+ : candidate,
1611
+ ),
1612
+ }),
1613
+ });
1614
+ if (!changed.ok) return changed as Result<never>;
1615
+ const updated = changed.data.task.workers.find(
1616
+ (candidate) => candidate.id === input.workerId,
1617
+ );
1618
+ return updated
1619
+ ? success(changed.data.task.revision, changed.data.workspace.revision, updated)
1620
+ : failure("recovery_required", "worker disappeared during report");
1621
+ }
1622
+
1623
+ if (helperId !== null)
1624
+ return failure("permission_denied", "helpers cannot cancel or control workers");
1625
+ if (task.data.status === "closed")
1626
+ return failure("invalid_transition", "closed task cannot cancel workers");
1627
+ if (entry.data.state === "unknown")
1628
+ return failure("recovery_required", "worker state requires recovery");
1629
+ if (entry.data.state === "stopped")
1630
+ return failure("invalid_transition", "stopped worker cannot be cancelled");
1631
+ // A reported worker already made its terminal session-authenticated
1632
+ // statement, and a repeat cancel is the lead's explicit confirmation that
1633
+ // an unconfirmed worker ended. Both settle instead of stranding the worker
1634
+ // in cancelling when the session end was never observed. A first cancel of
1635
+ // a live unreported worker still waits for an observed stop.
1636
+ const settles = entry.data.report !== null || entry.data.state === "cancelling";
1637
+ const changed = this.store.mutateTaskAndWorkspace({
1638
+ taskId: task.data.id,
1639
+ expectedTaskRevision: input.expectedRevision,
1640
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1641
+ now: trustedNow(this.context),
1642
+ workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
1643
+ task: (current, mutation) =>
1644
+ success(mutation.revision, null, {
1645
+ ...current,
1646
+ workers: current.workers.map((candidate) =>
1647
+ candidate.id === input.workerId
1648
+ ? {
1649
+ ...candidate,
1650
+ recordedAt: mutation.now,
1651
+ data: {
1652
+ ...candidate.data,
1653
+ state: settles ? ("stopped" as const) : ("cancelling" as const),
1654
+ },
1655
+ }
1656
+ : candidate,
1657
+ ),
1658
+ }),
1659
+ });
1660
+ if (!changed.ok) return changed as Result<never>;
1661
+ return success(
1662
+ changed.data.task.revision,
1663
+ changed.data.workspace.revision,
1664
+ changed.data.task.workers.find((candidate) => candidate.id === input.workerId)!,
1665
+ );
1666
+ }
1667
+
1668
+ observeWorkerLifecycle(input: NativeWorkerObservation): Result<Entry<Worker>> {
1669
+ const root = this.contextRootError();
1670
+ if (!root.ok) return root as Result<never>;
1671
+ if (
1672
+ this.context.workerId !== undefined &&
1673
+ this.context.workerId !== null &&
1674
+ this.context.workerId !== input.workerId
1675
+ )
1676
+ return failure("permission_denied", "worker observation does not match the caller");
1677
+ const task = this.store.readTask(input.taskId);
1678
+ if (!task.ok) return task as Result<never>;
1679
+ const workspace = this.store.readWorkspace();
1680
+ if (!workspace.ok) return workspace as Result<never>;
1681
+ if (!workspace.data) return failure("not_found", "workspace not found");
1682
+ const authority = verifyNativeWorker(
1683
+ this.context.nativeWorker,
1684
+ {
1685
+ observation: input.observation,
1686
+ expected: { ...input, workspaceId: workspace.data.id },
1687
+ caller: this.context.caller,
1688
+ },
1689
+ {
1690
+ owner: this.authorityOwner,
1691
+ store: this.store,
1692
+ root: this.store.root,
1693
+ caller: this.context.caller,
1694
+ },
1695
+ );
1696
+ if (!authority.ok) return authority as Result<never>;
1697
+ return applyWorkerLifecycle({
1698
+ ...input,
1699
+ dispatch: false,
1700
+ store: this.store,
1701
+ authority: authority.data,
1702
+ authorityOwner: this.authorityOwner,
1703
+ caller: this.context.caller,
1704
+ now: trustedNow(this.context),
1705
+ });
1706
+ }
1707
+
1708
+ /** Host-only launch bookkeeping; no operation family exposes these two methods. */
1709
+ private verifyDispatch(
1710
+ stage: WorkerDispatchStage,
1711
+ input: WorkerDispatchRequest,
1712
+ workspaceId: string,
1713
+ ): Result<Provenance> {
1714
+ const verifier = this.context.nativeWorker?.verifyDispatch;
1715
+ if (!verifier)
1716
+ return failure("permission_denied", "native worker dispatch attestation is unavailable");
1717
+ let result: Result<Provenance>;
1718
+ try {
1719
+ result = verifier({
1720
+ observation: input.observation,
1721
+ expected: {
1722
+ taskId: input.taskId,
1723
+ workspaceId,
1724
+ workerId: input.workerId,
1725
+ expectedRevision: input.expectedRevision,
1726
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1727
+ stage,
1728
+ },
1729
+ caller: this.context.caller,
1730
+ });
1731
+ } catch (error) {
1732
+ return failure("permission_denied", `native worker dispatch failed: ${String(error)}`);
1733
+ }
1734
+ if (!result.ok || !validDispatchProvenance(result.data, this.context.caller, input.workerId))
1735
+ return failure("permission_denied", "worker dispatch was not attested by the host");
1736
+ return result;
1737
+ }
1738
+
1739
+ /**
1740
+ * Claim the launch slot of an exactly-`assigned` worker before the host spawns it.
1741
+ * The returned reservation is the only thing that can later record a truthful
1742
+ * "never started" stop, so it stays in the adapter process and is never persisted.
1743
+ */
1744
+ prepareWorkerDispatch(input: WorkerDispatchRequest): Result<WorkerDispatch> {
1745
+ const root = this.contextRootError();
1746
+ if (!root.ok) return root as Result<never>;
1747
+ if ((this.context.workerId ?? null) !== null)
1748
+ return failure("permission_denied", "helpers cannot dispatch workers");
1749
+ const task = this.store.readTask(input.taskId);
1750
+ if (!task.ok) return task as Result<never>;
1751
+ const workspace = this.store.readWorkspace();
1752
+ if (!workspace.ok) return workspace as Result<never>;
1753
+ if (!workspace.data) return failure("not_found", "workspace not found");
1754
+ if (task.data.workspaceId !== workspace.data.id)
1755
+ return failure("recovery_required", "worker workspace binding is invalid");
1756
+ if (task.data.status !== "active")
1757
+ return failure("invalid_transition", "paused or closed tasks cannot dispatch workers");
1758
+ const entry = task.data.workers.find((candidate) => candidate.id === input.workerId);
1759
+ if (!entry) return failure("not_found", "worker not found");
1760
+ if (entry.data.state !== "assigned" || entry.data.session !== null)
1761
+ return failure("invalid_transition", "worker is not awaiting dispatch");
1762
+ const attested = this.verifyDispatch("prepare", input, workspace.data.id);
1763
+ if (!attested.ok) return attested as Result<never>;
1764
+ const changed = this.store.mutateTaskAndWorkspace({
1765
+ taskId: task.data.id,
1766
+ expectedTaskRevision: input.expectedRevision,
1767
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1768
+ now: trustedNow(this.context),
1769
+ workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
1770
+ task: (current, mutation) => {
1771
+ const candidate = current.workers.find((item) => item.id === input.workerId);
1772
+ if (!candidate) return failure("not_found", "worker not found");
1773
+ if (candidate.data.state !== "assigned" || candidate.data.session !== null)
1774
+ return failure("invalid_transition", "worker is not awaiting dispatch");
1775
+ return success(mutation.revision, null, {
1776
+ ...current,
1777
+ workers: current.workers.map((item) =>
1778
+ item.id === input.workerId
1779
+ ? { ...item, recordedAt: mutation.now, provenance: attested.data }
1780
+ : item,
1781
+ ),
1782
+ });
1783
+ },
1784
+ });
1785
+ if (!changed.ok) return changed as Result<never>;
1786
+ const dispatch = {} as WorkerDispatch;
1787
+ dispatchReservations.set(dispatch, {
1788
+ owner: this.authorityOwner,
1789
+ store: this.store,
1790
+ root: this.store.root,
1791
+ caller: this.context.caller,
1792
+ taskId: task.data.id,
1793
+ workspaceId: workspace.data.id,
1794
+ workerId: input.workerId,
1795
+ });
1796
+ return success(changed.data.task.revision, changed.data.workspace.revision, dispatch);
1797
+ }
1798
+
1799
+ /**
1800
+ * Settle a live reservation exactly once: `started` binds the observed child session,
1801
+ * `not_started` records a host-attested stop with no session. Whichever lands first
1802
+ * consumes the reservation, so a launch and a cancellation cannot both win.
1803
+ */
1804
+ commitWorkerDispatch(input: WorkerDispatchCommit): Result<Entry<Worker>> {
1805
+ const root = this.contextRootError();
1806
+ if (!root.ok) return root as Result<never>;
1807
+ if ((this.context.workerId ?? null) !== null)
1808
+ return failure("permission_denied", "helpers cannot dispatch workers");
1809
+ const reservation = dispatchReservations.get(input.dispatch);
1810
+ if (
1811
+ !reservation ||
1812
+ reservation.owner !== this.authorityOwner ||
1813
+ reservation.store !== this.store ||
1814
+ reservation.root !== this.store.root ||
1815
+ !sameValue(reservation.caller, this.context.caller) ||
1816
+ reservation.taskId !== input.taskId ||
1817
+ reservation.workerId !== input.workerId
1818
+ )
1819
+ return failure("permission_denied", "worker dispatch reservation is not live");
1820
+ const workspace = this.store.readWorkspace();
1821
+ if (!workspace.ok) return workspace as Result<never>;
1822
+ if (!workspace.data) return failure("not_found", "workspace not found");
1823
+ if (workspace.data.id !== reservation.workspaceId)
1824
+ return failure("recovery_required", "worker workspace binding is invalid");
1825
+ const observation: NativeWorkerObservation = {
1826
+ taskId: input.taskId,
1827
+ workerId: input.workerId,
1828
+ expectedRevision: input.expectedRevision,
1829
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1830
+ state: input.outcome === "started" ? "running" : "stopped",
1831
+ session: input.session,
1832
+ observation: input.observation,
1833
+ };
1834
+ let authority: VerifiedWorker;
1835
+ if (input.outcome === "started") {
1836
+ if (!input.session) return failure("invalid_input", "a started worker requires a session");
1837
+ const verified = verifyNativeWorker(
1838
+ this.context.nativeWorker,
1839
+ {
1840
+ observation: input.observation,
1841
+ expected: { ...observation, workspaceId: workspace.data.id },
1842
+ caller: this.context.caller,
1843
+ },
1844
+ {
1845
+ owner: this.authorityOwner,
1846
+ store: this.store,
1847
+ root: this.store.root,
1848
+ caller: this.context.caller,
1849
+ },
1850
+ );
1851
+ if (!verified.ok) return verified as Result<never>;
1852
+ authority = verified.data;
1853
+ } else {
1854
+ if (input.session !== null)
1855
+ return failure("invalid_input", "a never-dispatched worker stops with no session");
1856
+ const attested = this.verifyDispatch("not_started", input, workspace.data.id);
1857
+ if (!attested.ok) return attested as Result<never>;
1858
+ authority = {};
1859
+ verifiedWorkers.set(authority, {
1860
+ taskId: input.taskId,
1861
+ workspaceId: workspace.data.id,
1862
+ workerId: input.workerId,
1863
+ expectedRevision: input.expectedRevision,
1864
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1865
+ state: "stopped",
1866
+ session: null,
1867
+ provenance: attested.data,
1868
+ owner: this.authorityOwner,
1869
+ store: this.store,
1870
+ root: this.store.root,
1871
+ caller: this.context.caller,
1872
+ });
1873
+ }
1874
+ const applied = applyWorkerLifecycle({
1875
+ ...observation,
1876
+ dispatch: input.outcome === "not_started",
1877
+ store: this.store,
1878
+ authority,
1879
+ authorityOwner: this.authorityOwner,
1880
+ caller: this.context.caller,
1881
+ now: trustedNow(this.context),
1882
+ });
1883
+ if (applied.ok) dispatchReservations.delete(input.dispatch);
1884
+ return applied;
1885
+ }
1886
+
1887
+ writer(request: unknown): Result<WorkspaceRecord> {
1888
+ const root = this.contextRootError();
1889
+ if (!root.ok) return root as Result<never>;
1890
+ const parsed = parseOperation("writer", request);
1891
+ if (!parsed.ok) return parsed as Result<never>;
1892
+ const input = parsed.data as any;
1893
+ const task = this.store.readTask(input.taskId);
1894
+ if (!task.ok) return task as Result<never>;
1895
+ const workspace = this.store.readWorkspace();
1896
+ if (!workspace.ok) return workspace as Result<never>;
1897
+ if (!workspace.data) return failure("not_found", "workspace not found");
1898
+ if (task.data.status !== "active")
1899
+ return failure("invalid_transition", "paused or closed tasks cannot own product writes");
1900
+ this.fillRevisions(input, task.data, workspace.data);
1901
+ const helperId = this.context.workerId ?? null;
1902
+ if (input.action === "acquire") {
1903
+ if (input.workerId === undefined) input.workerId = helperId;
1904
+ if (input.workerId !== helperId)
1905
+ return failure("permission_denied", "writer identity does not match the caller");
1906
+ if (workspace.data.writer) {
1907
+ if (workspace.data.writer.state === "uncertain")
1908
+ return failure("recovery_required", "checkout writer requires recovery", {
1909
+ owner: workspace.data.writer.owner,
1910
+ });
1911
+ const currentOwner = workspace.data.writer.owner;
1912
+ if (currentOwner.workerId) {
1913
+ const ownerWorker = task.data.workers.find(
1914
+ (candidate) => candidate.id === currentOwner.workerId,
1915
+ );
1916
+ if (ownerWorker?.data.state === "cancelling" || ownerWorker?.data.state === "unknown")
1917
+ return failure("recovery_required", "current worker has not stopped", {
1918
+ owner: currentOwner,
1919
+ });
1920
+ }
1921
+ // A lead-held writer transfers across lead sessions of the same
1922
+ // checkout: restarts end the owning session, and per-write ownership
1923
+ // checks keep concurrent sessions from both believing they hold it.
1924
+ // Worker-held writers never transfer this way.
1925
+ if (currentOwner.workerId !== null || helperId !== null)
1926
+ return failure("writer_conflict", "checkout already has a writer", {
1927
+ owner: currentOwner,
1928
+ });
1929
+ }
1930
+ if (helperId !== null) {
1931
+ const helper = this.helperEntry(task.data);
1932
+ if (!helper.ok) return helper as Result<never>;
1933
+ if (helper.data.data.assignment.role !== "implementer")
1934
+ return failure("permission_denied", "only implementers can own product writes");
1935
+ if (helper.data.data.state !== "running" || !helper.data.data.session)
1936
+ return failure(
1937
+ "permission_denied",
1938
+ "writer ownership requires an observed running worker",
1939
+ );
1940
+ if (!sameSession(helper.data.data.session, this.context))
1941
+ return failure("permission_denied", "worker session does not match the caller");
1942
+ }
1943
+ const owner = {
1944
+ taskId: task.data.id,
1945
+ workerId: helperId,
1946
+ session: {
1947
+ kind: "host" as const,
1948
+ host: this.context.caller.host,
1949
+ handle: this.context.caller.actor,
1950
+ },
1951
+ };
1952
+ const changed = this.store.mutateTaskAndWorkspace({
1953
+ taskId: task.data.id,
1954
+ expectedTaskRevision: input.expectedRevision,
1955
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1956
+ now: trustedNow(this.context),
1957
+ workspace: (current, mutation) =>
1958
+ success(mutation.revision, mutation.revision, {
1959
+ ...current,
1960
+ writer: { state: "held" as const, owner, acquiredAt: mutation.now },
1961
+ }),
1962
+ task: (current) => success(null, null, current),
1963
+ });
1964
+ if (!changed.ok) return changed as Result<never>;
1965
+ return success(
1966
+ changed.data.task.revision,
1967
+ changed.data.workspace.revision,
1968
+ changed.data.workspace,
1969
+ );
1970
+ }
1971
+ const current = workspace.data.writer;
1972
+ if (!current) return failure("permission_denied", "checkout has no writer owner");
1973
+ if (current.state === "uncertain")
1974
+ return failure("recovery_required", "checkout writer requires recovery", {
1975
+ owner: current.owner,
1976
+ });
1977
+ if (
1978
+ current.owner.taskId !== task.data.id ||
1979
+ current.owner.workerId !== helperId ||
1980
+ !sameSession(current.owner.session, this.context)
1981
+ )
1982
+ return failure("permission_denied", "only the current writer can release ownership");
1983
+ if (helperId !== null) {
1984
+ const helper = this.helperEntry(task.data);
1985
+ if (!helper.ok) return helper as Result<never>;
1986
+ if (helper.data.data.state !== "running")
1987
+ return failure("recovery_required", "worker must be observed stopped before release");
1988
+ }
1989
+ const changed = this.store.mutateTaskAndWorkspace({
1990
+ taskId: task.data.id,
1991
+ expectedTaskRevision: input.expectedRevision,
1992
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1993
+ now: trustedNow(this.context),
1994
+ workspace: (currentWorkspace, mutation) =>
1995
+ success(mutation.revision, mutation.revision, { ...currentWorkspace, writer: null }),
1996
+ task: (currentTask) => success(null, null, currentTask),
1997
+ });
1998
+ if (!changed.ok) return changed as Result<never>;
1999
+ return success(
2000
+ changed.data.task.revision,
2001
+ changed.data.workspace.revision,
2002
+ changed.data.workspace,
2003
+ );
2004
+ }
2005
+
2006
+ assertProductWriteAllowed(input: {
2007
+ task: TaskRecord;
2008
+ workspace: WorkspaceRecord;
2009
+ paths: string[];
2010
+ }) {
2011
+ return assertProductWriteAllowed({ ...input, caller: this.callerContext(), store: this.store });
2012
+ }
2013
+
2014
+ applicableDecision(
2015
+ taskId: string,
2016
+ purpose: Decision["purpose"],
2017
+ binding: Decision["binding"],
2018
+ ): Result<Entry<Decision>[]> {
2019
+ const root = this.contextRootError();
2020
+ if (!root.ok) return root as Result<never>;
2021
+ const task = this.store.readTask(taskId);
2022
+ if (!task.ok) return task as Result<never>;
2023
+ const entries = storedDecisionApplicable(this.store, task.data, purpose, binding);
2024
+ return success(task.data.revision, null, entries);
2025
+ }
2026
+
2027
+ reserveAction(
2028
+ input: Omit<
2029
+ ReserveActionInput,
2030
+ "store" | "native" | "authority" | "authorityOwner" | "authorityCaller"
2031
+ > & { observation: unknown },
2032
+ ): Result<ActionReservation> {
2033
+ const root = this.contextRootError();
2034
+ if (!root.ok) return root as Result<never>;
2035
+ if ((this.context.workerId ?? null) !== null)
2036
+ return failure("permission_denied", "helpers cannot reserve external actions");
2037
+ if (input.observation === undefined)
2038
+ return failure("invalid_input", "native action observation is required");
2039
+ const task = this.store.readTask(input.taskId);
2040
+ if (!task.ok) return task as Result<never>;
2041
+ const workspace = this.store.readWorkspace();
2042
+ if (!workspace.ok) return workspace as Result<never>;
2043
+ if (!workspace.data) return failure("not_found", "workspace not found");
2044
+ const decision = task.data.decisions.find((entry) => entry.id === input.decisionId);
2045
+ if (!decision) return failure("not_found", "decision not found");
2046
+ const authority = verifyNativeAction(
2047
+ this.context.nativeAuthority,
2048
+ {
2049
+ observation: input.observation,
2050
+ expected: {
2051
+ taskId: task.data.id,
2052
+ workspaceId: workspace.data.id,
2053
+ decisionId: input.decisionId,
2054
+ actionRef: input.actionRef,
2055
+ taskRevision: input.expectedRevision,
2056
+ workspaceRevision: input.expectedWorkspaceRevision,
2057
+ outcome: "reserve",
2058
+ decision: decision.data,
2059
+ },
2060
+ caller: this.context.caller,
2061
+ },
2062
+ { owner: this.authorityOwner, store: this.store, root: this.store.root },
2063
+ );
2064
+ if (!authority.ok) return authority as Result<never>;
2065
+ const operation = decisionOperation(decision.data.binding.approvedContent);
2066
+ if (operation) {
2067
+ const view = this.view(task.data);
2068
+ if (!view.ok) return view as Result<never>;
2069
+ const evaluations = new Map(
2070
+ view.data.requirements.map((entry) => [entry.requirementId, entry.status]),
2071
+ );
2072
+ const blocked = (view.data.task.policy?.requirements ?? []).filter(
2073
+ (requirement) =>
2074
+ requirement.dependentAction === operation &&
2075
+ (evaluations.get(requirement.id) === "unsatisfied" ||
2076
+ evaluations.get(requirement.id) === "unavailable"),
2077
+ );
2078
+ if (blocked.length)
2079
+ return failure(
2080
+ "requirements_unsatisfied",
2081
+ `${operation} is gated by unsatisfied requirements; record their evidence or an approved limitation waiver first`,
2082
+ { operation, requirementIds: blocked.map((requirement) => requirement.id) },
2083
+ );
2084
+ }
2085
+ return reserveBoundedAction({
2086
+ ...input,
2087
+ store: this.store,
2088
+ authority: authority.data,
2089
+ authorityOwner: this.authorityOwner,
2090
+ authorityCaller: this.context.caller,
2091
+ native: { now: trustedNow(this.context) },
2092
+ });
2093
+ }
2094
+
2095
+ settleAction(
2096
+ input: Omit<
2097
+ SettleActionInput,
2098
+ "store" | "native" | "authority" | "authorityOwner" | "authorityCaller"
2099
+ > & { observation: unknown },
2100
+ ): Result<Entry<Decision>> {
2101
+ const root = this.contextRootError();
2102
+ if (!root.ok) return root as Result<never>;
2103
+ if ((this.context.workerId ?? null) !== null)
2104
+ return failure("permission_denied", "helpers cannot settle external actions");
2105
+ if (input.observation === undefined)
2106
+ return failure("invalid_input", "native action observation is required");
2107
+ const task = this.store.readTask(input.taskId);
2108
+ if (!task.ok) return task as Result<never>;
2109
+ const workspace = this.store.readWorkspace();
2110
+ if (!workspace.ok) return workspace as Result<never>;
2111
+ if (!workspace.data) return failure("not_found", "workspace not found");
2112
+ const decision = task.data.decisions.find((entry) => entry.id === input.decisionId);
2113
+ if (!decision) return failure("not_found", "decision not found");
2114
+ const authority = verifyNativeAction(
2115
+ this.context.nativeAuthority,
2116
+ {
2117
+ observation: input.observation,
2118
+ expected: {
2119
+ taskId: task.data.id,
2120
+ workspaceId: workspace.data.id,
2121
+ decisionId: input.decisionId,
2122
+ actionRef: input.actionRef,
2123
+ taskRevision: input.taskRevision,
2124
+ workspaceRevision: input.workspaceRevision,
2125
+ outcome: input.outcome,
2126
+ decision: decision.data,
2127
+ },
2128
+ caller: this.context.caller,
2129
+ },
2130
+ { owner: this.authorityOwner, store: this.store, root: this.store.root },
2131
+ );
2132
+ if (!authority.ok) return authority as Result<never>;
2133
+ return settleBoundedAction({
2134
+ ...input,
2135
+ store: this.store,
2136
+ authority: authority.data,
2137
+ authorityOwner: this.authorityOwner,
2138
+ authorityCaller: this.context.caller,
2139
+ native: { now: trustedNow(this.context) },
2140
+ });
2141
+ }
2142
+
2143
+ reconcileAction(
2144
+ input: Omit<
2145
+ ReconcileActionInput,
2146
+ | "store"
2147
+ | "native"
2148
+ | "authority"
2149
+ | "authorityOwner"
2150
+ | "authorityCaller"
2151
+ | "taskRevision"
2152
+ | "workspaceRevision"
2153
+ > & {
2154
+ expectedRevision: string;
2155
+ expectedWorkspaceRevision: string;
2156
+ observation: unknown;
2157
+ },
2158
+ ): Result<Entry<Decision>> {
2159
+ const root = this.contextRootError();
2160
+ if (!root.ok) return root as Result<never>;
2161
+ if ((this.context.workerId ?? null) !== null)
2162
+ return failure("permission_denied", "helpers cannot reconcile external actions");
2163
+ const task = this.store.readTask(input.taskId);
2164
+ if (!task.ok) return task as Result<never>;
2165
+ const workspace = this.store.readWorkspace();
2166
+ if (!workspace.ok) return workspace as Result<never>;
2167
+ if (!workspace.data) return failure("not_found", "workspace not found");
2168
+ const decision = task.data.decisions.find((entry) => entry.id === input.decisionId);
2169
+ if (!decision) return failure("not_found", "decision not found");
2170
+ const authority = verifyNativeReconciliation(
2171
+ this.context.nativeAuthority,
2172
+ {
2173
+ observation: input.observation,
2174
+ expected: {
2175
+ taskId: task.data.id,
2176
+ workspaceId: workspace.data.id,
2177
+ decisionId: input.decisionId,
2178
+ actionRef: input.actionRef,
2179
+ taskRevision: input.expectedRevision,
2180
+ workspaceRevision: input.expectedWorkspaceRevision,
2181
+ outcome: input.outcome,
2182
+ evidenceDigest: input.evidenceDigest,
2183
+ ...(input.step ? { step: input.step } : {}),
2184
+ decision: decision.data,
2185
+ },
2186
+ caller: this.context.caller,
2187
+ },
2188
+ { owner: this.authorityOwner, store: this.store, root: this.store.root },
2189
+ );
2190
+ if (!authority.ok) return authority as Result<never>;
2191
+ return reconcileBoundedAction({
2192
+ ...input,
2193
+ taskRevision: input.expectedRevision,
2194
+ workspaceRevision: input.expectedWorkspaceRevision,
2195
+ store: this.store,
2196
+ authority: authority.data,
2197
+ authorityOwner: this.authorityOwner,
2198
+ authorityCaller: this.context.caller,
2199
+ native: { now: trustedNow(this.context) },
2200
+ });
2201
+ }
2202
+
2203
+ private resolve(task: TaskRecord, assessment: Assessment): Result<Policy> {
2204
+ return resolvePolicy({
2205
+ intent: task.intent.data,
2206
+ assessment,
2207
+ constraints: this.context.constraints,
2208
+ prior: {
2209
+ decisions: task.decisions
2210
+ .filter((entry) => entry.provenance.kind !== "imported")
2211
+ .map((entry) => entry.data),
2212
+ findings: task.findings
2213
+ .filter((entry) => entry.provenance.kind !== "imported")
2214
+ .map((entry) => entry.data),
2215
+ requirements: task.origin || task.policy === null ? [] : task.policy.requirements,
2216
+ },
2217
+ });
2218
+ }
2219
+
2220
+ private summary(task: TaskRecord): Result<TaskSummary> {
2221
+ const workspace = this.store.readWorkspace();
2222
+ if (!workspace.ok) return workspace as Result<never>;
2223
+ if (!workspace.data) return failure("not_found", "workspace not found");
2224
+ const current = captureCandidate(this.store.root, task.intent.data.scope, environment());
2225
+ if (!current.ok) return current as Result<never>;
2226
+ const requirements = evaluateRequirements(
2227
+ task,
2228
+ workspace.data,
2229
+ this.context.capabilities,
2230
+ current.data,
2231
+ this.store.root,
2232
+ this.context.caller,
2233
+ );
2234
+ return success(task.revision, workspace.data.revision, {
2235
+ id: task.id,
2236
+ revision: task.revision,
2237
+ workspaceRevision: workspace.data.revision,
2238
+ objective: task.intent.data.objective,
2239
+ status: task.status,
2240
+ closure: task.closure,
2241
+ progress: task.progress,
2242
+ policy: task.policy,
2243
+ requirements,
2244
+ writer: workspace.data.writer,
2245
+ });
2246
+ }
2247
+
2248
+ private view(task: TaskRecord): Result<TaskView> {
2249
+ const workspace = this.store.readWorkspace();
2250
+ if (!workspace.ok) return workspace as Result<never>;
2251
+ if (!workspace.data) return failure("not_found", "workspace not found");
2252
+ const current = captureCandidate(this.store.root, task.intent.data.scope, environment());
2253
+ if (!current.ok) return current as Result<never>;
2254
+ const requirements = evaluateRequirements(
2255
+ task,
2256
+ workspace.data,
2257
+ this.context.capabilities,
2258
+ current.data,
2259
+ this.store.root,
2260
+ this.context.caller,
2261
+ );
2262
+ return success(task.revision, workspace.data.revision, {
2263
+ task,
2264
+ workspace: workspace.data,
2265
+ capabilities: this.context.capabilities,
2266
+ requirements,
2267
+ evidence: evaluateEvidence(task, current.data),
2268
+ });
2269
+ }
2270
+
2271
+ reconcileResume(
2272
+ view: TaskView,
2273
+ observations: NativeWorkerObservation[] = [],
2274
+ ): Result<ResumeReconciliation> {
2275
+ const root = this.contextRootError();
2276
+ if (!root.ok) return root as Result<never>;
2277
+ if ((this.context.workerId ?? null) !== null)
2278
+ return failure("permission_denied", "helpers cannot reconcile task workers");
2279
+ if (view.task.workspaceId !== view.workspace.id)
2280
+ return failure("invalid_input", "task and workspace bindings are invalid");
2281
+ try {
2282
+ if (realpathSync(view.workspace.root) !== this.store.root)
2283
+ return failure("invalid_input", "task view root does not match the task store");
2284
+ } catch {
2285
+ return failure("invalid_input", "task view root cannot be resolved");
2286
+ }
2287
+ const updates: NativeWorkerObservation[] = [];
2288
+ const seen = new Set<string>();
2289
+ const binding: WorkerBinding = {
2290
+ owner: this.authorityOwner,
2291
+ store: this.store,
2292
+ root: this.store.root,
2293
+ caller: this.context.caller,
2294
+ };
2295
+ for (const observation of observations) {
2296
+ if (seen.has(observation.workerId))
2297
+ return failure("invalid_input", "worker observations must be unique");
2298
+ seen.add(observation.workerId);
2299
+ if (
2300
+ observation.taskId !== view.task.id ||
2301
+ observation.expectedRevision !== view.task.revision ||
2302
+ observation.expectedWorkspaceRevision !== view.workspace.revision
2303
+ )
2304
+ return failure("revision_conflict", "worker observation is stale");
2305
+ if (!view.task.workers.some((entry) => entry.id === observation.workerId))
2306
+ return failure("permission_denied", "worker observation is not assigned");
2307
+ const verified = verifyNativeWorker(
2308
+ this.context.nativeWorker,
2309
+ {
2310
+ observation: observation.observation,
2311
+ expected: { ...observation, workspaceId: view.workspace.id },
2312
+ caller: this.context.caller,
2313
+ },
2314
+ binding,
2315
+ );
2316
+ if (!verified.ok) return verified as Result<never>;
2317
+ if (!takeWorker(verified.data, binding, observation))
2318
+ return failure("permission_denied", "worker observation authority was not retained");
2319
+ updates.push(observation);
2320
+ }
2321
+ const observedStates = new Map(updates.map((entry) => [entry.workerId, entry.state]));
2322
+ const effectiveView: TaskView = {
2323
+ ...view,
2324
+ task: {
2325
+ ...view.task,
2326
+ workers: view.task.workers.map((entry) => {
2327
+ const update = updates.find((item) => item.workerId === entry.id);
2328
+ return update
2329
+ ? {
2330
+ ...entry,
2331
+ data: { ...entry.data, state: update.state, session: update.session },
2332
+ }
2333
+ : entry;
2334
+ }),
2335
+ },
2336
+ };
2337
+ const base = reconcileResumeContext(effectiveView);
2338
+ if (!base.ok) return base;
2339
+ const needsReconciliation = view.task.workers.some((entry) =>
2340
+ ["running", "cancelling", "unknown"].includes(
2341
+ observedStates.get(entry.id) ?? entry.data.state,
2342
+ ),
2343
+ );
2344
+ const blockers = [...base.data.blockers];
2345
+ if (
2346
+ needsReconciliation &&
2347
+ !blockers.some((entry) => entry.reason === "worker state requires reconciliation")
2348
+ )
2349
+ blockers.push({
2350
+ reason: "worker state requires reconciliation",
2351
+ dependentAction: "resume",
2352
+ refs: [],
2353
+ });
2354
+ return success(null, null, { ...base.data, workerUpdates: updates, blockers });
2355
+ }
2356
+
2357
+ private transition(input: any, status: "active" | "paused"): Result<TaskSummary> {
2358
+ const task = this.store.readTask(input.taskId);
2359
+ if (!task.ok) return task as Result<never>;
2360
+ const helper = this.helperTaskGuard(task.data);
2361
+ if (!helper.ok) return helper as Result<never>;
2362
+ if (
2363
+ (status === "paused" && task.data.status !== "active") ||
2364
+ (status === "active" && task.data.status !== "paused")
2365
+ )
2366
+ return failure("invalid_transition", `cannot transition ${task.data.status} to ${status}`);
2367
+ if (status === "active" && input.authorityRefs.length === 0)
2368
+ return failure("permission_denied", "resuming a task requires authority references");
2369
+ const workspace = this.store.readWorkspace();
2370
+ if (!workspace.ok) return workspace as Result<never>;
2371
+ if (!workspace.data) return failure("not_found", "workspace not found");
2372
+ this.fillRevisions(input, task.data, workspace.data);
2373
+ let resumeCandidate: import("./task-contract").Candidate | null = null;
2374
+ if (status === "active" && task.data.origin) {
2375
+ if (!task.data.policy)
2376
+ return failure("needs_input", "imported task requires a current policy");
2377
+ if (task.data.policy.policyVersion !== POLICY_VERSION)
2378
+ return failure(
2379
+ "needs_input",
2380
+ "stored policy version is unsupported; reassessment is required",
2381
+ );
2382
+ const view = this.view(task.data);
2383
+ if (!view.ok) return view as Result<never>;
2384
+ const current = reconcileResumeContext(view.data);
2385
+ if (!current.ok) return current as Result<never>;
2386
+ const requirements = view.data.requirements;
2387
+ if (
2388
+ requirements.some(
2389
+ (requirement) =>
2390
+ requirement.status === "unsatisfied" || requirement.status === "unavailable",
2391
+ )
2392
+ )
2393
+ return failure("requirements_unsatisfied", "imported task requires current requirements", {
2394
+ requirementIds: requirements
2395
+ .filter(
2396
+ (requirement) =>
2397
+ requirement.status === "unsatisfied" || requirement.status === "unavailable",
2398
+ )
2399
+ .map((requirement) => requirement.requirementId),
2400
+ });
2401
+ if (current.data.reassessmentRequired)
2402
+ return failure("needs_input", "imported task requires destination policy reassessment");
2403
+ if (current.data.staleEvidenceIds.length)
2404
+ return failure("needs_input", "imported task requires fresh destination evidence");
2405
+ if (
2406
+ current.data.blockers.some(
2407
+ (blocker) =>
2408
+ blocker.dependentAction === "resume" ||
2409
+ blocker.reason === "worker state requires reconciliation",
2410
+ )
2411
+ )
2412
+ return failure("recovery_required", "imported task requires resume reconciliation");
2413
+ if (!validResumeApproval(task.data, workspace.data, input.authorityRefs, this.context))
2414
+ return failure("permission_denied", "imported resume requires native destination approval");
2415
+ resumeCandidate = current.data.candidate;
2416
+ }
2417
+ const blocker = this.activeWorkerBlocker(task.data, workspace.data);
2418
+ if (blocker) return blocker as Result<never>;
2419
+ const changed = this.store.mutateTaskAndWorkspace({
2420
+ taskId: task.data.id,
2421
+ expectedTaskRevision: input.expectedRevision,
2422
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
2423
+ now: trustedNow(this.context),
2424
+ workspace: (workspace, context) => success(context.revision, context.revision, workspace),
2425
+ task: (current, context) =>
2426
+ success(context.revision, null, {
2427
+ ...current,
2428
+ status,
2429
+ candidates:
2430
+ resumeCandidate &&
2431
+ !current.candidates.some((candidate) => candidate.id === resumeCandidate!.id)
2432
+ ? [...current.candidates, resumeCandidate]
2433
+ : current.candidates,
2434
+ progress:
2435
+ status === "paused" ? { ...current.progress, summary: input.reason } : current.progress,
2436
+ }),
2437
+ });
2438
+ if (!changed.ok) return changed as Result<never>;
2439
+ return this.summary(changed.data.task);
2440
+ }
2441
+
2442
+ private progress(input: any): Result<TaskSummary> {
2443
+ const task = this.store.readTask(input.taskId);
2444
+ if (!task.ok) return task as Result<never>;
2445
+ const helper = this.helperTaskGuard(task.data);
2446
+ if (!helper.ok) return helper as Result<never>;
2447
+ if (task.data.status === "closed")
2448
+ return failure("invalid_transition", "closed task cannot update progress");
2449
+ this.fillRevisions(input, task.data);
2450
+ const changed = this.store.mutateTask(
2451
+ task.data.id,
2452
+ input.expectedRevision,
2453
+ (current, context) =>
2454
+ success(context.revision, null, { ...current, progress: input.progress }),
2455
+ trustedNow(this.context),
2456
+ );
2457
+ if (!changed.ok) return changed as Result<never>;
2458
+ return this.summary(changed.data);
2459
+ }
2460
+
2461
+ private revise(input: any): Result<TaskSummary> {
2462
+ const task = this.store.readTask(input.taskId);
2463
+ if (!task.ok) return task as Result<never>;
2464
+ const helper = this.helperTaskGuard(task.data);
2465
+ if (!helper.ok) return helper as Result<never>;
2466
+ if (task.data.status === "closed")
2467
+ return failure("invalid_transition", "closed task cannot be revised");
2468
+ const workspace = this.store.readWorkspace();
2469
+ if (!workspace.ok) return workspace as Result<never>;
2470
+ if (!workspace.data) return failure("not_found", "workspace not found");
2471
+ if (workspace.data.writer || task.data.workers.some((entry) => entry.data.state !== "stopped"))
2472
+ return failure(
2473
+ "recovery_required",
2474
+ "scope revision requires worker ownership reconciliation",
2475
+ );
2476
+ this.fillRevisions(input, task.data, workspace.data);
2477
+ const changed = this.store.mutateTaskAndWorkspace({
2478
+ taskId: task.data.id,
2479
+ expectedTaskRevision: input.expectedRevision,
2480
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
2481
+ now: trustedNow(this.context),
2482
+ workspace: (workspace, context) => success(context.revision, context.revision, workspace),
2483
+ task: (current, context) =>
2484
+ success(context.revision, null, {
2485
+ ...current,
2486
+ intent: {
2487
+ id: newId(),
2488
+ recordedAt: context.now,
2489
+ provenance: provenance(this.context),
2490
+ data: input.intent,
2491
+ },
2492
+ policy: null,
2493
+ progress: { ...current.progress, summary: `Policy invalidated: ${input.reason}` },
2494
+ }),
2495
+ });
2496
+ if (!changed.ok) return changed as Result<never>;
2497
+ return this.summary(changed.data.task);
2498
+ }
2499
+
2500
+ private close(input: any): Result<TaskSummary> {
2501
+ const task = this.store.readTask(input.taskId);
2502
+ if (!task.ok) return task as Result<never>;
2503
+ const helper = this.helperTaskGuard(task.data);
2504
+ if (!helper.ok) return helper as Result<never>;
2505
+ if (task.data.status !== "active" && task.data.status !== "paused")
2506
+ return failure("invalid_transition", "closed task cannot be reopened or closed again");
2507
+ const workspace = this.store.readWorkspace();
2508
+ if (!workspace.ok) return workspace as Result<never>;
2509
+ if (!workspace.data) return failure("not_found", "workspace not found");
2510
+ const blocker = this.activeWorkerBlocker(task.data, workspace.data);
2511
+ if (blocker) return blocker as Result<never>;
2512
+ this.fillRevisions(input, task.data, workspace.data);
2513
+ const current = captureCandidate(this.store.root, task.data.intent.data.scope, environment());
2514
+ if (!current.ok) return current as Result<never>;
2515
+ const withCandidate = current.data;
2516
+ const taskForView =
2517
+ withCandidate.id === task.data.candidates.at(-1)?.id
2518
+ ? task.data
2519
+ : { ...task.data, candidates: [...task.data.candidates, withCandidate] };
2520
+ const requirements = evaluateRequirements(
2521
+ taskForView,
2522
+ workspace.data,
2523
+ this.context.capabilities,
2524
+ withCandidate,
2525
+ this.store.root,
2526
+ this.context.caller,
2527
+ );
2528
+ const view = {
2529
+ task: taskForView,
2530
+ workspace: workspace.data,
2531
+ capabilities: this.context.capabilities,
2532
+ requirements,
2533
+ evidence: evaluateEvidence(taskForView, withCandidate),
2534
+ } satisfies TaskView;
2535
+ const closure = evaluateClosure(input.outcome, view);
2536
+ if (!closure.ok) return closure as Result<never>;
2537
+ const changed = this.store.mutateTaskAndWorkspace({
2538
+ taskId: task.data.id,
2539
+ expectedTaskRevision: input.expectedRevision,
2540
+ expectedWorkspaceRevision: input.expectedWorkspaceRevision,
2541
+ now: trustedNow(this.context),
2542
+ workspace: (value, context) => success(context.revision, context.revision, value),
2543
+ task: (value, context) =>
2544
+ success(context.revision, null, {
2545
+ ...value,
2546
+ status: "closed",
2547
+ candidates: value.candidates.some((item) => item.id === withCandidate.id)
2548
+ ? value.candidates
2549
+ : [...value.candidates, withCandidate],
2550
+ closure: {
2551
+ outcome: closure.data.outcome,
2552
+ at: context.now,
2553
+ summary: input.summary,
2554
+ decisionIds: closure.data.decisionIds,
2555
+ evidenceIds: closure.data.evidenceIds,
2556
+ },
2557
+ }),
2558
+ });
2559
+ if (!changed.ok) return changed as Result<never>;
2560
+ return this.summary(changed.data.task);
2561
+ }
2562
+ }