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