@narumitw/pi-subagents 0.53.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.
- package/README.md +85 -15
- package/package.json +1 -1
- package/src/agents/built-ins.ts +124 -0
- package/src/agents/catalog.ts +224 -0
- package/src/agents/discovery.ts +249 -0
- package/src/agents/types.ts +98 -0
- package/src/agents.ts +47 -670
- package/src/auto-transport.ts +2 -1
- package/src/automation.ts +7 -2
- package/src/capability-router.ts +1 -1
- package/src/completion-delivery.ts +82 -11
- package/src/config-status.ts +2 -2
- package/src/config-ui.ts +8 -8
- package/src/consult-resources.ts +1 -1
- package/src/consult.ts +9 -7
- package/src/create-stateful-transport.ts +2 -1
- package/src/cwd-policy.ts +1 -1
- package/src/execution/budget.ts +56 -0
- package/src/execution/runtime-policy.ts +19 -0
- package/src/execution-plan.ts +1 -1
- package/src/execution-profiles.ts +1 -1
- package/src/execution-ui.ts +1 -1
- package/src/execution.ts +269 -100
- package/src/in-process-transport.ts +3 -2
- package/src/inspect.ts +39 -8
- package/src/limits.ts +1 -0
- package/src/orchestration-metrics.ts +12 -5
- package/src/panel-execution.ts +1 -1
- package/src/panel-planning.ts +1 -1
- package/src/params.ts +3 -1
- package/src/persistence.ts +106 -7
- package/src/registry-types.ts +22 -5
- package/src/registry.ts +205 -37
- package/src/render.ts +1 -1
- package/src/retained-semantic-state.ts +1 -1
- package/src/rpc-transport-metadata.ts +1 -1
- package/src/rpc-transport.ts +2 -1
- package/src/runner.ts +6 -1
- package/src/settings/inspection.ts +275 -0
- package/src/settings/schema.ts +186 -0
- package/src/settings.ts +72 -420
- package/src/spawn-idempotency.ts +1 -1
- package/src/stateful-agent-view.ts +87 -0
- package/src/stateful-config.ts +1 -1
- package/src/stateful-guidance.ts +2 -2
- package/src/stateful-limits.ts +1 -1
- package/src/stateful-prompt.ts +2 -2
- package/src/stateful-render.ts +0 -1
- package/src/stateful-safety.ts +2 -1
- package/src/stateful-tool-params.ts +13 -20
- package/src/stateful.ts +36 -117
- package/src/subagents.ts +8 -9
- package/src/subprocess-transport.ts +2 -6
- package/src/transport-types.ts +1 -1
- package/src/transport-ui.ts +1 -1
- package/src/verification-harness.ts +516 -0
- package/src/verification-receipt.ts +275 -0
- package/src/verified-execution-benchmark.ts +86 -0
- package/src/verified-execution-contract.ts +219 -0
- package/src/work-item-ledger.ts +510 -37
- package/src/work-item-persistence.ts +31 -0
- package/src/workflow-completion-controller.ts +397 -0
- package/src/workflow-plan-compiler.ts +1 -1
- package/src/workflow-plan-patch.ts +1 -1
- package/src/workflow-planning.ts +11 -1
- package/src/workflow-ui.ts +1 -1
package/src/work-item-ledger.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ledger transitions and restore validators stay together so every persisted acceptance invariant
|
|
3
|
+
* is audited against the same atomic state machine rather than duplicated across modules.
|
|
4
|
+
*/
|
|
1
5
|
import {
|
|
2
6
|
type ManagedIntegrationCandidate,
|
|
3
7
|
type ManagedIntegrationExpectation,
|
|
4
8
|
verifyManagedIntegration,
|
|
5
9
|
} from "./integration-controller.js";
|
|
10
|
+
import type { VerificationSubmission } from "./verification-harness.js";
|
|
11
|
+
import { isVerificationReceipt, type VerificationReceipt } from "./verification-receipt.js";
|
|
6
12
|
import {
|
|
7
13
|
isWorkflowTreeIdentity,
|
|
8
14
|
sameWorkflowTreeIdentity,
|
|
@@ -13,8 +19,18 @@ import {
|
|
|
13
19
|
type WorkflowVerificationReceipt,
|
|
14
20
|
} from "./workflow-verification.js";
|
|
15
21
|
|
|
16
|
-
export const WORK_ITEM_LEDGER_VERSION = "pi-subagents:work-ledger:
|
|
17
|
-
const
|
|
22
|
+
export const WORK_ITEM_LEDGER_VERSION = "pi-subagents:work-ledger:v3" as const;
|
|
23
|
+
const LEGACY_WORK_ITEM_LEDGER_VERSIONS = new Set([
|
|
24
|
+
"pi-subagents:work-ledger:v1",
|
|
25
|
+
"pi-subagents:work-ledger:v2",
|
|
26
|
+
]);
|
|
27
|
+
export const WORK_ITEM_ACCEPTANCE_VERSION = "pi-subagents:work-acceptance:v1" as const;
|
|
28
|
+
export type WorkItemAcceptanceState =
|
|
29
|
+
| "not-required"
|
|
30
|
+
| "pending"
|
|
31
|
+
| "accepted"
|
|
32
|
+
| "rework-requested"
|
|
33
|
+
| "rejected";
|
|
18
34
|
|
|
19
35
|
export type WorkItemState =
|
|
20
36
|
| "pending"
|
|
@@ -53,9 +69,12 @@ export interface WorkItemDefinition {
|
|
|
53
69
|
writePaths?: string[];
|
|
54
70
|
ownershipKeys?: string[];
|
|
55
71
|
acceptanceCriteria?: string[];
|
|
72
|
+
requiredEvidence?: string[];
|
|
56
73
|
integrationOwner?: boolean;
|
|
57
74
|
verifierFor?: string;
|
|
58
75
|
dependencyPolicy?: "completed" | "settled";
|
|
76
|
+
acceptanceRequired?: boolean;
|
|
77
|
+
maxReworkCycles?: 0 | 1;
|
|
59
78
|
}
|
|
60
79
|
|
|
61
80
|
export interface WorkItemRecord {
|
|
@@ -81,12 +100,21 @@ export interface WorkItemRecord {
|
|
|
81
100
|
writePaths: string[];
|
|
82
101
|
ownershipKeys: string[];
|
|
83
102
|
acceptanceCriteria: string[];
|
|
103
|
+
requiredEvidence: string[];
|
|
84
104
|
integrationOwner: boolean;
|
|
85
105
|
verifierFor?: string;
|
|
86
106
|
dependencyPolicy: "completed" | "settled";
|
|
107
|
+
acceptanceStateVersion: typeof WORK_ITEM_ACCEPTANCE_VERSION;
|
|
108
|
+
acceptanceRequired: boolean;
|
|
109
|
+
acceptanceState: WorkItemAcceptanceState;
|
|
110
|
+
reworkCount: number;
|
|
111
|
+
maxReworkCycles: 0 | 1;
|
|
87
112
|
verificationAccepted: boolean;
|
|
88
113
|
stagedTreeIdentity?: WorkflowTreeIdentity;
|
|
89
114
|
verificationReceipt?: WorkflowVerificationReceipt;
|
|
115
|
+
acceptanceReceipt?: VerificationReceipt;
|
|
116
|
+
acceptanceReceiptHistory: VerificationReceipt[];
|
|
117
|
+
submission?: VerificationSubmission;
|
|
90
118
|
invalidationReasons: string[];
|
|
91
119
|
outcomeReason?: string;
|
|
92
120
|
}
|
|
@@ -114,12 +142,31 @@ export interface StageWorkItemVerificationInput extends CompleteWorkItemInput {
|
|
|
114
142
|
treeIdentity: WorkflowTreeIdentity;
|
|
115
143
|
}
|
|
116
144
|
|
|
145
|
+
export interface StageVerifiedAcceptanceInput
|
|
146
|
+
extends CompleteWorkItemInput,
|
|
147
|
+
VerificationSubmission {
|
|
148
|
+
executionPlanId: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
117
151
|
export interface CompleteWorkItemVerificationInput {
|
|
118
152
|
taskGeneration: number;
|
|
119
153
|
executionPlanId: string;
|
|
120
154
|
receipt: WorkflowVerificationReceipt;
|
|
121
155
|
}
|
|
122
156
|
|
|
157
|
+
export interface RecordVerificationDecisionInput {
|
|
158
|
+
taskGeneration: number;
|
|
159
|
+
executionPlanId: string;
|
|
160
|
+
receipt: VerificationReceipt;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface VerifiedIntegrationInput {
|
|
164
|
+
verifierId: string;
|
|
165
|
+
verifierTaskGeneration: number;
|
|
166
|
+
verifierExecutionPlanId: string;
|
|
167
|
+
receipt: VerificationReceipt;
|
|
168
|
+
}
|
|
169
|
+
|
|
123
170
|
const MAX_ITEMS = 64;
|
|
124
171
|
const MAX_IDENTIFIER_LENGTH = 256;
|
|
125
172
|
const MAX_TEXT_LENGTH = 16 * 1024;
|
|
@@ -179,6 +226,9 @@ export class WorkItemLedger {
|
|
|
179
226
|
complete(id: string, input: CompleteWorkItemInput): WorkItemRecord {
|
|
180
227
|
const item = this.require(id);
|
|
181
228
|
this.assertMutable(item);
|
|
229
|
+
if (item.acceptanceRequired) {
|
|
230
|
+
throw new Error(`WorkItem ${id} requires executor-owned acceptance staging`);
|
|
231
|
+
}
|
|
182
232
|
if (item.state !== "running") {
|
|
183
233
|
throw new Error(`WorkItem ${id} cannot complete while ${item.state}`);
|
|
184
234
|
}
|
|
@@ -191,6 +241,41 @@ export class WorkItemLedger {
|
|
|
191
241
|
item.verificationAccepted = false;
|
|
192
242
|
item.stagedTreeIdentity = undefined;
|
|
193
243
|
item.verificationReceipt = undefined;
|
|
244
|
+
item.acceptanceState = "not-required";
|
|
245
|
+
item.state = "completed";
|
|
246
|
+
item.generation = ++this.generation;
|
|
247
|
+
this.refreshReadyState();
|
|
248
|
+
return structuredClone(item);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
stageForVerifiedAcceptance(id: string, input: StageVerifiedAcceptanceInput): WorkItemRecord {
|
|
252
|
+
const item = this.require(id);
|
|
253
|
+
this.assertMutable(item);
|
|
254
|
+
if (!item.acceptanceRequired || !item.integrationOwner) {
|
|
255
|
+
throw new Error(`WorkItem ${id} is not a verified integration target`);
|
|
256
|
+
}
|
|
257
|
+
if (item.state !== "running") {
|
|
258
|
+
throw new Error(`WorkItem ${id} cannot stage acceptance while ${item.state}`);
|
|
259
|
+
}
|
|
260
|
+
if (input.taskGeneration !== item.taskGeneration) {
|
|
261
|
+
throw new Error(`WorkItem ${id} rejected a stale task generation`);
|
|
262
|
+
}
|
|
263
|
+
validatePlanId(input.executionPlanId, "staged execution plan");
|
|
264
|
+
validateSubmission(input);
|
|
265
|
+
item.acceptedExecutionPlanId = input.executionPlanId;
|
|
266
|
+
item.artifactHistory.push(...item.artifacts.map((artifact) => structuredClone(artifact)));
|
|
267
|
+
item.artifacts = normalizeArtifacts(input.artifacts ?? [], id, this.generation + 1);
|
|
268
|
+
item.verificationAccepted = false;
|
|
269
|
+
item.acceptanceState = "pending";
|
|
270
|
+
item.stagedTreeIdentity = structuredClone(input.treeIdentity);
|
|
271
|
+
item.submission = {
|
|
272
|
+
treeIdentity: structuredClone(input.treeIdentity),
|
|
273
|
+
baseRepositoryGeneration: input.baseRepositoryGeneration,
|
|
274
|
+
patchDigest: input.patchDigest,
|
|
275
|
+
changedPaths: [...input.changedPaths],
|
|
276
|
+
fileVersions: { ...input.fileVersions },
|
|
277
|
+
};
|
|
278
|
+
item.acceptanceReceipt = undefined;
|
|
194
279
|
item.state = "completed";
|
|
195
280
|
item.generation = ++this.generation;
|
|
196
281
|
this.refreshReadyState();
|
|
@@ -268,6 +353,80 @@ export class WorkItemLedger {
|
|
|
268
353
|
return { target: structuredClone(target), verifier: structuredClone(verifier) };
|
|
269
354
|
}
|
|
270
355
|
|
|
356
|
+
recordVerificationDecision(
|
|
357
|
+
verifierId: string,
|
|
358
|
+
input: RecordVerificationDecisionInput,
|
|
359
|
+
): { target: WorkItemRecord; verifier: WorkItemRecord } {
|
|
360
|
+
const verifier = this.require(verifierId);
|
|
361
|
+
if (verifier.state !== "running" || !verifier.verifierFor) {
|
|
362
|
+
throw new Error(`WorkItem ${verifierId} is not a running verifier`);
|
|
363
|
+
}
|
|
364
|
+
if (input.taskGeneration !== verifier.taskGeneration) {
|
|
365
|
+
throw new Error(`WorkItem ${verifierId} rejected a stale verifier generation`);
|
|
366
|
+
}
|
|
367
|
+
validatePlanId(input.executionPlanId, "verifier execution plan");
|
|
368
|
+
if (!isVerificationReceipt(input.receipt) || input.receipt.decision === "accept") {
|
|
369
|
+
throw new Error(`WorkItem ${verifierId} received an invalid non-acceptance receipt`);
|
|
370
|
+
}
|
|
371
|
+
const target = this.require(verifier.verifierFor);
|
|
372
|
+
assertVerifiedReceiptMatches(target, verifier, input.executionPlanId, input.receipt);
|
|
373
|
+
verifier.acceptedExecutionPlanId = input.executionPlanId;
|
|
374
|
+
verifier.state = "completed";
|
|
375
|
+
verifier.generation = ++this.generation;
|
|
376
|
+
target.acceptanceReceipt = structuredClone(input.receipt);
|
|
377
|
+
target.acceptanceReceiptHistory.push(structuredClone(input.receipt));
|
|
378
|
+
target.verificationAccepted = false;
|
|
379
|
+
if (input.receipt.decision === "rework" && target.reworkCount < target.maxReworkCycles) {
|
|
380
|
+
target.reworkCount++;
|
|
381
|
+
target.acceptanceState = "rework-requested";
|
|
382
|
+
target.state = "blocked";
|
|
383
|
+
target.outcomeReason = "verification-rework";
|
|
384
|
+
} else {
|
|
385
|
+
target.acceptanceState = "rejected";
|
|
386
|
+
target.state = "failed";
|
|
387
|
+
target.outcomeReason =
|
|
388
|
+
input.receipt.decision === "rework"
|
|
389
|
+
? "verification-rework-exhausted"
|
|
390
|
+
: "verification-rejected";
|
|
391
|
+
}
|
|
392
|
+
target.generation = ++this.generation;
|
|
393
|
+
this.invalidateDependents(target.id, verifier.id, target.outcomeReason);
|
|
394
|
+
this.refreshReadyState();
|
|
395
|
+
return { target: structuredClone(target), verifier: structuredClone(verifier) };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
beginVerificationRework(id: string): WorkItemRecord {
|
|
399
|
+
const target = this.require(id);
|
|
400
|
+
if (target.acceptanceState !== "rework-requested" || target.state !== "blocked") {
|
|
401
|
+
throw new Error(`WorkItem ${id} has no accepted rework request`);
|
|
402
|
+
}
|
|
403
|
+
const verifier = [...this.items.values()].find((item) => item.verifierFor === id);
|
|
404
|
+
if (verifier?.state !== "completed") {
|
|
405
|
+
throw new Error(`WorkItem ${id} has no completed verifier for rework`);
|
|
406
|
+
}
|
|
407
|
+
target.artifactHistory.push(...target.artifacts.map((artifact) => structuredClone(artifact)));
|
|
408
|
+
target.artifacts = [];
|
|
409
|
+
target.state = "pending";
|
|
410
|
+
target.acceptanceState = "pending";
|
|
411
|
+
target.taskGeneration++;
|
|
412
|
+
target.assignedAgentId = undefined;
|
|
413
|
+
target.acceptedExecutionPlanId = undefined;
|
|
414
|
+
target.stagedTreeIdentity = undefined;
|
|
415
|
+
target.submission = undefined;
|
|
416
|
+
target.verificationAccepted = false;
|
|
417
|
+
target.outcomeReason = undefined;
|
|
418
|
+
target.generation = ++this.generation;
|
|
419
|
+
verifier.state = "pending";
|
|
420
|
+
verifier.taskGeneration++;
|
|
421
|
+
verifier.assignedAgentId = undefined;
|
|
422
|
+
verifier.acceptedExecutionPlanId = undefined;
|
|
423
|
+
verifier.outcomeReason = undefined;
|
|
424
|
+
verifier.generation = ++this.generation;
|
|
425
|
+
this.resetReworkDependents(target.id, verifier.id);
|
|
426
|
+
this.refreshReadyState();
|
|
427
|
+
return structuredClone(target);
|
|
428
|
+
}
|
|
429
|
+
|
|
271
430
|
failVerification(verifierId: string, reason: string): WorkItemRecord[] {
|
|
272
431
|
const verifier = this.require(verifierId);
|
|
273
432
|
if (!verifier.verifierFor) throw new Error(`WorkItem ${verifierId} is not a verifier`);
|
|
@@ -279,8 +438,12 @@ export class WorkItemLedger {
|
|
|
279
438
|
verifier.outcomeReason = boundedReason;
|
|
280
439
|
verifier.generation = ++this.generation;
|
|
281
440
|
}
|
|
282
|
-
if (
|
|
441
|
+
if (
|
|
442
|
+
target.state === "awaiting-verification" ||
|
|
443
|
+
(target.state === "completed" && target.acceptanceState === "pending")
|
|
444
|
+
) {
|
|
283
445
|
target.state = "failed";
|
|
446
|
+
if (target.acceptanceRequired) target.acceptanceState = "rejected";
|
|
284
447
|
target.verificationAccepted = false;
|
|
285
448
|
target.outcomeReason = boundedReason;
|
|
286
449
|
target.generation = ++this.generation;
|
|
@@ -299,12 +462,14 @@ export class WorkItemLedger {
|
|
|
299
462
|
if (
|
|
300
463
|
item.state !== "running" &&
|
|
301
464
|
item.state !== "awaiting-verification" &&
|
|
465
|
+
!(item.state === "completed" && item.acceptanceState === "pending") &&
|
|
302
466
|
item.state !== "ready" &&
|
|
303
467
|
item.state !== "pending"
|
|
304
468
|
) {
|
|
305
469
|
throw new Error(`WorkItem ${id} cannot settle while ${item.state}`);
|
|
306
470
|
}
|
|
307
471
|
item.state = state;
|
|
472
|
+
if (item.acceptanceRequired) item.acceptanceState = "rejected";
|
|
308
473
|
item.outcomeReason = reason ? bounded(reason, MAX_TEXT_LENGTH) : undefined;
|
|
309
474
|
item.generation = ++this.generation;
|
|
310
475
|
this.refreshReadyState();
|
|
@@ -315,25 +480,64 @@ export class WorkItemLedger {
|
|
|
315
480
|
id: string,
|
|
316
481
|
expected: ManagedIntegrationExpectation,
|
|
317
482
|
candidate: ManagedIntegrationCandidate,
|
|
483
|
+
verified?: VerifiedIntegrationInput,
|
|
318
484
|
): WorkItemRecord {
|
|
319
485
|
const item = this.require(id);
|
|
320
486
|
if (!item.integrationOwner) {
|
|
321
487
|
throw new Error(`WorkItem ${id} is not the integration owner`);
|
|
322
488
|
}
|
|
323
|
-
if (item.state !== "running") {
|
|
324
|
-
throw new Error(`WorkItem ${id} cannot integrate while ${item.state}`);
|
|
325
|
-
}
|
|
326
489
|
if (expected.taskId !== id || expected.taskGeneration !== item.taskGeneration) {
|
|
327
490
|
throw new Error(`WorkItem ${id} integration expectation has a stale generation`);
|
|
328
491
|
}
|
|
492
|
+
if (!verified) {
|
|
493
|
+
if (item.state !== "running" || item.acceptanceRequired) {
|
|
494
|
+
throw new Error(`WorkItem ${id} cannot integrate while ${item.state}`);
|
|
495
|
+
}
|
|
496
|
+
verifyManagedIntegration(expected, candidate);
|
|
497
|
+
item.verificationAccepted = true;
|
|
498
|
+
item.generation = ++this.generation;
|
|
499
|
+
return structuredClone(item);
|
|
500
|
+
}
|
|
501
|
+
if (item.state !== "completed" || item.acceptanceState !== "pending") {
|
|
502
|
+
throw new Error(`WorkItem ${id} is not pending verified acceptance`);
|
|
503
|
+
}
|
|
504
|
+
const verifier = this.require(verified.verifierId);
|
|
505
|
+
if (
|
|
506
|
+
verifier.state !== "running" ||
|
|
507
|
+
verifier.verifierFor !== id ||
|
|
508
|
+
verified.verifierTaskGeneration !== verifier.taskGeneration
|
|
509
|
+
) {
|
|
510
|
+
throw new Error(`WorkItem ${verified.verifierId} is not the current verifier`);
|
|
511
|
+
}
|
|
512
|
+
if (!isVerificationReceipt(verified.receipt) || verified.receipt.decision !== "accept") {
|
|
513
|
+
throw new Error("Managed integration requires an accepting verification receipt");
|
|
514
|
+
}
|
|
515
|
+
assertVerifiedReceiptMatches(
|
|
516
|
+
item,
|
|
517
|
+
verifier,
|
|
518
|
+
verified.verifierExecutionPlanId,
|
|
519
|
+
verified.receipt,
|
|
520
|
+
);
|
|
329
521
|
verifyManagedIntegration(expected, candidate);
|
|
330
522
|
item.verificationAccepted = true;
|
|
523
|
+
item.acceptanceState = "accepted";
|
|
524
|
+
item.acceptanceReceipt = structuredClone(verified.receipt);
|
|
525
|
+
item.acceptanceReceiptHistory.push(structuredClone(verified.receipt));
|
|
526
|
+
item.artifacts = item.artifacts.map((artifact) => ({ ...artifact, verified: true }));
|
|
527
|
+
item.outcomeReason = undefined;
|
|
331
528
|
item.generation = ++this.generation;
|
|
529
|
+
verifier.acceptedExecutionPlanId = verified.verifierExecutionPlanId;
|
|
530
|
+
verifier.state = "completed";
|
|
531
|
+
verifier.generation = ++this.generation;
|
|
532
|
+
this.refreshReadyState();
|
|
332
533
|
return structuredClone(item);
|
|
333
534
|
}
|
|
334
535
|
|
|
335
536
|
rerun(id: string): WorkItemRecord {
|
|
336
537
|
const item = this.require(id);
|
|
538
|
+
if (item.acceptanceState === "accepted") {
|
|
539
|
+
throw new Error(`WorkItem ${id} has terminal accepted evidence`);
|
|
540
|
+
}
|
|
337
541
|
if (
|
|
338
542
|
!["blocked", "needs-input", "failed", "interrupted", "stale", "invalidated"].includes(
|
|
339
543
|
item.state,
|
|
@@ -347,8 +551,11 @@ export class WorkItemLedger {
|
|
|
347
551
|
item.acceptedExecutionPlanId = undefined;
|
|
348
552
|
item.outcomeReason = undefined;
|
|
349
553
|
item.verificationAccepted = false;
|
|
554
|
+
item.acceptanceState = item.acceptanceRequired ? "pending" : "not-required";
|
|
350
555
|
item.stagedTreeIdentity = undefined;
|
|
556
|
+
item.submission = undefined;
|
|
351
557
|
item.verificationReceipt = undefined;
|
|
558
|
+
item.acceptanceReceipt = undefined;
|
|
352
559
|
item.generation = ++this.generation;
|
|
353
560
|
this.refreshReadyState();
|
|
354
561
|
return structuredClone(item);
|
|
@@ -358,7 +565,7 @@ export class WorkItemLedger {
|
|
|
358
565
|
const root = this.require(id);
|
|
359
566
|
const normalizedReason = bounded(reason, MAX_TEXT_LENGTH);
|
|
360
567
|
if (!normalizedReason) throw new Error("WorkItem invalidation requires a reason");
|
|
361
|
-
const
|
|
568
|
+
const ordered: WorkItemRecord[] = [];
|
|
362
569
|
const queue = [root.id];
|
|
363
570
|
const seen = new Set<string>();
|
|
364
571
|
while (queue.length > 0) {
|
|
@@ -366,14 +573,20 @@ export class WorkItemLedger {
|
|
|
366
573
|
if (!currentId || seen.has(currentId)) continue;
|
|
367
574
|
seen.add(currentId);
|
|
368
575
|
const item = this.require(currentId);
|
|
369
|
-
item.
|
|
576
|
+
if (item.acceptanceState === "accepted") {
|
|
577
|
+
throw new Error(`WorkItem ${item.id} has terminal accepted evidence`);
|
|
578
|
+
}
|
|
579
|
+
ordered.push(item);
|
|
580
|
+
queue.push(...item.dependents);
|
|
581
|
+
}
|
|
582
|
+
return ordered.map((item) => {
|
|
583
|
+
item.state = item.id === root.id ? "stale" : "invalidated";
|
|
584
|
+
if (item.acceptanceRequired) item.acceptanceState = "rejected";
|
|
370
585
|
item.taskGeneration++;
|
|
371
586
|
item.invalidationReasons.push(`${root.id}:${normalizedReason}`);
|
|
372
587
|
item.generation = ++this.generation;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
return affected;
|
|
588
|
+
return structuredClone(item);
|
|
589
|
+
});
|
|
377
590
|
}
|
|
378
591
|
|
|
379
592
|
snapshot(): WorkItemLedgerSnapshot {
|
|
@@ -388,16 +601,18 @@ export class WorkItemLedger {
|
|
|
388
601
|
}
|
|
389
602
|
|
|
390
603
|
static restore(snapshot: WorkItemLedgerSnapshot): WorkItemLedger {
|
|
604
|
+
const storedVersion = (snapshot as { version?: unknown } | undefined)?.version;
|
|
391
605
|
if (
|
|
392
606
|
!snapshot ||
|
|
393
|
-
(
|
|
394
|
-
(
|
|
607
|
+
(storedVersion !== WORK_ITEM_LEDGER_VERSION &&
|
|
608
|
+
!LEGACY_WORK_ITEM_LEDGER_VERSIONS.has(String(storedVersion))) ||
|
|
395
609
|
!Number.isSafeInteger(snapshot.generation) ||
|
|
396
610
|
snapshot.generation < 0
|
|
397
611
|
) {
|
|
398
612
|
throw new Error("Unsupported or malformed WorkItem ledger snapshot");
|
|
399
613
|
}
|
|
400
|
-
const isLegacySnapshot =
|
|
614
|
+
const isLegacySnapshot = storedVersion !== WORK_ITEM_LEDGER_VERSION;
|
|
615
|
+
const isV1Snapshot = storedVersion === "pi-subagents:work-ledger:v1";
|
|
401
616
|
if (
|
|
402
617
|
!Array.isArray(snapshot.items) ||
|
|
403
618
|
snapshot.items.length < 1 ||
|
|
@@ -405,10 +620,13 @@ export class WorkItemLedger {
|
|
|
405
620
|
) {
|
|
406
621
|
throw new Error("Malformed WorkItem ledger items");
|
|
407
622
|
}
|
|
408
|
-
|
|
623
|
+
const storedItems = snapshot.items.map((item) =>
|
|
624
|
+
isLegacySnapshot ? normalizeLegacyAcceptance(item, isV1Snapshot) : item,
|
|
625
|
+
);
|
|
626
|
+
for (const item of storedItems) validateStoredRecord(item, snapshot.generation);
|
|
409
627
|
const ledger = WorkItemLedger.create({
|
|
410
628
|
workflowId: snapshot.workflowId,
|
|
411
|
-
items:
|
|
629
|
+
items: storedItems.map((item) => ({
|
|
412
630
|
id: item.id,
|
|
413
631
|
objective: item.objective,
|
|
414
632
|
dependencies: item.dependencies,
|
|
@@ -422,16 +640,25 @@ export class WorkItemLedger {
|
|
|
422
640
|
writePaths: item.writePaths,
|
|
423
641
|
ownershipKeys: item.ownershipKeys,
|
|
424
642
|
acceptanceCriteria: item.acceptanceCriteria,
|
|
643
|
+
requiredEvidence: item.requiredEvidence,
|
|
425
644
|
integrationOwner: item.integrationOwner,
|
|
426
645
|
verifierFor: item.verifierFor,
|
|
427
646
|
dependencyPolicy: item.dependencyPolicy,
|
|
647
|
+
acceptanceRequired: item.acceptanceRequired,
|
|
648
|
+
maxReworkCycles: item.maxReworkCycles,
|
|
428
649
|
})),
|
|
429
650
|
});
|
|
430
651
|
ledger.generation = snapshot.generation;
|
|
431
|
-
for (const stored of
|
|
652
|
+
for (const stored of storedItems) {
|
|
432
653
|
const item = ledger.require(stored.id);
|
|
654
|
+
const interruptedAcceptance =
|
|
655
|
+
stored.acceptanceRequired &&
|
|
656
|
+
stored.state === "completed" &&
|
|
657
|
+
stored.acceptanceState === "pending";
|
|
433
658
|
item.state =
|
|
434
|
-
stored.state === "running" ||
|
|
659
|
+
stored.state === "running" ||
|
|
660
|
+
stored.state === "awaiting-verification" ||
|
|
661
|
+
interruptedAcceptance
|
|
435
662
|
? "interrupted"
|
|
436
663
|
: stored.state;
|
|
437
664
|
item.generation = stored.generation;
|
|
@@ -443,25 +670,38 @@ export class WorkItemLedger {
|
|
|
443
670
|
stored.artifacts,
|
|
444
671
|
stored.id,
|
|
445
672
|
stored.generation,
|
|
446
|
-
!
|
|
673
|
+
!isV1Snapshot,
|
|
447
674
|
);
|
|
448
675
|
item.artifactHistory = normalizeStoredArtifacts(
|
|
449
676
|
stored.artifactHistory ?? [],
|
|
450
677
|
stored.id,
|
|
451
678
|
stored.generation,
|
|
452
|
-
!
|
|
679
|
+
!isV1Snapshot,
|
|
453
680
|
);
|
|
454
|
-
item.
|
|
455
|
-
item.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
681
|
+
item.acceptanceStateVersion = WORK_ITEM_ACCEPTANCE_VERSION;
|
|
682
|
+
item.acceptanceRequired = stored.acceptanceRequired;
|
|
683
|
+
item.acceptanceState = interruptedAcceptance ? "rejected" : stored.acceptanceState;
|
|
684
|
+
item.reworkCount = stored.reworkCount;
|
|
685
|
+
item.maxReworkCycles = stored.maxReworkCycles;
|
|
686
|
+
item.verificationAccepted = isV1Snapshot ? false : stored.verificationAccepted;
|
|
687
|
+
item.stagedTreeIdentity = stored.stagedTreeIdentity
|
|
688
|
+
? structuredClone(stored.stagedTreeIdentity)
|
|
689
|
+
: undefined;
|
|
459
690
|
item.verificationReceipt =
|
|
460
|
-
!
|
|
691
|
+
!isV1Snapshot && stored.verificationReceipt
|
|
461
692
|
? structuredClone(stored.verificationReceipt)
|
|
462
693
|
: undefined;
|
|
694
|
+
item.acceptanceReceipt = stored.acceptanceReceipt
|
|
695
|
+
? structuredClone(stored.acceptanceReceipt)
|
|
696
|
+
: undefined;
|
|
697
|
+
item.acceptanceReceiptHistory = stored.acceptanceReceiptHistory.map((receipt) =>
|
|
698
|
+
structuredClone(receipt),
|
|
699
|
+
);
|
|
700
|
+
item.submission = stored.submission ? structuredClone(stored.submission) : undefined;
|
|
463
701
|
item.invalidationReasons = [...stored.invalidationReasons];
|
|
464
|
-
item.outcomeReason =
|
|
702
|
+
item.outcomeReason = interruptedAcceptance
|
|
703
|
+
? "verification-interrupted"
|
|
704
|
+
: stored.outcomeReason;
|
|
465
705
|
}
|
|
466
706
|
ledger.validateRestoredVerificationLinks();
|
|
467
707
|
return ledger;
|
|
@@ -470,16 +710,32 @@ export class WorkItemLedger {
|
|
|
470
710
|
private validateRestoredVerificationLinks(): void {
|
|
471
711
|
for (const target of this.items.values()) {
|
|
472
712
|
const receipt = target.verificationReceipt;
|
|
473
|
-
if (
|
|
474
|
-
|
|
713
|
+
if (receipt) {
|
|
714
|
+
const verifier = this.items.get(receipt.verifierTaskId);
|
|
715
|
+
if (
|
|
716
|
+
!verifier ||
|
|
717
|
+
verifier.verifierFor !== target.id ||
|
|
718
|
+
verifier.state !== "completed" ||
|
|
719
|
+
verifier.taskGeneration !== receipt.verifierTaskGeneration ||
|
|
720
|
+
verifier.acceptedExecutionPlanId !== receipt.verifierExecutionPlanId
|
|
721
|
+
) {
|
|
722
|
+
throw new Error(`Malformed stored WorkItem verification link for ${target.id}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const acceptanceReceipt = target.acceptanceReceipt;
|
|
726
|
+
if (!acceptanceReceipt || target.acceptanceState === "pending") continue;
|
|
727
|
+
const verifier = this.items.get(acceptanceReceipt.verifierTaskId);
|
|
475
728
|
if (
|
|
476
729
|
!verifier ||
|
|
477
730
|
verifier.verifierFor !== target.id ||
|
|
478
731
|
verifier.state !== "completed" ||
|
|
479
|
-
verifier.taskGeneration !==
|
|
480
|
-
verifier.acceptedExecutionPlanId !==
|
|
732
|
+
verifier.taskGeneration !== acceptanceReceipt.verifierTaskGeneration ||
|
|
733
|
+
verifier.acceptedExecutionPlanId !== acceptanceReceipt.verifierExecutionPlanId ||
|
|
734
|
+
acceptanceReceipt.targetTaskId !== target.id ||
|
|
735
|
+
acceptanceReceipt.targetTaskGeneration !== target.taskGeneration ||
|
|
736
|
+
acceptanceReceipt.targetExecutionPlanId !== target.acceptedExecutionPlanId
|
|
481
737
|
) {
|
|
482
|
-
throw new Error(`Malformed stored WorkItem
|
|
738
|
+
throw new Error(`Malformed stored WorkItem acceptance link for ${target.id}`);
|
|
483
739
|
}
|
|
484
740
|
}
|
|
485
741
|
}
|
|
@@ -517,13 +773,23 @@ export class WorkItemLedger {
|
|
|
517
773
|
definition.acceptanceCriteria ?? [],
|
|
518
774
|
"acceptance criterion",
|
|
519
775
|
),
|
|
776
|
+
requiredEvidence: uniqueBounded(definition.requiredEvidence ?? [], "required evidence"),
|
|
520
777
|
integrationOwner: definition.integrationOwner === true,
|
|
521
778
|
verifierFor: definition.verifierFor,
|
|
522
779
|
dependencyPolicy: definition.dependencyPolicy ?? "completed",
|
|
780
|
+
acceptanceStateVersion: WORK_ITEM_ACCEPTANCE_VERSION,
|
|
781
|
+
acceptanceRequired: definition.acceptanceRequired === true,
|
|
782
|
+
acceptanceState: definition.acceptanceRequired === true ? "pending" : "not-required",
|
|
783
|
+
reworkCount: 0,
|
|
784
|
+
maxReworkCycles: definition.maxReworkCycles === 0 ? 0 : 1,
|
|
523
785
|
verificationAccepted: false,
|
|
524
786
|
stagedTreeIdentity: undefined,
|
|
525
787
|
verificationReceipt: undefined,
|
|
788
|
+
acceptanceReceipt: undefined,
|
|
789
|
+
acceptanceReceiptHistory: [],
|
|
790
|
+
submission: undefined,
|
|
526
791
|
invalidationReasons: [],
|
|
792
|
+
outcomeReason: undefined,
|
|
527
793
|
});
|
|
528
794
|
}
|
|
529
795
|
|
|
@@ -532,6 +798,9 @@ export class WorkItemLedger {
|
|
|
532
798
|
if (integrationOwners.length > 1)
|
|
533
799
|
throw new Error("Workflow can have only one integration owner");
|
|
534
800
|
for (const item of this.items.values()) {
|
|
801
|
+
if (item.acceptanceRequired && !item.integrationOwner) {
|
|
802
|
+
throw new Error(`WorkItem ${item.id} requires acceptance but is not integration owner`);
|
|
803
|
+
}
|
|
535
804
|
for (const dependency of item.dependencies) {
|
|
536
805
|
if (dependency === item.id) throw new Error(`WorkItem ${item.id} has a self cycle`);
|
|
537
806
|
const parent = this.items.get(dependency);
|
|
@@ -564,20 +833,26 @@ export class WorkItemLedger {
|
|
|
564
833
|
for (const item of this.items.values()) {
|
|
565
834
|
if (item.state !== "pending") continue;
|
|
566
835
|
const dependencies = item.dependencies.map((id) => this.require(id));
|
|
836
|
+
const acceptedDependency = (dependency: WorkItemRecord) =>
|
|
837
|
+
dependency.state === "completed" &&
|
|
838
|
+
(!dependency.acceptanceRequired || dependency.acceptanceState === "accepted");
|
|
567
839
|
const dependenciesReady = item.verifierFor
|
|
568
840
|
? dependencies.every((dependency) =>
|
|
569
841
|
dependency.id === item.verifierFor
|
|
570
|
-
? dependency.
|
|
571
|
-
|
|
842
|
+
? dependency.acceptanceRequired
|
|
843
|
+
? dependency.state === "completed" && dependency.acceptanceState === "pending"
|
|
844
|
+
: dependency.state === "awaiting-verification"
|
|
845
|
+
: acceptedDependency(dependency),
|
|
572
846
|
)
|
|
573
847
|
: item.dependencyPolicy === "settled"
|
|
574
848
|
? dependencies.every(
|
|
575
849
|
(dependency) =>
|
|
850
|
+
(!dependency.acceptanceRequired || dependency.acceptanceState === "accepted") &&
|
|
576
851
|
!["pending", "ready", "running", "awaiting-verification"].includes(
|
|
577
852
|
dependency.state,
|
|
578
853
|
),
|
|
579
854
|
)
|
|
580
|
-
: dependencies.every(
|
|
855
|
+
: dependencies.every(acceptedDependency);
|
|
581
856
|
if (!dependenciesReady) continue;
|
|
582
857
|
const available = new Map<string, WorkArtifactReference>();
|
|
583
858
|
for (const dependency of dependencies) {
|
|
@@ -599,6 +874,39 @@ export class WorkItemLedger {
|
|
|
599
874
|
}
|
|
600
875
|
}
|
|
601
876
|
|
|
877
|
+
private resetReworkDependents(targetId: string, excludedId: string): void {
|
|
878
|
+
const target = this.require(targetId);
|
|
879
|
+
const reworkReason = `${targetId}:verification-rework`;
|
|
880
|
+
const queue = target.dependents.filter((id) => id !== excludedId);
|
|
881
|
+
const seen = new Set<string>();
|
|
882
|
+
while (queue.length > 0) {
|
|
883
|
+
const currentId = queue.shift();
|
|
884
|
+
if (!currentId || seen.has(currentId)) continue;
|
|
885
|
+
seen.add(currentId);
|
|
886
|
+
const current = this.require(currentId);
|
|
887
|
+
queue.push(...current.dependents.filter((id) => id !== excludedId));
|
|
888
|
+
if (current.state !== "invalidated" || current.invalidationReasons.at(-1) !== reworkReason) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
current.artifactHistory.push(
|
|
892
|
+
...current.artifacts.map((artifact) => structuredClone(artifact)),
|
|
893
|
+
);
|
|
894
|
+
current.artifacts = [];
|
|
895
|
+
current.inputArtifactVersions = {};
|
|
896
|
+
current.state = "pending";
|
|
897
|
+
current.assignedAgentId = undefined;
|
|
898
|
+
current.acceptedExecutionPlanId = undefined;
|
|
899
|
+
current.outcomeReason = undefined;
|
|
900
|
+
current.verificationAccepted = false;
|
|
901
|
+
current.acceptanceState = current.acceptanceRequired ? "pending" : "not-required";
|
|
902
|
+
current.stagedTreeIdentity = undefined;
|
|
903
|
+
current.submission = undefined;
|
|
904
|
+
current.verificationReceipt = undefined;
|
|
905
|
+
current.acceptanceReceipt = undefined;
|
|
906
|
+
current.generation = ++this.generation;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
602
910
|
private invalidateDependents(
|
|
603
911
|
targetId: string,
|
|
604
912
|
excludedId: string,
|
|
@@ -631,7 +939,10 @@ export class WorkItemLedger {
|
|
|
631
939
|
}
|
|
632
940
|
|
|
633
941
|
private assertMutable(item: WorkItemRecord): void {
|
|
634
|
-
if (
|
|
942
|
+
if (
|
|
943
|
+
TERMINAL_STATES.has(item.state) &&
|
|
944
|
+
!(item.state === "completed" && item.acceptanceState === "pending")
|
|
945
|
+
) {
|
|
635
946
|
throw new Error(`WorkItem ${item.id} is terminal (${item.state})`);
|
|
636
947
|
}
|
|
637
948
|
}
|
|
@@ -657,10 +968,108 @@ function assertReceiptMatches(
|
|
|
657
968
|
}
|
|
658
969
|
}
|
|
659
970
|
|
|
971
|
+
function assertVerifiedReceiptMatches(
|
|
972
|
+
target: WorkItemRecord,
|
|
973
|
+
verifier: WorkItemRecord,
|
|
974
|
+
verifierExecutionPlanId: string,
|
|
975
|
+
receipt: VerificationReceipt,
|
|
976
|
+
): void {
|
|
977
|
+
if (
|
|
978
|
+
!target.acceptanceRequired ||
|
|
979
|
+
target.acceptanceState !== "pending" ||
|
|
980
|
+
target.state !== "completed" ||
|
|
981
|
+
!target.submission ||
|
|
982
|
+
receipt.targetTaskId !== target.id ||
|
|
983
|
+
receipt.targetTaskGeneration !== target.taskGeneration ||
|
|
984
|
+
receipt.targetExecutionPlanId !== target.acceptedExecutionPlanId ||
|
|
985
|
+
receipt.verifierTaskId !== verifier.id ||
|
|
986
|
+
receipt.verifierTaskGeneration !== verifier.taskGeneration ||
|
|
987
|
+
receipt.verifierExecutionPlanId !== verifierExecutionPlanId ||
|
|
988
|
+
receipt.verifierAgent !== verifier.assignedAgentId?.replace(/^agent:/u, "") ||
|
|
989
|
+
!sameWorkflowTreeIdentity(receipt.beforeTreeIdentity, target.submission.treeIdentity) ||
|
|
990
|
+
!sameWorkflowTreeIdentity(receipt.afterTreeIdentity, target.submission.treeIdentity) ||
|
|
991
|
+
receipt.baseRepositoryGeneration !== target.submission.baseRepositoryGeneration ||
|
|
992
|
+
receipt.patchDigest !== target.submission.patchDigest ||
|
|
993
|
+
JSON.stringify(receipt.changedPaths) !== JSON.stringify(target.submission.changedPaths) ||
|
|
994
|
+
JSON.stringify(receipt.allowedScopes) !== JSON.stringify(target.writePaths) ||
|
|
995
|
+
JSON.stringify(receipt.dependencyVersions) !== JSON.stringify(target.inputArtifactVersions) ||
|
|
996
|
+
JSON.stringify(receipt.readSetVersions) !== JSON.stringify(target.submission.fileVersions) ||
|
|
997
|
+
JSON.stringify(receipt.acceptanceCriteria) !== JSON.stringify(target.acceptanceCriteria) ||
|
|
998
|
+
JSON.stringify(receipt.requiredEvidenceIds) !== JSON.stringify(target.requiredEvidence)
|
|
999
|
+
) {
|
|
1000
|
+
throw new Error("WorkItem verification receipt has stale or mismatched managed identity");
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
660
1004
|
function validatePlanId(value: string, label: string): void {
|
|
661
1005
|
if (!/^[a-f0-9]{64}$/u.test(value)) throw new Error(`Invalid ${label}`);
|
|
662
1006
|
}
|
|
663
1007
|
|
|
1008
|
+
function validateSubmission(value: VerificationSubmission): void {
|
|
1009
|
+
if (
|
|
1010
|
+
!isWorkflowTreeIdentity(value.treeIdentity) ||
|
|
1011
|
+
!/^[a-f0-9]{40,64}$/u.test(value.baseRepositoryGeneration) ||
|
|
1012
|
+
!/^[a-f0-9]{64}$/u.test(value.patchDigest) ||
|
|
1013
|
+
!Array.isArray(value.changedPaths) ||
|
|
1014
|
+
value.changedPaths.length > MAX_LIST_ITEMS ||
|
|
1015
|
+
value.changedPaths.some(
|
|
1016
|
+
(item) =>
|
|
1017
|
+
typeof item !== "string" ||
|
|
1018
|
+
!item ||
|
|
1019
|
+
item.length > 4096 ||
|
|
1020
|
+
item.startsWith("/") ||
|
|
1021
|
+
item.startsWith("\\") ||
|
|
1022
|
+
item.split(/[\\/]/u).includes(".."),
|
|
1023
|
+
) ||
|
|
1024
|
+
new Set(value.changedPaths).size !== value.changedPaths.length ||
|
|
1025
|
+
value.changedPaths.reduce((total, item) => total + Buffer.byteLength(item, "utf8"), 0) >
|
|
1026
|
+
8 * 1024 ||
|
|
1027
|
+
JSON.stringify([...value.changedPaths].sort((left, right) => left.localeCompare(right))) !==
|
|
1028
|
+
JSON.stringify(value.changedPaths) ||
|
|
1029
|
+
!value.fileVersions ||
|
|
1030
|
+
typeof value.fileVersions !== "object" ||
|
|
1031
|
+
Array.isArray(value.fileVersions) ||
|
|
1032
|
+
Object.entries(value.fileVersions).length > MAX_LIST_ITEMS ||
|
|
1033
|
+
JSON.stringify(
|
|
1034
|
+
Object.keys(value.fileVersions).sort((left, right) => left.localeCompare(right)),
|
|
1035
|
+
) !== JSON.stringify(value.changedPaths) ||
|
|
1036
|
+
Object.entries(value.fileVersions).some(
|
|
1037
|
+
([key, digest]) =>
|
|
1038
|
+
!key ||
|
|
1039
|
+
key.length > 4096 ||
|
|
1040
|
+
typeof digest !== "string" ||
|
|
1041
|
+
(digest !== "deleted" && !/^[a-f0-9]{64}$/u.test(digest)),
|
|
1042
|
+
)
|
|
1043
|
+
) {
|
|
1044
|
+
throw new Error("WorkItem received malformed verification submission metadata");
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function validSubmission(value: VerificationSubmission): boolean {
|
|
1049
|
+
try {
|
|
1050
|
+
validateSubmission(value);
|
|
1051
|
+
return true;
|
|
1052
|
+
} catch {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function normalizeLegacyAcceptance(item: WorkItemRecord, isV1: boolean): WorkItemRecord {
|
|
1058
|
+
return {
|
|
1059
|
+
...structuredClone(item),
|
|
1060
|
+
requiredEvidence: [],
|
|
1061
|
+
acceptanceStateVersion: WORK_ITEM_ACCEPTANCE_VERSION,
|
|
1062
|
+
acceptanceRequired: false,
|
|
1063
|
+
acceptanceState: "not-required",
|
|
1064
|
+
reworkCount: 0,
|
|
1065
|
+
maxReworkCycles: 1,
|
|
1066
|
+
verificationAccepted: isV1 ? false : item.verificationAccepted,
|
|
1067
|
+
acceptanceReceipt: undefined,
|
|
1068
|
+
acceptanceReceiptHistory: [],
|
|
1069
|
+
submission: undefined,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
664
1073
|
function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): void {
|
|
665
1074
|
const states: WorkItemState[] = [
|
|
666
1075
|
"pending",
|
|
@@ -702,6 +1111,17 @@ function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): v
|
|
|
702
1111
|
item.selectedAgentName.length === 0 ||
|
|
703
1112
|
item.selectedAgentName.length > MAX_IDENTIFIER_LENGTH ||
|
|
704
1113
|
item.selectedAgentName.trim() !== item.selectedAgentName)) ||
|
|
1114
|
+
item.acceptanceStateVersion !== WORK_ITEM_ACCEPTANCE_VERSION ||
|
|
1115
|
+
typeof item.acceptanceRequired !== "boolean" ||
|
|
1116
|
+
!["not-required", "pending", "accepted", "rework-requested", "rejected"].includes(
|
|
1117
|
+
item.acceptanceState,
|
|
1118
|
+
) ||
|
|
1119
|
+
!Number.isSafeInteger(item.reworkCount) ||
|
|
1120
|
+
item.reworkCount < 0 ||
|
|
1121
|
+
item.reworkCount > 1 ||
|
|
1122
|
+
(item.maxReworkCycles !== 0 && item.maxReworkCycles !== 1) ||
|
|
1123
|
+
(item.acceptanceRequired && !item.integrationOwner) ||
|
|
1124
|
+
(!item.acceptanceRequired && item.acceptanceState !== "not-required") ||
|
|
705
1125
|
typeof item.integrationOwner !== "boolean" ||
|
|
706
1126
|
!(["completed", "settled"] as const).includes(item.dependencyPolicy) ||
|
|
707
1127
|
!(["read-only", "idempotent", "mutating"] as const).includes(item.sideEffectPolicy) ||
|
|
@@ -716,6 +1136,7 @@ function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): v
|
|
|
716
1136
|
item.writePaths,
|
|
717
1137
|
item.ownershipKeys,
|
|
718
1138
|
item.acceptanceCriteria,
|
|
1139
|
+
item.requiredEvidence,
|
|
719
1140
|
].every(
|
|
720
1141
|
(values) =>
|
|
721
1142
|
Array.isArray(values) &&
|
|
@@ -766,16 +1187,68 @@ function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): v
|
|
|
766
1187
|
(item.verificationReceipt !== undefined &&
|
|
767
1188
|
(!isWorkflowVerificationReceipt(item.verificationReceipt) ||
|
|
768
1189
|
!storedVerificationMatchesItem(item, item.verificationReceipt))) ||
|
|
1190
|
+
(item.acceptanceReceipt !== undefined &&
|
|
1191
|
+
(!isVerificationReceipt(item.acceptanceReceipt) ||
|
|
1192
|
+
(item.acceptanceState !== "pending" &&
|
|
1193
|
+
!storedAcceptanceMatchesItem(item, item.acceptanceReceipt)))) ||
|
|
1194
|
+
!Array.isArray(item.acceptanceReceiptHistory) ||
|
|
1195
|
+
item.acceptanceReceiptHistory.length > 2 ||
|
|
1196
|
+
!item.acceptanceReceiptHistory.every(isVerificationReceipt) ||
|
|
1197
|
+
(item.acceptanceReceipt !== undefined &&
|
|
1198
|
+
JSON.stringify(item.acceptanceReceiptHistory.at(-1)) !==
|
|
1199
|
+
JSON.stringify(item.acceptanceReceipt)) ||
|
|
1200
|
+
item.reworkCount !==
|
|
1201
|
+
Math.min(
|
|
1202
|
+
item.maxReworkCycles,
|
|
1203
|
+
item.acceptanceReceiptHistory.filter((receipt) => receipt.decision === "rework").length,
|
|
1204
|
+
) ||
|
|
1205
|
+
(item.submission !== undefined && !validSubmission(item.submission)) ||
|
|
1206
|
+
(item.acceptanceRequired &&
|
|
1207
|
+
item.state === "completed" &&
|
|
1208
|
+
item.acceptanceState === "pending" &&
|
|
1209
|
+
(!item.submission || !item.acceptedExecutionPlanId)) ||
|
|
1210
|
+
(item.acceptanceState === "accepted" &&
|
|
1211
|
+
(item.acceptanceReceipt?.decision !== "accept" || !item.verificationAccepted)) ||
|
|
1212
|
+
(item.acceptanceState === "rejected" &&
|
|
1213
|
+
(!["failed", "interrupted", "stale", "invalidated", "blocked", "needs-input"].includes(
|
|
1214
|
+
item.state,
|
|
1215
|
+
) ||
|
|
1216
|
+
(item.acceptanceReceipt !== undefined &&
|
|
1217
|
+
item.acceptanceReceipt.decision !== "reject" &&
|
|
1218
|
+
item.acceptanceReceipt.decision !== "rework"))) ||
|
|
1219
|
+
(item.acceptanceState === "rework-requested" &&
|
|
1220
|
+
(item.state !== "blocked" || item.acceptanceReceipt?.decision !== "rework")) ||
|
|
769
1221
|
(item.state === "awaiting-verification" &&
|
|
770
1222
|
(!item.stagedTreeIdentity || !item.acceptedExecutionPlanId)) ||
|
|
771
1223
|
(item.stagedTreeIdentity !== undefined &&
|
|
772
1224
|
item.state === "completed" &&
|
|
1225
|
+
!item.acceptanceRequired &&
|
|
773
1226
|
item.verificationReceipt === undefined)
|
|
774
1227
|
) {
|
|
775
1228
|
throw new Error(`Malformed stored WorkItem ${String(item?.id ?? "unknown")}`);
|
|
776
1229
|
}
|
|
777
1230
|
}
|
|
778
1231
|
|
|
1232
|
+
function storedAcceptanceMatchesItem(item: WorkItemRecord, receipt: VerificationReceipt): boolean {
|
|
1233
|
+
return (
|
|
1234
|
+
item.acceptanceRequired &&
|
|
1235
|
+
item.submission !== undefined &&
|
|
1236
|
+
receipt.targetTaskId === item.id &&
|
|
1237
|
+
receipt.targetTaskGeneration === item.taskGeneration &&
|
|
1238
|
+
receipt.targetExecutionPlanId === item.acceptedExecutionPlanId &&
|
|
1239
|
+
sameWorkflowTreeIdentity(receipt.beforeTreeIdentity, item.submission.treeIdentity) &&
|
|
1240
|
+
sameWorkflowTreeIdentity(receipt.afterTreeIdentity, item.submission.treeIdentity) &&
|
|
1241
|
+
receipt.baseRepositoryGeneration === item.submission.baseRepositoryGeneration &&
|
|
1242
|
+
receipt.patchDigest === item.submission.patchDigest &&
|
|
1243
|
+
JSON.stringify(receipt.changedPaths) === JSON.stringify(item.submission.changedPaths) &&
|
|
1244
|
+
JSON.stringify(receipt.allowedScopes) === JSON.stringify(item.writePaths) &&
|
|
1245
|
+
JSON.stringify(receipt.dependencyVersions) === JSON.stringify(item.inputArtifactVersions) &&
|
|
1246
|
+
JSON.stringify(receipt.readSetVersions) === JSON.stringify(item.submission.fileVersions) &&
|
|
1247
|
+
JSON.stringify(receipt.acceptanceCriteria) === JSON.stringify(item.acceptanceCriteria) &&
|
|
1248
|
+
JSON.stringify(receipt.requiredEvidenceIds) === JSON.stringify(item.requiredEvidence)
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
779
1252
|
function storedVerificationMatchesItem(
|
|
780
1253
|
item: WorkItemRecord,
|
|
781
1254
|
receipt: WorkflowVerificationReceipt,
|