@zq-silk/yui 0.8.9 → 0.9.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/ARCHITECTURE.md +48 -46
- package/README.md +33 -31
- package/dist/cli/commandCatalog.js +7 -7
- package/dist/cli.js +1 -31
- package/dist/commands/executionAuditCommands.js +2 -2
- package/dist/commands/taskCommands.js +110 -307
- package/dist/commands/taskCompletionGate.js +15 -12
- package/dist/commands/taskContextCommand.js +18 -11
- package/dist/commands/taskNextActionCommand.js +4 -5
- package/dist/commands/taskWorkspaceCommands.js +2 -2
- package/dist/execution/executionGroup.js +0 -3
- package/dist/executor/agentExecutor.js +4 -2
- package/dist/executor/effectiveLaunch.js +33 -3
- package/dist/integration/gitIntegrationService.js +1 -1
- package/dist/lifecycle/exactRunTerminalization.js +12 -8
- package/dist/observability/orchestrationMetrics.js +5 -19
- package/dist/profile/agentProfile.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +2 -0
- package/dist/repository/taskWorkspacePreparer.js +173 -26
- package/dist/review/reviewRound.js +41 -24
- package/dist/storage/migration/productionRegistry.js +138 -0
- package/dist/storage/sqliteStore.js +1 -1
- package/dist/storage/taskStore.js +26 -27
- package/dist/task/completionReadiness.js +24 -22
- package/dist/task/nextAction.js +47 -61
- package/dist/task/task.js +12 -19
- package/dist/web/assets/client/components.js +3 -1
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/webSnapshot.js +0 -3
- package/i18n/README.zh-CN.md +18 -19
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +47 -43
- package/skills/yui-operator/SKILL.md +24 -33
- package/skills/yui-reviewer/SKILL.md +18 -12
- package/skills/yui-worker/SKILL.md +5 -3
|
@@ -36,13 +36,13 @@ import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coord
|
|
|
36
36
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
37
37
|
import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
38
38
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
39
|
-
import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask,
|
|
39
|
+
import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
40
40
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
41
41
|
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
42
42
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
43
43
|
import { resolveProject } from "../repository/project.js";
|
|
44
44
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
45
|
-
import { currentWorkItemCandidate,
|
|
45
|
+
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
46
46
|
import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
|
|
47
47
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
48
48
|
import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
@@ -367,13 +367,12 @@ function taskProjectCommand(args, store, options) {
|
|
|
367
367
|
}
|
|
368
368
|
function updateTaskCommand(args, store, options) {
|
|
369
369
|
const optionNames = new Set([
|
|
370
|
-
"--title", "--description", "--priority", "--tags", "--due-at"
|
|
370
|
+
"--title", "--type", "--description", "--priority", "--tags", "--due-at"
|
|
371
371
|
]);
|
|
372
372
|
const flagOptions = new Set([
|
|
373
|
-
"--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at"
|
|
374
|
-
"--require-integration"
|
|
373
|
+
"--clear-type", "--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at"
|
|
375
374
|
]);
|
|
376
|
-
const usage = "Task update usage: yui task update <id> [--title <text>] [--description <text>|--clear-description] [--priority <low|medium|high|urgent>|--clear-priority] [--tags <comma-separated>|--clear-tags] [--due-at <RFC3339>|--clear-due-at]
|
|
375
|
+
const usage = "Task update usage: yui task update <id> [--title <text>] [--type <project-defined-type>|--clear-type] [--description <text>|--clear-description] [--priority <low|medium|high|urgent>|--clear-priority] [--tags <comma-separated>|--clear-tags] [--due-at <RFC3339>|--clear-due-at].";
|
|
377
376
|
const parsed = parseTail(args, optionNames, usage, flagOptions);
|
|
378
377
|
exactPositionals(parsed.positionals, 1, usage);
|
|
379
378
|
if (parsed.options.size === 0)
|
|
@@ -397,44 +396,16 @@ function updateTaskCommand(args, store, options) {
|
|
|
397
396
|
const tags = parsed.options.has("--tags")
|
|
398
397
|
? parseTaskTags(requiredOption(parsed.options, "--tags"))
|
|
399
398
|
: undefined;
|
|
400
|
-
const requestedDelivery = parsed.options.has("--delivery")
|
|
401
|
-
? parseTaskDelivery(requiredOption(parsed.options, "--delivery"))
|
|
402
|
-
: undefined;
|
|
403
|
-
if (requestedDelivery === "direct" && parsed.options.has("--require-integration")) {
|
|
404
|
-
throw usageError("--delivery direct conflicts with --require-integration.", usage);
|
|
405
|
-
}
|
|
406
|
-
const enableIntegration = requestedDelivery === "integrated"
|
|
407
|
-
|| parsed.options.has("--require-integration");
|
|
408
399
|
const now = clock(options);
|
|
409
400
|
const result = store.transaction((tx) => {
|
|
410
401
|
const current = requireTask(tx, parsed.positionals[0]);
|
|
411
402
|
if (current.status === "archived")
|
|
412
403
|
throw usageError(`Task is archived: ${current.id}.`);
|
|
413
|
-
if ((requestedDelivery !== undefined || parsed.options.has("--require-integration"))
|
|
414
|
-
&& current.projectBindings.length === 0) {
|
|
415
|
-
throw usageError(`Task ${current.id} has no Project; delivery selection is not applicable.`);
|
|
416
|
-
}
|
|
417
|
-
if (requestedDelivery === "direct" && current.requireIntegration === true) {
|
|
418
|
-
throw usageError(`Task ${current.id} already uses integrated delivery and cannot be downgraded to direct.`);
|
|
419
|
-
}
|
|
420
|
-
if (enableIntegration && current.status === "completed") {
|
|
421
|
-
throw usageError(`Task ${current.id} is completed; use task reopen before enabling integration evidence.`);
|
|
422
|
-
}
|
|
423
|
-
if (enableIntegration && current.requireIntegration !== true) {
|
|
424
|
-
assertTaskDeliveryPromotionEligible(tx, current, options.directTaskMainSnapshot);
|
|
425
|
-
}
|
|
426
|
-
if (parsed.options.size === 1
|
|
427
|
-
&& enableIntegration
|
|
428
|
-
&& current.requireIntegration === true) {
|
|
429
|
-
return { task: current, integrationState: "already-enabled" };
|
|
430
|
-
}
|
|
431
|
-
if (parsed.options.size === 1
|
|
432
|
-
&& requestedDelivery === "direct"
|
|
433
|
-
&& taskDeliveryPath(current) === "direct") {
|
|
434
|
-
return { task: current, integrationState: "already-direct" };
|
|
435
|
-
}
|
|
436
404
|
const updated = updateTaskMetadata(current, {
|
|
437
405
|
...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
|
|
406
|
+
...(parsed.options.has("--type")
|
|
407
|
+
? { type: requiredOption(parsed.options, "--type") }
|
|
408
|
+
: parsed.options.has("--clear-type") ? { type: null } : {}),
|
|
438
409
|
...(parsed.options.has("--description")
|
|
439
410
|
? { description: requiredOption(parsed.options, "--description") }
|
|
440
411
|
: parsed.options.has("--clear-description") ? { description: null } : {}),
|
|
@@ -446,94 +417,18 @@ function updateTaskCommand(args, store, options) {
|
|
|
446
417
|
: { tags }),
|
|
447
418
|
...(dueAt === undefined
|
|
448
419
|
? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
|
|
449
|
-
: { dueAt })
|
|
450
|
-
...(enableIntegration ? { requireIntegration: true } : {})
|
|
420
|
+
: { dueAt })
|
|
451
421
|
}, now);
|
|
452
422
|
tx.saveTask(updated);
|
|
453
423
|
recordTaskEvent(tx, updated.id, "task.updated", {
|
|
454
424
|
status: updated.status,
|
|
455
|
-
...(
|
|
456
|
-
? {}
|
|
457
|
-
: {
|
|
458
|
-
completionEvidence: enableIntegration
|
|
459
|
-
? "integration-required"
|
|
460
|
-
: "direct",
|
|
461
|
-
deliveryPath: taskDeliveryPath(updated)
|
|
462
|
-
})
|
|
425
|
+
...(updated.type === undefined ? {} : { taskType: updated.type })
|
|
463
426
|
}, now);
|
|
464
427
|
enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
|
|
465
|
-
return
|
|
466
|
-
task: updated,
|
|
467
|
-
integrationState: enableIntegration
|
|
468
|
-
? "enabled"
|
|
469
|
-
: requestedDelivery === "direct"
|
|
470
|
-
? "direct"
|
|
471
|
-
: "unchanged"
|
|
472
|
-
};
|
|
428
|
+
return updated;
|
|
473
429
|
});
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
477
|
-
}
|
|
478
|
-
return result.integrationState === "enabled"
|
|
479
|
-
? `Updated task ${result.task.id}\nDelivery: integrated (WorkItem, ChangeSet, and committed Integration required)\n`
|
|
480
|
-
: result.integrationState === "already-enabled"
|
|
481
|
-
? `Task ${result.task.id} already uses integrated delivery\n`
|
|
482
|
-
: result.integrationState === "already-direct"
|
|
483
|
-
? `Task ${result.task.id} already uses direct delivery\n`
|
|
484
|
-
: result.integrationState === "direct"
|
|
485
|
-
? `Updated task ${result.task.id}\nDelivery: direct\n`
|
|
486
|
-
: `Updated task ${result.task.id}\n`;
|
|
487
|
-
}
|
|
488
|
-
function assertTaskDeliveryPromotionEligible(store, task, snapshot) {
|
|
489
|
-
const evidence = [
|
|
490
|
-
...store.listWorkItems(task.id).map(({ id }) => `WorkItem ${id}`),
|
|
491
|
-
...store.listChangeSets(task.id).map(({ id }) => `ChangeSet ${id}`),
|
|
492
|
-
...store.listIntegrationAttempts(task.id).map(({ id }) => `IntegrationAttempt ${id}`),
|
|
493
|
-
...store.listReviewRounds(task.id).map(({ id }) => `ReviewRound ${id}`)
|
|
494
|
-
];
|
|
495
|
-
if (evidence.length > 0) {
|
|
496
|
-
throw usageError(`Task ${task.id} cannot promote to integrated delivery after delivery evidence exists: `
|
|
497
|
-
+ `${evidence.join(", ")}. Create an integrated replacement Task or keep the current direct contract.`);
|
|
498
|
-
}
|
|
499
|
-
if (task.status !== "draft" && task.status !== "active") {
|
|
500
|
-
throw usageError(`Task ${task.id} must be Draft or Active to promote delivery; current status is ${task.status}.`);
|
|
501
|
-
}
|
|
502
|
-
const workspace = store.getTaskWorkspace(task.id);
|
|
503
|
-
if (task.status === "draft" && workspace === null)
|
|
504
|
-
return;
|
|
505
|
-
if (snapshot === undefined) {
|
|
506
|
-
throw usageError(`Task ${task.id} delivery promotion requires a CLI-verified clean Task-main snapshot.`);
|
|
507
|
-
}
|
|
508
|
-
if (workspace === null
|
|
509
|
-
|| workspace.owner.type !== "task"
|
|
510
|
-
|| workspace.owner.taskId !== task.id) {
|
|
511
|
-
throw usageError(`Task has no authoritative main workspace: ${task.id}.`);
|
|
512
|
-
}
|
|
513
|
-
const snapshotIds = snapshot.schemaVersion === 1 && Array.isArray(snapshot.projects)
|
|
514
|
-
? snapshot.projects.map(({ projectId }) => projectId)
|
|
515
|
-
: [];
|
|
516
|
-
if (snapshotIds.length !== task.projectBindings.length
|
|
517
|
-
|| new Set(snapshotIds).size !== snapshotIds.length) {
|
|
518
|
-
throw usageError(`Task-main promotion snapshot does not match bound Projects: ${task.id}.`);
|
|
519
|
-
}
|
|
520
|
-
for (const binding of task.projectBindings) {
|
|
521
|
-
const project = snapshot.projects.find(({ projectId }) => projectId === binding.projectId);
|
|
522
|
-
const entry = workspace.entries.find(({ projectId }) => projectId === binding.projectId);
|
|
523
|
-
if (project === undefined
|
|
524
|
-
|| entry === undefined
|
|
525
|
-
|| entry.access !== "write"
|
|
526
|
-
|| project.directory !== entry.directory
|
|
527
|
-
|| project.branch !== entry.branch
|
|
528
|
-
|| project.baseCommit !== entry.baseCommit) {
|
|
529
|
-
throw usageError(`Task-main promotion snapshot changed before mutation: ${task.id}/${binding.projectId}.`);
|
|
530
|
-
}
|
|
531
|
-
if (project.headCommit !== project.baseCommit) {
|
|
532
|
-
throw usageError(`Task ${task.id} main already advanced for Project ${binding.projectId}; `
|
|
533
|
-
+ "cannot promote without losing ChangeSet provenance. Create an integrated replacement "
|
|
534
|
-
+ "Task or keep the current direct contract.");
|
|
535
|
-
}
|
|
536
|
-
}
|
|
430
|
+
notifyMailbox(options.runtime, taskMailbox(result.id), result.id);
|
|
431
|
+
return `Updated task ${result.id}\n`;
|
|
537
432
|
}
|
|
538
433
|
/** Compatibility helper for call sites that cannot yet handle foreground enter. */
|
|
539
434
|
export function runTaskOutputCommand(args, store, options = {}) {
|
|
@@ -569,25 +464,22 @@ function createTaskCommand(args, store, options) {
|
|
|
569
464
|
const now = clock(options);
|
|
570
465
|
const created = store.transaction((tx) => createTaskAggregate(tx, parsed.title, {
|
|
571
466
|
projectBindings: parsed.projectBindings,
|
|
572
|
-
...(parsed.
|
|
467
|
+
...(parsed.type === undefined ? {} : { type: parsed.type })
|
|
573
468
|
}, now, parsed.defaultProjectIds));
|
|
574
469
|
notifyMailbox(options.runtime, taskMailbox(created.task.id), created.task.id);
|
|
575
470
|
return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
|
|
576
471
|
+ `Assigned role: ${created.leader.name}\n`
|
|
577
|
-
+ `
|
|
578
|
-
+ (created.task.
|
|
579
|
-
? "
|
|
580
|
-
:
|
|
581
|
-
? "Completion: clean committed Task main required; no WorkItem, ChangeSet, IntegrationAttempt, or managed ReviewRound required\n"
|
|
582
|
-
: "Completion: no Project delivery evidence required\n"), {
|
|
472
|
+
+ `Type: ${created.task.type ?? "unspecified"}\n`
|
|
473
|
+
+ (created.task.projectBindings.length > 0
|
|
474
|
+
? "Execution: Leader decides whether independent WorkItems are warranted\n"
|
|
475
|
+
: "Execution: no Project delivery evidence required\n"), {
|
|
583
476
|
task: created.task,
|
|
584
|
-
leader: created.leader
|
|
585
|
-
deliveryPath: taskDeliveryPath(created.task)
|
|
477
|
+
leader: created.leader
|
|
586
478
|
});
|
|
587
479
|
}
|
|
588
480
|
function parseTaskCreation(args, store) {
|
|
589
|
-
const usage = "Task create usage: yui task create <title> [--
|
|
590
|
-
const parsed = parseMultiValueTail(args, new Set(["--
|
|
481
|
+
const usage = "Task create usage: yui task create <title> [--type <project-defined-type>] [--project <project> ...] [--base <project>=<ref> ...].";
|
|
482
|
+
const parsed = parseMultiValueTail(args, new Set(["--type"]), new Set(["--project", "--base"]), usage);
|
|
591
483
|
exactPositionals(parsed.positionals, 1, usage);
|
|
592
484
|
const projectReferences = parsed.multiOptions.get("--project") ?? [];
|
|
593
485
|
const baseOptions = parsed.multiOptions.get("--base") ?? [];
|
|
@@ -603,16 +495,6 @@ function parseTaskCreation(args, store) {
|
|
|
603
495
|
if (new Set(projects.map(({ id }) => id)).size !== projects.length) {
|
|
604
496
|
throw usageError("A Task cannot bind the same Project more than once.");
|
|
605
497
|
}
|
|
606
|
-
const requestedDelivery = parsed.options.has("--delivery")
|
|
607
|
-
? parseTaskDelivery(requiredOption(parsed.options, "--delivery"))
|
|
608
|
-
: undefined;
|
|
609
|
-
if ((requestedDelivery !== undefined || parsed.options.has("--require-integration"))
|
|
610
|
-
&& projects.length === 0) {
|
|
611
|
-
throw usageError("Delivery selection requires at least one --project.", usage);
|
|
612
|
-
}
|
|
613
|
-
if (requestedDelivery === "direct" && parsed.options.has("--require-integration")) {
|
|
614
|
-
throw usageError("--delivery direct conflicts with --require-integration.", usage);
|
|
615
|
-
}
|
|
616
498
|
const bases = new Map();
|
|
617
499
|
for (const option of baseOptions) {
|
|
618
500
|
const separator = option.indexOf("=");
|
|
@@ -637,14 +519,15 @@ function parseTaskCreation(args, store) {
|
|
|
637
519
|
.map(({ id }) => id);
|
|
638
520
|
return {
|
|
639
521
|
title: parsed.positionals[0],
|
|
522
|
+
...(parsed.options.has("--type")
|
|
523
|
+
? { type: requiredOption(parsed.options, "--type") }
|
|
524
|
+
: {}),
|
|
640
525
|
projectBindings: projects.map((project) => ({
|
|
641
526
|
projectId: project.id,
|
|
642
527
|
directory: project.name,
|
|
643
528
|
baseRef: bases.get(project.id) ?? project.developmentBranch
|
|
644
529
|
})),
|
|
645
|
-
defaultProjectIds
|
|
646
|
-
requireIntegration: requestedDelivery === "integrated"
|
|
647
|
-
|| parsed.options.has("--require-integration")
|
|
530
|
+
defaultProjectIds
|
|
648
531
|
};
|
|
649
532
|
}
|
|
650
533
|
function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []) {
|
|
@@ -654,7 +537,7 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
|
|
|
654
537
|
store.saveRole(task.id, leader);
|
|
655
538
|
recordTaskEvent(store, task.id, "task.created", {
|
|
656
539
|
status: task.status,
|
|
657
|
-
|
|
540
|
+
...(task.type === undefined ? {} : { taskType: task.type }),
|
|
658
541
|
...(defaultProjectIds.length === 0
|
|
659
542
|
? {}
|
|
660
543
|
: { defaultProjectIds: defaultProjectIds.join(",") })
|
|
@@ -703,16 +586,14 @@ function showTaskCommand(args, store) {
|
|
|
703
586
|
`Task: ${task.id}`,
|
|
704
587
|
`Title: ${task.title}`,
|
|
705
588
|
`Status: ${task.status}`,
|
|
706
|
-
`
|
|
589
|
+
`Type: ${task.type ?? "unspecified"}`,
|
|
707
590
|
...(task.description === undefined ? [] : [`Description: ${task.description}`]),
|
|
708
591
|
...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
|
|
709
592
|
...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
|
|
710
593
|
...(task.dueAt === undefined ? [] : [`Due: ${presentTime(task.dueAt, timeZone)}`]),
|
|
711
|
-
`
|
|
712
|
-
? "
|
|
713
|
-
:
|
|
714
|
-
? "clean committed Task main required"
|
|
715
|
-
: "no Project evidence required"}`,
|
|
594
|
+
`Execution topology: ${task.projectBindings.length > 0
|
|
595
|
+
? "Leader-owned; WorkItems are optional independent delivery units"
|
|
596
|
+
: "no Project delivery evidence required"}`,
|
|
716
597
|
...(task.completedAt === undefined ? [] : [`Completed: ${presentTime(task.completedAt, timeZone)}`]),
|
|
717
598
|
...(task.completedBy === undefined ? [] : [`Completed by: ${task.completedBy}`]),
|
|
718
599
|
...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
|
|
@@ -741,12 +622,7 @@ function showTaskCommand(args, store) {
|
|
|
741
622
|
`Created: ${presentTime(task.createdAt, timeZone)}`,
|
|
742
623
|
`Updated: ${presentTime(task.updatedAt, timeZone)}`
|
|
743
624
|
].join("\n").concat("\n");
|
|
744
|
-
return output(rendered, {
|
|
745
|
-
task,
|
|
746
|
-
deliveryPath: taskDeliveryPath(task),
|
|
747
|
-
counts,
|
|
748
|
-
hasBrief: brief !== null
|
|
749
|
-
});
|
|
625
|
+
return output(rendered, { task, counts, hasBrief: brief !== null });
|
|
750
626
|
}
|
|
751
627
|
function activateTaskCommand(args, store, options) {
|
|
752
628
|
exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
|
|
@@ -860,7 +736,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
860
736
|
terminalizedLeaderRun = true;
|
|
861
737
|
}
|
|
862
738
|
// A final (Task-scoped) review is the Task delivery policy: it reviews the
|
|
863
|
-
// complete
|
|
739
|
+
// complete frozen Task heads, not a single WorkItem Candidate. If the
|
|
864
740
|
// review config requests a final review, create the Task ReviewRound here
|
|
865
741
|
// and return it for dispatch instead of completing the Task.
|
|
866
742
|
const pendingFinalReviewIds = new Set(tx.listReviewRounds(task.id)
|
|
@@ -915,7 +791,6 @@ function completeTaskCommand(args, store, options) {
|
|
|
915
791
|
recordTaskEvent(tx, task.id, "task.completed", {
|
|
916
792
|
by: actor,
|
|
917
793
|
summary,
|
|
918
|
-
deliveryPath: taskDeliveryPath(task),
|
|
919
794
|
...(actualTaskCandidate === undefined
|
|
920
795
|
? {}
|
|
921
796
|
: {
|
|
@@ -2518,14 +2393,16 @@ function executionTargetForWorkItem(taskId, workItemId, revision, item, workspac
|
|
|
2518
2393
|
}
|
|
2519
2394
|
function executionTargetForReviewRound(task, round, item, candidate) {
|
|
2520
2395
|
const taskScope = (round.scope ?? "work-item") === "task";
|
|
2396
|
+
if (!taskScope && (item === undefined || candidate === undefined)) {
|
|
2397
|
+
throw dataError(`WorkItem ReviewRound target is missing its Candidate: ${round.id}.`);
|
|
2398
|
+
}
|
|
2521
2399
|
const projects = taskScope
|
|
2522
2400
|
? round.taskCandidate?.projects ?? []
|
|
2523
2401
|
: candidate.gitSnapshot?.projects ?? [];
|
|
2524
2402
|
const fingerprint = JSON.stringify({
|
|
2525
2403
|
taskId: task.id,
|
|
2526
2404
|
reviewRoundId: round.id,
|
|
2527
|
-
workItemId: item.id,
|
|
2528
|
-
candidateId: candidate.id,
|
|
2405
|
+
...(taskScope ? {} : { workItemId: item.id, candidateId: candidate.id }),
|
|
2529
2406
|
scope: taskScope ? "task" : "work-item",
|
|
2530
2407
|
projects,
|
|
2531
2408
|
contractDigest: round.taskFinalReviewContract?.digest
|
|
@@ -2534,9 +2411,10 @@ function executionTargetForReviewRound(task, round, item, candidate) {
|
|
|
2534
2411
|
schemaVersion: 1,
|
|
2535
2412
|
kind: taskScope ? "task-final-review" : "work-item",
|
|
2536
2413
|
taskId: task.id,
|
|
2537
|
-
workItemId: item.id,
|
|
2538
|
-
|
|
2539
|
-
revision
|
|
2414
|
+
...(taskScope ? {} : { workItemId: item.id, candidateId: candidate.id }),
|
|
2415
|
+
// A Task-final ReviewRound is itself the immutable semantic target. Runs
|
|
2416
|
+
// retry that same revision; a changed frozen Task creates a new Round.
|
|
2417
|
+
revision: taskScope ? 1 : candidate.workItemRevision,
|
|
2540
2418
|
projects,
|
|
2541
2419
|
...(round.taskFinalReviewContract === undefined
|
|
2542
2420
|
? {}
|
|
@@ -3096,7 +2974,7 @@ function resolveReviewExecutionGroup(args, store, options) {
|
|
|
3096
2974
|
reconcileReviewFindingsAfterReview(tx, task.id, terminal.id, now);
|
|
3097
2975
|
}
|
|
3098
2976
|
enqueueWork(tx, leaderMailbox(task.id), "review-group-resolved", now, [
|
|
3099
|
-
workItemRef(task.id, round.workItemId)
|
|
2977
|
+
...(round.workItemId === undefined ? [] : [workItemRef(task.id, round.workItemId)])
|
|
3100
2978
|
]);
|
|
3101
2979
|
return terminal;
|
|
3102
2980
|
});
|
|
@@ -3323,7 +3201,6 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3323
3201
|
if (producerCollision !== null) {
|
|
3324
3202
|
throw usageError(producerCollision);
|
|
3325
3203
|
}
|
|
3326
|
-
const anchor = latestTaskReviewAnchor(tx, task);
|
|
3327
3204
|
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id))
|
|
3328
3205
|
.filter((entry) => (entry.scope ?? "work-item") === "task");
|
|
3329
3206
|
const exact = taskRounds.filter((entry) => (entry.reviewerRoleName === reviewerRoleName
|
|
@@ -3441,11 +3318,11 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3441
3318
|
deltaRecord = validateDeltaRecheckRequest(tx, task.id, reviewerRoleName, provenance.candidate, options.deltaRecheckPreflight);
|
|
3442
3319
|
}
|
|
3443
3320
|
let created = deltaRecord === undefined
|
|
3444
|
-
? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id,
|
|
3445
|
-
: createTaskDeltaReviewRound(tx.nextReviewRoundId(task.id), task.id,
|
|
3321
|
+
? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, now, taskFinalContract)
|
|
3322
|
+
: createTaskDeltaReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, deltaRecord, now, taskFinalContract);
|
|
3446
3323
|
let group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
|
|
3447
3324
|
purpose: "review",
|
|
3448
|
-
target: executionTargetForReviewRound(task, created
|
|
3325
|
+
target: executionTargetForReviewRound(task, created),
|
|
3449
3326
|
strategy,
|
|
3450
3327
|
lanes: laneRoles.map((roleName) => ({ roleName, reviewRoundId: created.id }))
|
|
3451
3328
|
}, now);
|
|
@@ -3471,8 +3348,6 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3471
3348
|
}
|
|
3472
3349
|
recordTaskEvent(tx, task.id, "review.task-final-requested", {
|
|
3473
3350
|
reviewRoundId: created.id,
|
|
3474
|
-
workItemId: created.workItemId,
|
|
3475
|
-
candidateId: created.candidateId,
|
|
3476
3351
|
reviewerRoleName: created.reviewerRoleName,
|
|
3477
3352
|
requestedBy: created.requestedBy,
|
|
3478
3353
|
taskCandidate: JSON.stringify(created.taskCandidate),
|
|
@@ -3532,14 +3407,10 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3532
3407
|
if (replacement === null
|
|
3533
3408
|
|| replacement.id === source.id
|
|
3534
3409
|
|| (replacement.scope ?? "work-item") !== "task"
|
|
3535
|
-
|| replacement.workItemId !== source.workItemId
|
|
3536
|
-
|| replacement.candidateId !== source.candidateId
|
|
3537
3410
|
|| replacement.reviewerRoleName !== source.reviewerRoleName
|
|
3538
3411
|
|| replacement.deltaRecheck !== undefined
|
|
3539
3412
|
|| !sameTaskFinalReviewContract(replacement.taskFinalReviewContract, source.taskFinalReviewContract)
|
|
3540
3413
|
|| !isSameTaskReviewCandidate(replacement.taskCandidate, source.taskCandidate)
|
|
3541
|
-
|| replacementEvent.payload.workItemId !== replacement.workItemId
|
|
3542
|
-
|| replacementEvent.payload.candidateId !== replacement.candidateId
|
|
3543
3414
|
|| replacementEvent.payload.reviewerRoleName !== replacement.reviewerRoleName
|
|
3544
3415
|
|| replacementEvent.payload.taskCandidate !== JSON.stringify(replacement.taskCandidate)) {
|
|
3545
3416
|
throw dataError(`Force-fresh audit for ${source.id} does not match its replacement Round.`);
|
|
@@ -3564,12 +3435,6 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3564
3435
|
|| source.executionGroup.lanes[0].roleName !== source.reviewerRoleName)) {
|
|
3565
3436
|
throw usageError(`ReviewRound ${source.id} is not a single-Reviewer full Review; force-fresh is refused.`);
|
|
3566
3437
|
}
|
|
3567
|
-
const item = tx.getWorkItem(task.id, source.workItemId);
|
|
3568
|
-
const candidate = item?.candidates.find(({ id }) => id === source.candidateId);
|
|
3569
|
-
if (item === null || item === undefined || candidate === undefined) {
|
|
3570
|
-
throw dataError(`Final Review anchor Candidate is no longer available: `
|
|
3571
|
-
+ `${source.workItemId}/${source.candidateId}.`);
|
|
3572
|
-
}
|
|
3573
3438
|
const provenance = taskReviewProvenance(tx, task, options);
|
|
3574
3439
|
if (!isSameTaskReviewCandidate(source.taskCandidate, provenance.candidate)) {
|
|
3575
3440
|
throw usageError(`Final ReviewRound ${source.id} freezes a candidate that is no longer the current Task candidate.`);
|
|
@@ -3598,10 +3463,10 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3598
3463
|
reviewer = createTaskRole(tx, task, source.reviewerRoleName, undefined, now, source.reviewerRoleName);
|
|
3599
3464
|
tx.saveRole(task.id, reviewer);
|
|
3600
3465
|
}
|
|
3601
|
-
let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.
|
|
3466
|
+
let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.reviewerRoleName, "leader", source.taskCandidate, now, taskFinalContract);
|
|
3602
3467
|
const group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
|
|
3603
3468
|
purpose: "review",
|
|
3604
|
-
target: executionTargetForReviewRound(task, created
|
|
3469
|
+
target: executionTargetForReviewRound(task, created),
|
|
3605
3470
|
strategy: { mode: "fixed", count: 1 },
|
|
3606
3471
|
lanes: [{ roleName: reviewer.name, reviewRoundId: created.id }]
|
|
3607
3472
|
}, now);
|
|
@@ -3611,8 +3476,6 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3611
3476
|
sourceReviewRoundId: source.id,
|
|
3612
3477
|
...(source.reviewerRunId === undefined ? {} : { sourceReviewerRunId: source.reviewerRunId }),
|
|
3613
3478
|
reviewRoundId: created.id,
|
|
3614
|
-
workItemId: created.workItemId,
|
|
3615
|
-
candidateId: created.candidateId,
|
|
3616
3479
|
reviewerRoleName: created.reviewerRoleName,
|
|
3617
3480
|
taskCandidate: JSON.stringify(created.taskCandidate),
|
|
3618
3481
|
reason: "source-round-terminal-without-semantic-review",
|
|
@@ -3763,15 +3626,9 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3763
3626
|
if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)) {
|
|
3764
3627
|
throw usageError(`Task final-review contract does not match ReviewRound ${round.id}.`);
|
|
3765
3628
|
}
|
|
3766
|
-
// Re-read the exact current
|
|
3767
|
-
//
|
|
3768
|
-
//
|
|
3769
|
-
// with the old failed Round byte-for-byte unchanged.
|
|
3770
|
-
const item = tx.getWorkItem(task.id, round.workItemId);
|
|
3771
|
-
if (item === null || item.candidates.every(({ id }) => id !== round.candidateId)) {
|
|
3772
|
-
throw dataError(`Final Review anchor Candidate is no longer available: `
|
|
3773
|
-
+ `${round.workItemId}/${round.candidateId}.`);
|
|
3774
|
-
}
|
|
3629
|
+
// Re-read the exact current Task heads and producer roles before touching
|
|
3630
|
+
// any record. A moved head or reviewer collision fails closed with the old
|
|
3631
|
+
// failed Round byte-for-byte unchanged.
|
|
3775
3632
|
const provenance = taskReviewProvenance(tx, task, options);
|
|
3776
3633
|
if (!isSameTaskReviewCandidate(round.taskCandidate, provenance.candidate)) {
|
|
3777
3634
|
throw usageError(`Final ReviewRound ${round.id} freezes a candidate that is no longer the current Task candidate.`);
|
|
@@ -3834,9 +3691,7 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3834
3691
|
const resetRound = retryTaskReviewRound(round);
|
|
3835
3692
|
tx.saveReviewRound(task.id, resetRound);
|
|
3836
3693
|
recordTaskEvent(tx, task.id, "review.task-final-retried", {
|
|
3837
|
-
reviewRoundId: round.id
|
|
3838
|
-
workItemId: round.workItemId,
|
|
3839
|
-
candidateId: round.candidateId
|
|
3694
|
+
reviewRoundId: round.id
|
|
3840
3695
|
}, now);
|
|
3841
3696
|
return { round: resetRound, created: true };
|
|
3842
3697
|
});
|
|
@@ -4026,7 +3881,7 @@ function listRuns(args, store, options) {
|
|
|
4026
3881
|
* on. This is deliberately narrower than retry: it cannot manufacture a
|
|
4027
3882
|
* review or fail an arbitrary Round, and every identity/mailbox fence is
|
|
4028
3883
|
* checked before the old Round changes. The next normal Task completion then
|
|
4029
|
-
* creates one fresh Round over the newer
|
|
3884
|
+
* creates one fresh Round over the newer frozen Task heads.
|
|
4030
3885
|
*/
|
|
4031
3886
|
function settleStaleFinalReviewRun(args, store, options) {
|
|
4032
3887
|
exactPositionals(args, 1, "Task run settle usage: yui task run settle <task>/<run>.");
|
|
@@ -4065,14 +3920,6 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4065
3920
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
4066
3921
|
throw usageError(`Review Run ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
4067
3922
|
}
|
|
4068
|
-
const item = tx.getWorkItem(task.id, round.workItemId);
|
|
4069
|
-
if (item === null) {
|
|
4070
|
-
throw dataError(`Work item not found for run ${run.id}: ${round.workItemId}.`);
|
|
4071
|
-
}
|
|
4072
|
-
const candidate = item.candidates.find(({ id }) => id === round.candidateId);
|
|
4073
|
-
if (candidate === undefined) {
|
|
4074
|
-
throw dataError(`ReviewRound Candidate not found: ${round.candidateId}.`);
|
|
4075
|
-
}
|
|
4076
3923
|
const activeReviewerRun = tx.listAgentRuns(task.id).find((entry) => (entry.purpose === "review" && entry.status === "active"));
|
|
4077
3924
|
const activeRoleRun = tx.getActiveAgentRun(task.id, round.reviewerRoleName);
|
|
4078
3925
|
if (activeReviewerRun !== undefined || activeRoleRun !== null) {
|
|
@@ -4159,7 +4006,6 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4159
4006
|
recordTaskEvent(tx, task.id, "run.review-stale-settled", {
|
|
4160
4007
|
runId: run.id,
|
|
4161
4008
|
reviewRoundId: round.id,
|
|
4162
|
-
candidateId: candidate.id,
|
|
4163
4009
|
previousTaskCandidate: JSON.stringify(round.taskCandidate),
|
|
4164
4010
|
currentTaskCandidate: JSON.stringify(currentTaskCandidate)
|
|
4165
4011
|
}, now);
|
|
@@ -4364,7 +4210,7 @@ function taskReviewProducerCollision(provenance, reviewerRoleName) {
|
|
|
4364
4210
|
.sort((left, right) => left.localeCompare(right, undefined, { numeric: true }));
|
|
4365
4211
|
return workItemIds.length === 0
|
|
4366
4212
|
? null
|
|
4367
|
-
: `Reviewer Role must be separate from every
|
|
4213
|
+
: `Reviewer Role must be separate from every WorkItem Candidate producer: `
|
|
4368
4214
|
+ `${reviewerRoleName} (${workItemIds.join(", ")}).`;
|
|
4369
4215
|
}
|
|
4370
4216
|
/**
|
|
@@ -4381,7 +4227,7 @@ function taskReviewProducerCollision(provenance, reviewerRoleName) {
|
|
|
4381
4227
|
function taskReviewProvenance(store, task, options, expected) {
|
|
4382
4228
|
const candidate = actualTaskReviewCandidateForMutation(store, task, options);
|
|
4383
4229
|
if (expected !== undefined && !isSameTaskReviewCandidate(candidate, expected)) {
|
|
4384
|
-
throw usageError(`Task-final ReviewRound frozen
|
|
4230
|
+
throw usageError(`Task-final ReviewRound frozen Task heads changed for its Project set.`);
|
|
4385
4231
|
}
|
|
4386
4232
|
const producerRoles = new Set();
|
|
4387
4233
|
const producerWorkItemIds = new Map();
|
|
@@ -4484,32 +4330,18 @@ function assertNoConflictingTaskReviewRound(rounds, reusableRoundIds = []) {
|
|
|
4484
4330
|
throw usageError(`Another active Task-final ReviewRound already exists: ${conflicting.id}/${conflicting.reviewerRoleName}.`);
|
|
4485
4331
|
}
|
|
4486
4332
|
}
|
|
4487
|
-
function latestTaskReviewAnchor(store, task) {
|
|
4488
|
-
const item = [...store.listWorkItems(task.id)]
|
|
4489
|
-
.filter(({ candidates }) => candidates.length > 0)
|
|
4490
|
-
.sort((left, right) => (left.updatedAt.localeCompare(right.updatedAt) || left.id.localeCompare(right.id)))
|
|
4491
|
-
.at(-1);
|
|
4492
|
-
if (item === undefined) {
|
|
4493
|
-
throw usageError(`Task ${task.id} has no WorkItem Candidate to anchor its final Review.`);
|
|
4494
|
-
}
|
|
4495
|
-
const candidate = item.candidates.at(-1);
|
|
4496
|
-
if (candidate === undefined) {
|
|
4497
|
-
throw usageError(`Task ${task.id} has no WorkItem Candidate to anchor its final Review.`);
|
|
4498
|
-
}
|
|
4499
|
-
return { item, candidate };
|
|
4500
|
-
}
|
|
4501
4333
|
/**
|
|
4502
4334
|
* Queues a Task-scoped final ReviewRound. The reviewer Role must be separate
|
|
4503
4335
|
* from the Candidate producer. Returns the round (pending or failed) so the
|
|
4504
4336
|
* caller can surface it to the CLI for workspace preparation and dispatch.
|
|
4505
4337
|
*/
|
|
4506
|
-
function queueTaskReviewRound(store, task,
|
|
4338
|
+
function queueTaskReviewRound(store, task, config, taskCandidate, options, now, requestedBy = "policy", taskFinalContract) {
|
|
4507
4339
|
assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id));
|
|
4508
4340
|
const provenance = taskReviewProvenance(store, task, options, taskCandidate);
|
|
4509
4341
|
if (!isSameTaskReviewCandidate(provenance.candidate, taskCandidate)) {
|
|
4510
|
-
throw usageError(`Task-final ReviewRound
|
|
4342
|
+
throw usageError(`Task-final ReviewRound frozen Task heads changed before queueing.`);
|
|
4511
4343
|
}
|
|
4512
|
-
const pending = createTaskReviewRound(store.nextReviewRoundId(task.id), task.id,
|
|
4344
|
+
const pending = createTaskReviewRound(store.nextReviewRoundId(task.id), task.id, config.roleName, requestedBy, taskCandidate, now, taskFinalContract);
|
|
4513
4345
|
store.saveReviewRound(task.id, pending);
|
|
4514
4346
|
const producerCollision = taskReviewProducerCollision(provenance, config.roleName);
|
|
4515
4347
|
if (producerCollision !== null) {
|
|
@@ -4546,8 +4378,8 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4546
4378
|
// Any Task-final ReviewRound is durable completion evidence/obligation.
|
|
4547
4379
|
// Once one exists, later changes to the mutable global review config cannot
|
|
4548
4380
|
// weaken the requirement or change its reviewer. Before the first such
|
|
4549
|
-
// Round, the current global `final` config
|
|
4550
|
-
//
|
|
4381
|
+
// Round, the current global `final` config remains an available Reviewer
|
|
4382
|
+
// default but does not establish an obligation by itself.
|
|
4551
4383
|
const taskRounds = reviewRoundsByIdentity(store.listReviewRounds(task.id))
|
|
4552
4384
|
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
4553
4385
|
&& (taskFinalContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract))));
|
|
@@ -4557,16 +4389,10 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4557
4389
|
config = taskFinalReviewConfig(taskFinalContract);
|
|
4558
4390
|
}
|
|
4559
4391
|
else if (establishedRound === undefined) {
|
|
4560
|
-
|
|
4561
|
-
//
|
|
4562
|
-
//
|
|
4563
|
-
|
|
4564
|
-
// established Task Round or immutable contract remains authoritative
|
|
4565
|
-
// through the branches above.
|
|
4566
|
-
config = taskDeliveryPath(task) === "integrated"
|
|
4567
|
-
&& globalConfig?.trigger === "final"
|
|
4568
|
-
? globalConfig
|
|
4569
|
-
: null;
|
|
4392
|
+
// The configured Reviewer is available to the Leader, but does not choose
|
|
4393
|
+
// the Task topology. A managed final Review becomes an obligation only
|
|
4394
|
+
// after the Leader requests one (or an immutable Task contract requires it).
|
|
4395
|
+
config = null;
|
|
4570
4396
|
}
|
|
4571
4397
|
else {
|
|
4572
4398
|
config = { roleName: establishedRound.reviewerRoleName, trigger: "final" };
|
|
@@ -4593,10 +4419,7 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4593
4419
|
&& latest.deltaRecheck !== undefined
|
|
4594
4420
|
&& latest.deltaRecheck.disposition === "requires-full-review") {
|
|
4595
4421
|
// Fall through to queue a full Review for the same candidate.
|
|
4596
|
-
const
|
|
4597
|
-
? latestTaskReviewAnchor(store, task)
|
|
4598
|
-
: latestTaskReviewContractAnchor(store, task, taskFinalContract);
|
|
4599
|
-
const escalated = queueTaskReviewRound(store, task, anchor.item, anchor.candidate.id, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
|
|
4422
|
+
const escalated = queueTaskReviewRound(store, task, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
|
|
4600
4423
|
// Record the escalation lineage on the delta Round so the full Review
|
|
4601
4424
|
// is traceable from the non-accepting delta disposition.
|
|
4602
4425
|
if (latest.deltaRecheck.escalatedToReviewRoundId === undefined) {
|
|
@@ -4617,10 +4440,7 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4617
4440
|
: latest;
|
|
4618
4441
|
}
|
|
4619
4442
|
}
|
|
4620
|
-
|
|
4621
|
-
? latestTaskReviewAnchor(store, task)
|
|
4622
|
-
: latestTaskReviewContractAnchor(store, task, taskFinalContract);
|
|
4623
|
-
return queueTaskReviewRound(store, task, anchor.item, anchor.candidate.id, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
|
|
4443
|
+
return queueTaskReviewRound(store, task, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
|
|
4624
4444
|
}
|
|
4625
4445
|
function resumablePendingFinalTaskReview(store, task, round, config, taskCandidate, taskFinalContract, options) {
|
|
4626
4446
|
if (taskFinalContract === undefined || round.taskFinalReviewContract === undefined) {
|
|
@@ -4685,22 +4505,6 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
4685
4505
|
}
|
|
4686
4506
|
}
|
|
4687
4507
|
}
|
|
4688
|
-
function latestTaskReviewContractAnchor(store, task, taskFinalContract) {
|
|
4689
|
-
const anchor = store.listWorkItems(task.id)
|
|
4690
|
-
.flatMap((item) => {
|
|
4691
|
-
const candidate = governingWorkItemCandidate(item);
|
|
4692
|
-
return candidate !== undefined && sameTaskFinalReviewContract(candidate.taskFinalReviewContract, taskFinalContract)
|
|
4693
|
-
? [{ item, candidate }]
|
|
4694
|
-
: [];
|
|
4695
|
-
})
|
|
4696
|
-
.sort((left, right) => (left.item.updatedAt.localeCompare(right.item.updatedAt)
|
|
4697
|
-
|| left.item.id.localeCompare(right.item.id)))
|
|
4698
|
-
.at(-1);
|
|
4699
|
-
if (anchor === undefined) {
|
|
4700
|
-
throw usageError(`Task ${task.id} has no WorkItem Candidate to anchor its final Review.`);
|
|
4701
|
-
}
|
|
4702
|
-
return anchor;
|
|
4703
|
-
}
|
|
4704
4508
|
/**
|
|
4705
4509
|
* Leader-only retry of an exact failed Task-final review Run. The old failed
|
|
4706
4510
|
* Run remains the attempt trail, while the semantic ReviewRound is reset to
|
|
@@ -4743,15 +4547,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4743
4547
|
}
|
|
4744
4548
|
const currentTaskCandidate = actualTaskReviewCandidateForMutation(tx, task, options);
|
|
4745
4549
|
if (!isSameTaskReviewCandidate(currentTaskCandidate, round.taskCandidate)) {
|
|
4746
|
-
throw usageError(`Task-final ReviewRound ${round.id} no longer matches the
|
|
4747
|
-
}
|
|
4748
|
-
const item = tx.getWorkItem(task.id, round.workItemId);
|
|
4749
|
-
if (item === null) {
|
|
4750
|
-
throw dataError(`Work item not found for run ${run.id}: ${round.workItemId}.`);
|
|
4751
|
-
}
|
|
4752
|
-
const candidate = item.candidates.find(({ id }) => id === round.candidateId);
|
|
4753
|
-
if (candidate === undefined) {
|
|
4754
|
-
throw dataError(`ReviewRound Candidate not found: ${round.candidateId}.`);
|
|
4550
|
+
throw usageError(`Task-final ReviewRound ${round.id} no longer matches the frozen Task heads.`);
|
|
4755
4551
|
}
|
|
4756
4552
|
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
|
|
4757
4553
|
.filter((entry) => (entry.scope ?? "work-item") === "task"));
|
|
@@ -4885,8 +4681,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4885
4681
|
tx.saveReviewRound(task.id, resetRound);
|
|
4886
4682
|
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
4887
4683
|
runId: run.id,
|
|
4888
|
-
reviewRoundId: round.id
|
|
4889
|
-
candidateId: candidate.id
|
|
4684
|
+
reviewRoundId: round.id
|
|
4890
4685
|
}, now);
|
|
4891
4686
|
return { round: resetRound, previousRun: run, created: true };
|
|
4892
4687
|
});
|
|
@@ -5300,8 +5095,8 @@ function yieldRun(args, store, options) {
|
|
|
5300
5095
|
const panelGroup = round.executionGroup;
|
|
5301
5096
|
recordTaskEvent(tx, task.id, "review-group-ready", {
|
|
5302
5097
|
reviewRoundId: round.id,
|
|
5303
|
-
workItemId: round.workItemId,
|
|
5304
|
-
candidateId: round.candidateId,
|
|
5098
|
+
...(round.workItemId === undefined ? {} : { workItemId: round.workItemId }),
|
|
5099
|
+
...(round.candidateId === undefined ? {} : { candidateId: round.candidateId }),
|
|
5305
5100
|
executionGroupId: panelGroup.id,
|
|
5306
5101
|
terminalLanes: String(panelGroup.lanes
|
|
5307
5102
|
.filter(({ status }) => ["yielded", "completed", "failed"].includes(status)).length),
|
|
@@ -5315,8 +5110,8 @@ function yieldRun(args, store, options) {
|
|
|
5315
5110
|
}
|
|
5316
5111
|
recordTaskEvent(tx, task.id, "review.completed", {
|
|
5317
5112
|
reviewRoundId: round.id,
|
|
5318
|
-
workItemId: round.workItemId,
|
|
5319
|
-
candidateId: round.candidateId,
|
|
5113
|
+
...(round.workItemId === undefined ? {} : { workItemId: round.workItemId }),
|
|
5114
|
+
...(round.candidateId === undefined ? {} : { candidateId: round.candidateId }),
|
|
5320
5115
|
reviewBaseCommit: round.reviewBaseCommit,
|
|
5321
5116
|
evidenceCommit: round.evidenceCommit ?? "none",
|
|
5322
5117
|
checks: round.checks?.map(({ name, outcome }) => `${name}:${outcome}`)
|
|
@@ -5614,18 +5409,12 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5614
5409
|
}
|
|
5615
5410
|
throw usageError(`ReviewRound workspace is not ready: ${round.id}.`);
|
|
5616
5411
|
}
|
|
5617
|
-
const item = tx.getWorkItem(taskId, round.workItemId);
|
|
5618
|
-
if (item === null)
|
|
5619
|
-
throw dataError(`ReviewRound Work Item not found: ${round.workItemId}.`);
|
|
5620
|
-
const candidate = item.candidates.find(({ id }) => id === round.candidateId);
|
|
5621
|
-
if (candidate === undefined) {
|
|
5622
|
-
throw dataError(`ReviewRound Candidate not found: ${round.candidateId}.`);
|
|
5623
|
-
}
|
|
5624
5412
|
const taskScope = (round.scope ?? "work-item") === "task";
|
|
5413
|
+
let item;
|
|
5414
|
+
let candidate;
|
|
5625
5415
|
if (taskScope) {
|
|
5626
|
-
// A Task-scoped final ReviewRound
|
|
5627
|
-
//
|
|
5628
|
-
// authoritative review base is the first Project's committed head.
|
|
5416
|
+
// A Task-scoped final ReviewRound is anchored directly to its frozen
|
|
5417
|
+
// Task candidate. It deliberately has no synthetic WorkItem Candidate.
|
|
5629
5418
|
if (round.taskCandidate === undefined) {
|
|
5630
5419
|
throw new TaskFinalReviewDispatchDriftError(`Task ReviewRound ${round.id} has no frozen Task candidate.`);
|
|
5631
5420
|
}
|
|
@@ -5633,8 +5422,21 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5633
5422
|
throw new TaskFinalReviewDispatchDriftError(`Task ReviewRound base does not match its frozen Task candidate: ${round.id}.`);
|
|
5634
5423
|
}
|
|
5635
5424
|
}
|
|
5636
|
-
else
|
|
5637
|
-
|
|
5425
|
+
else {
|
|
5426
|
+
if (round.workItemId === undefined || round.candidateId === undefined) {
|
|
5427
|
+
throw dataError(`WorkItem ReviewRound has no Candidate anchor: ${round.id}.`);
|
|
5428
|
+
}
|
|
5429
|
+
item = tx.getWorkItem(taskId, round.workItemId) ?? undefined;
|
|
5430
|
+
if (item === undefined) {
|
|
5431
|
+
throw dataError(`ReviewRound Work Item not found: ${round.workItemId}.`);
|
|
5432
|
+
}
|
|
5433
|
+
candidate = item.candidates.find(({ id }) => id === round.candidateId);
|
|
5434
|
+
if (candidate === undefined) {
|
|
5435
|
+
throw dataError(`ReviewRound Candidate not found: ${round.candidateId}.`);
|
|
5436
|
+
}
|
|
5437
|
+
if (candidate.gitSnapshot?.reviewBaseCommit !== round.reviewBaseCommit) {
|
|
5438
|
+
throw usageError(`ReviewRound Candidate snapshot changed: ${round.id}.`);
|
|
5439
|
+
}
|
|
5638
5440
|
}
|
|
5639
5441
|
const task = requireTask(tx, taskId);
|
|
5640
5442
|
if ((round.scope ?? "work-item") === "task") {
|
|
@@ -5671,7 +5473,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5671
5473
|
&& frozenCommits.get(project.projectId) !== project.commit;
|
|
5672
5474
|
});
|
|
5673
5475
|
throw new TaskFinalReviewDispatchDriftError(committedIntegrationMoved
|
|
5674
|
-
? "Task-final ReviewRound frozen
|
|
5476
|
+
? "Task-final ReviewRound frozen Task heads changed for its Project set."
|
|
5675
5477
|
: `Final ReviewRound ${round.id} freezes a candidate that is no longer `
|
|
5676
5478
|
+ "the current Task candidate.");
|
|
5677
5479
|
}
|
|
@@ -5732,9 +5534,11 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5732
5534
|
throw new TaskFinalReviewDispatchDriftError(`Task ReviewRound frozen Project heads changed: ${round.id}.`);
|
|
5733
5535
|
}
|
|
5734
5536
|
}
|
|
5735
|
-
const candidateLabel =
|
|
5736
|
-
?
|
|
5737
|
-
:
|
|
5537
|
+
const candidateLabel = taskScope
|
|
5538
|
+
? "frozen Task candidate"
|
|
5539
|
+
: candidate.source.type === "run"
|
|
5540
|
+
? `candidate Run ${candidate.source.runId}`
|
|
5541
|
+
: `revision ${candidate.workItemRevision}`;
|
|
5738
5542
|
const frozenHeads = taskScope
|
|
5739
5543
|
? round.taskCandidate.projects
|
|
5740
5544
|
.map(({ projectId, commit }) => `${projectId}@${commit}`)
|
|
@@ -5772,22 +5576,26 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5772
5576
|
.map(({ projectId }) => (`yui project show ${projectId}; yui project knowledge list ${projectId}`))
|
|
5773
5577
|
.join(" | ");
|
|
5774
5578
|
const rawInput = [
|
|
5775
|
-
|
|
5579
|
+
taskScope
|
|
5580
|
+
? `Review the ${scopeLabel} ${candidateLabel}.`
|
|
5581
|
+
: `Review ${scopeLabel} WorkItem ${item.id} ${candidateLabel}.`,
|
|
5776
5582
|
`ReviewRound: ${round.id}`,
|
|
5777
5583
|
`Review scope: ${taskScope ? "task" : "work-item"}`,
|
|
5778
5584
|
`Review base commit: ${round.reviewBaseCommit}`,
|
|
5779
5585
|
...(taskScope
|
|
5780
|
-
? [`Frozen
|
|
5586
|
+
? [`Frozen Task heads: ${frozenHeads}`]
|
|
5781
5587
|
: [`Candidate snapshot base: ${round.reviewBaseCommit}`]),
|
|
5782
5588
|
`Project Policy pointers: ${projectPolicyPointers || "none"}`,
|
|
5783
5589
|
`Review workspace source: exact workspace attached to this Reviewer Lane`,
|
|
5784
|
-
`Candidate summary: ${candidate.summary}`,
|
|
5785
|
-
`Acceptance criteria: ${
|
|
5590
|
+
`Candidate summary: ${taskScope ? task.title : candidate.summary}`,
|
|
5591
|
+
`Acceptance criteria: ${taskScope
|
|
5592
|
+
? "Task objective, maintained decisions, and Project Policy"
|
|
5593
|
+
: item.acceptance.length === 0 ? "none" : item.acceptance.join("; ")}`,
|
|
5786
5594
|
...(taskScope ? [deltaContext !== "" ? deltaContext : findingContext] : []),
|
|
5787
5595
|
"Start from the user's core outcome and the WorkItem intent. The candidate summary is a pointer, not proof: inspect the complete relevant change, callers, and proportionate checks.",
|
|
5788
5596
|
"Keep Yui Core lifecycle safety, generic Reviewer behavior, Project Policy/Knowledge, and the Task Contract separate. Follow Project Policy pointers from the dispatch context for project-specific checks.",
|
|
5789
5597
|
...(round.scope === "task" && round.deltaRecheck === undefined
|
|
5790
|
-
? ["This is the one final Task Review: inspect every bound Project at the frozen
|
|
5598
|
+
? ["This is the one final Task Review: inspect every bound Project at the frozen Task heads, and report only reachable, material, actionable P1/P2 findings or bounded verification gaps."]
|
|
5791
5599
|
: []),
|
|
5792
5600
|
"You may freely edit source/tests, run local build or test commands, and optionally commit diagnostic evidence only inside this ReviewRound-owned workspace.",
|
|
5793
5601
|
"Do not push, integrate, mutate Task state, touch the Candidate or Worker workspace, another Task/workspace, a stable checkout, or the real Yui control-plane home.",
|
|
@@ -5848,7 +5656,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5848
5656
|
action: "review-round",
|
|
5849
5657
|
subject: {
|
|
5850
5658
|
taskId,
|
|
5851
|
-
workItemId: item.id,
|
|
5659
|
+
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5852
5660
|
reviewRoundId: round.id,
|
|
5853
5661
|
executionGroupId: runningGroup.id,
|
|
5854
5662
|
executionLaneId: lane.id
|
|
@@ -5864,7 +5672,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5864
5672
|
workspace: laneWorkspace
|
|
5865
5673
|
}, now);
|
|
5866
5674
|
createdRuns.push(createAgentRun(runId, taskId, laneReviewer.name, roleAgentSessionResumeMode(sessions, effective.agentId, effective), assignment, now, {
|
|
5867
|
-
workItemId: item.id,
|
|
5675
|
+
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5868
5676
|
purpose: "review",
|
|
5869
5677
|
reviewRoundId: round.id,
|
|
5870
5678
|
executionGroupId: runningGroup.id,
|
|
@@ -5901,7 +5709,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5901
5709
|
taskId,
|
|
5902
5710
|
roleName: unboundRun.roleName,
|
|
5903
5711
|
purpose: "review",
|
|
5904
|
-
workItemId: item.id,
|
|
5712
|
+
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5905
5713
|
reviewRoundId: round.id
|
|
5906
5714
|
}, now);
|
|
5907
5715
|
const created = withAgentRunContextSnapshot(unboundRun, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
@@ -5912,7 +5720,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5912
5720
|
tx.saveRole(taskId, updateRoleStatus(laneReviewer, "running", now));
|
|
5913
5721
|
enqueueWork(tx, roleMailbox(taskId, laneReviewer.name), "review-requested", now, [
|
|
5914
5722
|
runRef(taskId, created.id),
|
|
5915
|
-
workItemRef(taskId, item.id)
|
|
5723
|
+
...(item === undefined ? [] : [workItemRef(taskId, item.id)])
|
|
5916
5724
|
]);
|
|
5917
5725
|
recordTaskEvent(tx, taskId, "run.review-dispatched", runLaunchEventPayload(created), now);
|
|
5918
5726
|
}
|
|
@@ -5934,7 +5742,7 @@ export function failPendingReviewRound(taskId, reviewRoundId, summary, store, op
|
|
|
5934
5742
|
const terminal = finishReviewRound(round, "failed", summary, now);
|
|
5935
5743
|
tx.saveReviewRound(taskId, terminal);
|
|
5936
5744
|
enqueueWork(tx, leaderMailbox(taskId), "review-failed", now, [
|
|
5937
|
-
workItemRef(taskId, round.workItemId)
|
|
5745
|
+
...(round.workItemId === undefined ? [] : [workItemRef(taskId, round.workItemId)])
|
|
5938
5746
|
]);
|
|
5939
5747
|
return terminal;
|
|
5940
5748
|
});
|
|
@@ -6068,7 +5876,7 @@ function requirePublishedTreeAuthorization(store, proof) {
|
|
|
6068
5876
|
if (authorization === undefined) {
|
|
6069
5877
|
throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
|
|
6070
5878
|
+ `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
|
|
6071
|
-
? "the
|
|
5879
|
+
? "the Leader-owned Task-main head"
|
|
6072
5880
|
: `Task-final Review ${proof.reviewRoundId}`}.`);
|
|
6073
5881
|
}
|
|
6074
5882
|
return authorization;
|
|
@@ -6337,11 +6145,6 @@ function parseTaskPriority(value) {
|
|
|
6337
6145
|
return value;
|
|
6338
6146
|
throw usageError(`Invalid Task priority: ${value}.`);
|
|
6339
6147
|
}
|
|
6340
|
-
function parseTaskDelivery(value) {
|
|
6341
|
-
if (value === "direct" || value === "integrated")
|
|
6342
|
-
return value;
|
|
6343
|
-
throw usageError(`Task delivery is invalid: ${value}. Use direct or integrated.`);
|
|
6344
|
-
}
|
|
6345
6148
|
function parseTaskTags(value) {
|
|
6346
6149
|
const tags = [...new Set(value.split(",").map((tag) => tag.trim()).filter(Boolean))];
|
|
6347
6150
|
if (tags.length === 0)
|