@zq-silk/yui 0.8.9 → 0.10.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 +72 -42
- package/dist/cli/commandCatalog.js +16 -8
- package/dist/cli.js +27 -32
- package/dist/commands/executionAuditCommands.js +2 -2
- package/dist/commands/globalRoleCommands.js +0 -12
- package/dist/commands/sessionCommands.js +116 -0
- package/dist/commands/taskBaseCommands.js +1 -11
- package/dist/commands/taskCommands.js +123 -340
- 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/controller/clientRuntime.js +65 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2 -49
- package/dist/doctor/doctor.js +16 -12
- package/dist/execution/executionGroup.js +0 -3
- package/dist/executor/agentAdapter.js +39 -42
- package/dist/executor/agentExecutor.js +4 -2
- package/dist/executor/codexConfigConflict.js +40 -16
- package/dist/executor/effectiveLaunch.js +33 -3
- package/dist/executor/fileRoleLaunchPlanner.js +15 -24
- package/dist/integration/deliveryObligation.js +72 -0
- package/dist/integration/gitIntegrationService.js +1 -1
- package/dist/lifecycle/exactRunTerminalization.js +12 -8
- package/dist/observability/orchestrationMetrics.js +12 -26
- package/dist/profile/agentProfile.js +1 -1
- package/dist/repository/taskBaseFreshness.js +5 -11
- package/dist/repository/taskWorkspaceCoordinator.js +2 -0
- package/dist/repository/taskWorkspacePreparer.js +173 -26
- package/dist/review/reviewRound.js +41 -24
- package/dist/role/role.js +0 -9
- package/dist/runtime/{firstProgressStopLoss.js → firstProgressAdvisory.js} +7 -21
- package/dist/scheduler/leaderWakeupProcessor.js +0 -25
- package/dist/setup/setupCommand.js +0 -5
- package/dist/storage/migration/productionRegistry.js +138 -0
- package/dist/storage/sqliteStore.js +2 -1
- package/dist/storage/taskStore.js +27 -27
- package/dist/storage/upgrade/upgradeOrchestrator.js +35 -7
- package/dist/task/completionReadiness.js +35 -25
- package/dist/task/nextAction.js +70 -78
- 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 +33 -24
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +55 -52
- package/skills/yui-operator/SKILL.md +31 -40
- 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";
|
|
@@ -102,7 +102,7 @@ function storedTaskFinalReviewContractResolution(store, taskId) {
|
|
|
102
102
|
* acceptance, and completion mutation must present the same verified
|
|
103
103
|
* capability; shared review-config drift is intentionally irrelevant.
|
|
104
104
|
*/
|
|
105
|
-
function taskFinalReviewContractForMutation(store, taskId, options
|
|
105
|
+
function taskFinalReviewContractForMutation(store, taskId, options) {
|
|
106
106
|
const supplied = options.taskFinalReviewContract;
|
|
107
107
|
if (supplied !== undefined) {
|
|
108
108
|
validateTaskFinalReviewContract(supplied);
|
|
@@ -117,11 +117,12 @@ function taskFinalReviewContractForMutation(store, taskId, options, authorizatio
|
|
|
117
117
|
}
|
|
118
118
|
if (stored === undefined)
|
|
119
119
|
return supplied;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
// Historical exact contracts now express only a durable Reviewer policy.
|
|
121
|
+
// The persisted contract remains the audit authority, but ordinary
|
|
122
|
+
// protocol/storage-compatible CLIs may continue the Task without presenting
|
|
123
|
+
// a release-bound capability on every mutation.
|
|
124
|
+
if (supplied === undefined)
|
|
125
|
+
return stored;
|
|
125
126
|
if (!sameTaskFinalReviewContract(stored, supplied)) {
|
|
126
127
|
throw usageError(`Task final-review contract control-plane digest mismatch for ${taskId}.`);
|
|
127
128
|
}
|
|
@@ -168,18 +169,10 @@ export function preflightTaskCompletion(taskId, store, options = {}, request = {
|
|
|
168
169
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
169
170
|
if (task.status !== "active")
|
|
170
171
|
throw usageError(`Task is not active: ${task.id}.`);
|
|
171
|
-
// Resolve
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
// For the explicit published-tree path only, let that caller authenticate
|
|
176
|
-
// the stored contract far enough to persist an exact authorization fact.
|
|
177
|
-
// The same command must return before any contract-governed completion
|
|
178
|
-
// mutation; the exact Leader later consumes the authorization with the real
|
|
179
|
-
// contract capability.
|
|
180
|
-
const authorizingPublishedTree = request.acceptedPublishedTreePublicationId !== undefined
|
|
181
|
-
&& actor !== "leader";
|
|
182
|
-
const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options, { allowStoredWithoutSupplied: authorizingPublishedTree });
|
|
172
|
+
// Resolve the durable Task-local Reviewer policy before any remote fetch or
|
|
173
|
+
// Integration write. Historical control-plane identity is audit evidence,
|
|
174
|
+
// not a capability that every compatible CLI must reproduce.
|
|
175
|
+
const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options);
|
|
183
176
|
const activeTaskReview = store.listReviewRounds(task.id).some((round) => ((round.scope ?? "work-item") === "task"
|
|
184
177
|
&& (round.status === "pending" || round.status === "running")));
|
|
185
178
|
// Issue 06: one shared readiness projection enumerates every blocker.
|
|
@@ -367,13 +360,12 @@ function taskProjectCommand(args, store, options) {
|
|
|
367
360
|
}
|
|
368
361
|
function updateTaskCommand(args, store, options) {
|
|
369
362
|
const optionNames = new Set([
|
|
370
|
-
"--title", "--description", "--priority", "--tags", "--due-at"
|
|
363
|
+
"--title", "--type", "--description", "--priority", "--tags", "--due-at"
|
|
371
364
|
]);
|
|
372
365
|
const flagOptions = new Set([
|
|
373
|
-
"--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at"
|
|
374
|
-
"--require-integration"
|
|
366
|
+
"--clear-type", "--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at"
|
|
375
367
|
]);
|
|
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]
|
|
368
|
+
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
369
|
const parsed = parseTail(args, optionNames, usage, flagOptions);
|
|
378
370
|
exactPositionals(parsed.positionals, 1, usage);
|
|
379
371
|
if (parsed.options.size === 0)
|
|
@@ -397,44 +389,16 @@ function updateTaskCommand(args, store, options) {
|
|
|
397
389
|
const tags = parsed.options.has("--tags")
|
|
398
390
|
? parseTaskTags(requiredOption(parsed.options, "--tags"))
|
|
399
391
|
: 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
392
|
const now = clock(options);
|
|
409
393
|
const result = store.transaction((tx) => {
|
|
410
394
|
const current = requireTask(tx, parsed.positionals[0]);
|
|
411
395
|
if (current.status === "archived")
|
|
412
396
|
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
397
|
const updated = updateTaskMetadata(current, {
|
|
437
398
|
...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
|
|
399
|
+
...(parsed.options.has("--type")
|
|
400
|
+
? { type: requiredOption(parsed.options, "--type") }
|
|
401
|
+
: parsed.options.has("--clear-type") ? { type: null } : {}),
|
|
438
402
|
...(parsed.options.has("--description")
|
|
439
403
|
? { description: requiredOption(parsed.options, "--description") }
|
|
440
404
|
: parsed.options.has("--clear-description") ? { description: null } : {}),
|
|
@@ -446,94 +410,18 @@ function updateTaskCommand(args, store, options) {
|
|
|
446
410
|
: { tags }),
|
|
447
411
|
...(dueAt === undefined
|
|
448
412
|
? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
|
|
449
|
-
: { dueAt })
|
|
450
|
-
...(enableIntegration ? { requireIntegration: true } : {})
|
|
413
|
+
: { dueAt })
|
|
451
414
|
}, now);
|
|
452
415
|
tx.saveTask(updated);
|
|
453
416
|
recordTaskEvent(tx, updated.id, "task.updated", {
|
|
454
417
|
status: updated.status,
|
|
455
|
-
...(
|
|
456
|
-
? {}
|
|
457
|
-
: {
|
|
458
|
-
completionEvidence: enableIntegration
|
|
459
|
-
? "integration-required"
|
|
460
|
-
: "direct",
|
|
461
|
-
deliveryPath: taskDeliveryPath(updated)
|
|
462
|
-
})
|
|
418
|
+
...(updated.type === undefined ? {} : { taskType: updated.type })
|
|
463
419
|
}, now);
|
|
464
420
|
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
|
-
};
|
|
421
|
+
return updated;
|
|
473
422
|
});
|
|
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
|
-
}
|
|
423
|
+
notifyMailbox(options.runtime, taskMailbox(result.id), result.id);
|
|
424
|
+
return `Updated task ${result.id}\n`;
|
|
537
425
|
}
|
|
538
426
|
/** Compatibility helper for call sites that cannot yet handle foreground enter. */
|
|
539
427
|
export function runTaskOutputCommand(args, store, options = {}) {
|
|
@@ -569,25 +457,22 @@ function createTaskCommand(args, store, options) {
|
|
|
569
457
|
const now = clock(options);
|
|
570
458
|
const created = store.transaction((tx) => createTaskAggregate(tx, parsed.title, {
|
|
571
459
|
projectBindings: parsed.projectBindings,
|
|
572
|
-
...(parsed.
|
|
460
|
+
...(parsed.type === undefined ? {} : { type: parsed.type })
|
|
573
461
|
}, now, parsed.defaultProjectIds));
|
|
574
462
|
notifyMailbox(options.runtime, taskMailbox(created.task.id), created.task.id);
|
|
575
463
|
return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
|
|
576
464
|
+ `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"), {
|
|
465
|
+
+ `Type: ${created.task.type ?? "unspecified"}\n`
|
|
466
|
+
+ (created.task.projectBindings.length > 0
|
|
467
|
+
? "Execution: Leader decides whether independent WorkItems are warranted\n"
|
|
468
|
+
: "Execution: no Project delivery evidence required\n"), {
|
|
583
469
|
task: created.task,
|
|
584
|
-
leader: created.leader
|
|
585
|
-
deliveryPath: taskDeliveryPath(created.task)
|
|
470
|
+
leader: created.leader
|
|
586
471
|
});
|
|
587
472
|
}
|
|
588
473
|
function parseTaskCreation(args, store) {
|
|
589
|
-
const usage = "Task create usage: yui task create <title> [--
|
|
590
|
-
const parsed = parseMultiValueTail(args, new Set(["--
|
|
474
|
+
const usage = "Task create usage: yui task create <title> [--type <project-defined-type>] [--project <project> ...] [--base <project>=<ref> ...].";
|
|
475
|
+
const parsed = parseMultiValueTail(args, new Set(["--type"]), new Set(["--project", "--base"]), usage);
|
|
591
476
|
exactPositionals(parsed.positionals, 1, usage);
|
|
592
477
|
const projectReferences = parsed.multiOptions.get("--project") ?? [];
|
|
593
478
|
const baseOptions = parsed.multiOptions.get("--base") ?? [];
|
|
@@ -603,16 +488,6 @@ function parseTaskCreation(args, store) {
|
|
|
603
488
|
if (new Set(projects.map(({ id }) => id)).size !== projects.length) {
|
|
604
489
|
throw usageError("A Task cannot bind the same Project more than once.");
|
|
605
490
|
}
|
|
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
491
|
const bases = new Map();
|
|
617
492
|
for (const option of baseOptions) {
|
|
618
493
|
const separator = option.indexOf("=");
|
|
@@ -637,14 +512,15 @@ function parseTaskCreation(args, store) {
|
|
|
637
512
|
.map(({ id }) => id);
|
|
638
513
|
return {
|
|
639
514
|
title: parsed.positionals[0],
|
|
515
|
+
...(parsed.options.has("--type")
|
|
516
|
+
? { type: requiredOption(parsed.options, "--type") }
|
|
517
|
+
: {}),
|
|
640
518
|
projectBindings: projects.map((project) => ({
|
|
641
519
|
projectId: project.id,
|
|
642
520
|
directory: project.name,
|
|
643
521
|
baseRef: bases.get(project.id) ?? project.developmentBranch
|
|
644
522
|
})),
|
|
645
|
-
defaultProjectIds
|
|
646
|
-
requireIntegration: requestedDelivery === "integrated"
|
|
647
|
-
|| parsed.options.has("--require-integration")
|
|
523
|
+
defaultProjectIds
|
|
648
524
|
};
|
|
649
525
|
}
|
|
650
526
|
function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []) {
|
|
@@ -654,7 +530,7 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
|
|
|
654
530
|
store.saveRole(task.id, leader);
|
|
655
531
|
recordTaskEvent(store, task.id, "task.created", {
|
|
656
532
|
status: task.status,
|
|
657
|
-
|
|
533
|
+
...(task.type === undefined ? {} : { taskType: task.type }),
|
|
658
534
|
...(defaultProjectIds.length === 0
|
|
659
535
|
? {}
|
|
660
536
|
: { defaultProjectIds: defaultProjectIds.join(",") })
|
|
@@ -703,16 +579,14 @@ function showTaskCommand(args, store) {
|
|
|
703
579
|
`Task: ${task.id}`,
|
|
704
580
|
`Title: ${task.title}`,
|
|
705
581
|
`Status: ${task.status}`,
|
|
706
|
-
`
|
|
582
|
+
`Type: ${task.type ?? "unspecified"}`,
|
|
707
583
|
...(task.description === undefined ? [] : [`Description: ${task.description}`]),
|
|
708
584
|
...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
|
|
709
585
|
...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
|
|
710
586
|
...(task.dueAt === undefined ? [] : [`Due: ${presentTime(task.dueAt, timeZone)}`]),
|
|
711
|
-
`
|
|
712
|
-
? "
|
|
713
|
-
:
|
|
714
|
-
? "clean committed Task main required"
|
|
715
|
-
: "no Project evidence required"}`,
|
|
587
|
+
`Execution topology: ${task.projectBindings.length > 0
|
|
588
|
+
? "Leader-owned; WorkItems are optional independent delivery units"
|
|
589
|
+
: "no Project delivery evidence required"}`,
|
|
716
590
|
...(task.completedAt === undefined ? [] : [`Completed: ${presentTime(task.completedAt, timeZone)}`]),
|
|
717
591
|
...(task.completedBy === undefined ? [] : [`Completed by: ${task.completedBy}`]),
|
|
718
592
|
...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
|
|
@@ -741,12 +615,7 @@ function showTaskCommand(args, store) {
|
|
|
741
615
|
`Created: ${presentTime(task.createdAt, timeZone)}`,
|
|
742
616
|
`Updated: ${presentTime(task.updatedAt, timeZone)}`
|
|
743
617
|
].join("\n").concat("\n");
|
|
744
|
-
return output(rendered, {
|
|
745
|
-
task,
|
|
746
|
-
deliveryPath: taskDeliveryPath(task),
|
|
747
|
-
counts,
|
|
748
|
-
hasBrief: brief !== null
|
|
749
|
-
});
|
|
618
|
+
return output(rendered, { task, counts, hasBrief: brief !== null });
|
|
750
619
|
}
|
|
751
620
|
function activateTaskCommand(args, store, options) {
|
|
752
621
|
exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
|
|
@@ -860,7 +729,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
860
729
|
terminalizedLeaderRun = true;
|
|
861
730
|
}
|
|
862
731
|
// A final (Task-scoped) review is the Task delivery policy: it reviews the
|
|
863
|
-
// complete
|
|
732
|
+
// complete frozen Task heads, not a single WorkItem Candidate. If the
|
|
864
733
|
// review config requests a final review, create the Task ReviewRound here
|
|
865
734
|
// and return it for dispatch instead of completing the Task.
|
|
866
735
|
const pendingFinalReviewIds = new Set(tx.listReviewRounds(task.id)
|
|
@@ -915,7 +784,6 @@ function completeTaskCommand(args, store, options) {
|
|
|
915
784
|
recordTaskEvent(tx, task.id, "task.completed", {
|
|
916
785
|
by: actor,
|
|
917
786
|
summary,
|
|
918
|
-
deliveryPath: taskDeliveryPath(task),
|
|
919
787
|
...(actualTaskCandidate === undefined
|
|
920
788
|
? {}
|
|
921
789
|
: {
|
|
@@ -2518,14 +2386,16 @@ function executionTargetForWorkItem(taskId, workItemId, revision, item, workspac
|
|
|
2518
2386
|
}
|
|
2519
2387
|
function executionTargetForReviewRound(task, round, item, candidate) {
|
|
2520
2388
|
const taskScope = (round.scope ?? "work-item") === "task";
|
|
2389
|
+
if (!taskScope && (item === undefined || candidate === undefined)) {
|
|
2390
|
+
throw dataError(`WorkItem ReviewRound target is missing its Candidate: ${round.id}.`);
|
|
2391
|
+
}
|
|
2521
2392
|
const projects = taskScope
|
|
2522
2393
|
? round.taskCandidate?.projects ?? []
|
|
2523
2394
|
: candidate.gitSnapshot?.projects ?? [];
|
|
2524
2395
|
const fingerprint = JSON.stringify({
|
|
2525
2396
|
taskId: task.id,
|
|
2526
2397
|
reviewRoundId: round.id,
|
|
2527
|
-
workItemId: item.id,
|
|
2528
|
-
candidateId: candidate.id,
|
|
2398
|
+
...(taskScope ? {} : { workItemId: item.id, candidateId: candidate.id }),
|
|
2529
2399
|
scope: taskScope ? "task" : "work-item",
|
|
2530
2400
|
projects,
|
|
2531
2401
|
contractDigest: round.taskFinalReviewContract?.digest
|
|
@@ -2534,9 +2404,10 @@ function executionTargetForReviewRound(task, round, item, candidate) {
|
|
|
2534
2404
|
schemaVersion: 1,
|
|
2535
2405
|
kind: taskScope ? "task-final-review" : "work-item",
|
|
2536
2406
|
taskId: task.id,
|
|
2537
|
-
workItemId: item.id,
|
|
2538
|
-
|
|
2539
|
-
revision
|
|
2407
|
+
...(taskScope ? {} : { workItemId: item.id, candidateId: candidate.id }),
|
|
2408
|
+
// A Task-final ReviewRound is itself the immutable semantic target. Runs
|
|
2409
|
+
// retry that same revision; a changed frozen Task creates a new Round.
|
|
2410
|
+
revision: taskScope ? 1 : candidate.workItemRevision,
|
|
2540
2411
|
projects,
|
|
2541
2412
|
...(round.taskFinalReviewContract === undefined
|
|
2542
2413
|
? {}
|
|
@@ -3096,7 +2967,7 @@ function resolveReviewExecutionGroup(args, store, options) {
|
|
|
3096
2967
|
reconcileReviewFindingsAfterReview(tx, task.id, terminal.id, now);
|
|
3097
2968
|
}
|
|
3098
2969
|
enqueueWork(tx, leaderMailbox(task.id), "review-group-resolved", now, [
|
|
3099
|
-
workItemRef(task.id, round.workItemId)
|
|
2970
|
+
...(round.workItemId === undefined ? [] : [workItemRef(task.id, round.workItemId)])
|
|
3100
2971
|
]);
|
|
3101
2972
|
return terminal;
|
|
3102
2973
|
});
|
|
@@ -3323,7 +3194,6 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3323
3194
|
if (producerCollision !== null) {
|
|
3324
3195
|
throw usageError(producerCollision);
|
|
3325
3196
|
}
|
|
3326
|
-
const anchor = latestTaskReviewAnchor(tx, task);
|
|
3327
3197
|
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id))
|
|
3328
3198
|
.filter((entry) => (entry.scope ?? "work-item") === "task");
|
|
3329
3199
|
const exact = taskRounds.filter((entry) => (entry.reviewerRoleName === reviewerRoleName
|
|
@@ -3441,11 +3311,11 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3441
3311
|
deltaRecord = validateDeltaRecheckRequest(tx, task.id, reviewerRoleName, provenance.candidate, options.deltaRecheckPreflight);
|
|
3442
3312
|
}
|
|
3443
3313
|
let created = deltaRecord === undefined
|
|
3444
|
-
? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id,
|
|
3445
|
-
: createTaskDeltaReviewRound(tx.nextReviewRoundId(task.id), task.id,
|
|
3314
|
+
? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, now, taskFinalContract)
|
|
3315
|
+
: createTaskDeltaReviewRound(tx.nextReviewRoundId(task.id), task.id, reviewerRoleName, "leader", provenance.candidate, deltaRecord, now, taskFinalContract);
|
|
3446
3316
|
let group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
|
|
3447
3317
|
purpose: "review",
|
|
3448
|
-
target: executionTargetForReviewRound(task, created
|
|
3318
|
+
target: executionTargetForReviewRound(task, created),
|
|
3449
3319
|
strategy,
|
|
3450
3320
|
lanes: laneRoles.map((roleName) => ({ roleName, reviewRoundId: created.id }))
|
|
3451
3321
|
}, now);
|
|
@@ -3471,8 +3341,6 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3471
3341
|
}
|
|
3472
3342
|
recordTaskEvent(tx, task.id, "review.task-final-requested", {
|
|
3473
3343
|
reviewRoundId: created.id,
|
|
3474
|
-
workItemId: created.workItemId,
|
|
3475
|
-
candidateId: created.candidateId,
|
|
3476
3344
|
reviewerRoleName: created.reviewerRoleName,
|
|
3477
3345
|
requestedBy: created.requestedBy,
|
|
3478
3346
|
taskCandidate: JSON.stringify(created.taskCandidate),
|
|
@@ -3532,14 +3400,10 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3532
3400
|
if (replacement === null
|
|
3533
3401
|
|| replacement.id === source.id
|
|
3534
3402
|
|| (replacement.scope ?? "work-item") !== "task"
|
|
3535
|
-
|| replacement.workItemId !== source.workItemId
|
|
3536
|
-
|| replacement.candidateId !== source.candidateId
|
|
3537
3403
|
|| replacement.reviewerRoleName !== source.reviewerRoleName
|
|
3538
3404
|
|| replacement.deltaRecheck !== undefined
|
|
3539
3405
|
|| !sameTaskFinalReviewContract(replacement.taskFinalReviewContract, source.taskFinalReviewContract)
|
|
3540
3406
|
|| !isSameTaskReviewCandidate(replacement.taskCandidate, source.taskCandidate)
|
|
3541
|
-
|| replacementEvent.payload.workItemId !== replacement.workItemId
|
|
3542
|
-
|| replacementEvent.payload.candidateId !== replacement.candidateId
|
|
3543
3407
|
|| replacementEvent.payload.reviewerRoleName !== replacement.reviewerRoleName
|
|
3544
3408
|
|| replacementEvent.payload.taskCandidate !== JSON.stringify(replacement.taskCandidate)) {
|
|
3545
3409
|
throw dataError(`Force-fresh audit for ${source.id} does not match its replacement Round.`);
|
|
@@ -3564,12 +3428,6 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3564
3428
|
|| source.executionGroup.lanes[0].roleName !== source.reviewerRoleName)) {
|
|
3565
3429
|
throw usageError(`ReviewRound ${source.id} is not a single-Reviewer full Review; force-fresh is refused.`);
|
|
3566
3430
|
}
|
|
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
3431
|
const provenance = taskReviewProvenance(tx, task, options);
|
|
3574
3432
|
if (!isSameTaskReviewCandidate(source.taskCandidate, provenance.candidate)) {
|
|
3575
3433
|
throw usageError(`Final ReviewRound ${source.id} freezes a candidate that is no longer the current Task candidate.`);
|
|
@@ -3598,10 +3456,10 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3598
3456
|
reviewer = createTaskRole(tx, task, source.reviewerRoleName, undefined, now, source.reviewerRoleName);
|
|
3599
3457
|
tx.saveRole(task.id, reviewer);
|
|
3600
3458
|
}
|
|
3601
|
-
let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.
|
|
3459
|
+
let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.reviewerRoleName, "leader", source.taskCandidate, now, taskFinalContract);
|
|
3602
3460
|
const group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
|
|
3603
3461
|
purpose: "review",
|
|
3604
|
-
target: executionTargetForReviewRound(task, created
|
|
3462
|
+
target: executionTargetForReviewRound(task, created),
|
|
3605
3463
|
strategy: { mode: "fixed", count: 1 },
|
|
3606
3464
|
lanes: [{ roleName: reviewer.name, reviewRoundId: created.id }]
|
|
3607
3465
|
}, now);
|
|
@@ -3611,8 +3469,6 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3611
3469
|
sourceReviewRoundId: source.id,
|
|
3612
3470
|
...(source.reviewerRunId === undefined ? {} : { sourceReviewerRunId: source.reviewerRunId }),
|
|
3613
3471
|
reviewRoundId: created.id,
|
|
3614
|
-
workItemId: created.workItemId,
|
|
3615
|
-
candidateId: created.candidateId,
|
|
3616
3472
|
reviewerRoleName: created.reviewerRoleName,
|
|
3617
3473
|
taskCandidate: JSON.stringify(created.taskCandidate),
|
|
3618
3474
|
reason: "source-round-terminal-without-semantic-review",
|
|
@@ -3763,15 +3619,9 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3763
3619
|
if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)) {
|
|
3764
3620
|
throw usageError(`Task final-review contract does not match ReviewRound ${round.id}.`);
|
|
3765
3621
|
}
|
|
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
|
-
}
|
|
3622
|
+
// Re-read the exact current Task heads and producer roles before touching
|
|
3623
|
+
// any record. A moved head or reviewer collision fails closed with the old
|
|
3624
|
+
// failed Round byte-for-byte unchanged.
|
|
3775
3625
|
const provenance = taskReviewProvenance(tx, task, options);
|
|
3776
3626
|
if (!isSameTaskReviewCandidate(round.taskCandidate, provenance.candidate)) {
|
|
3777
3627
|
throw usageError(`Final ReviewRound ${round.id} freezes a candidate that is no longer the current Task candidate.`);
|
|
@@ -3834,9 +3684,7 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3834
3684
|
const resetRound = retryTaskReviewRound(round);
|
|
3835
3685
|
tx.saveReviewRound(task.id, resetRound);
|
|
3836
3686
|
recordTaskEvent(tx, task.id, "review.task-final-retried", {
|
|
3837
|
-
reviewRoundId: round.id
|
|
3838
|
-
workItemId: round.workItemId,
|
|
3839
|
-
candidateId: round.candidateId
|
|
3687
|
+
reviewRoundId: round.id
|
|
3840
3688
|
}, now);
|
|
3841
3689
|
return { round: resetRound, created: true };
|
|
3842
3690
|
});
|
|
@@ -4026,7 +3874,7 @@ function listRuns(args, store, options) {
|
|
|
4026
3874
|
* on. This is deliberately narrower than retry: it cannot manufacture a
|
|
4027
3875
|
* review or fail an arbitrary Round, and every identity/mailbox fence is
|
|
4028
3876
|
* checked before the old Round changes. The next normal Task completion then
|
|
4029
|
-
* creates one fresh Round over the newer
|
|
3877
|
+
* creates one fresh Round over the newer frozen Task heads.
|
|
4030
3878
|
*/
|
|
4031
3879
|
function settleStaleFinalReviewRun(args, store, options) {
|
|
4032
3880
|
exactPositionals(args, 1, "Task run settle usage: yui task run settle <task>/<run>.");
|
|
@@ -4065,14 +3913,6 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4065
3913
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
4066
3914
|
throw usageError(`Review Run ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
4067
3915
|
}
|
|
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
3916
|
const activeReviewerRun = tx.listAgentRuns(task.id).find((entry) => (entry.purpose === "review" && entry.status === "active"));
|
|
4077
3917
|
const activeRoleRun = tx.getActiveAgentRun(task.id, round.reviewerRoleName);
|
|
4078
3918
|
if (activeReviewerRun !== undefined || activeRoleRun !== null) {
|
|
@@ -4159,7 +3999,6 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4159
3999
|
recordTaskEvent(tx, task.id, "run.review-stale-settled", {
|
|
4160
4000
|
runId: run.id,
|
|
4161
4001
|
reviewRoundId: round.id,
|
|
4162
|
-
candidateId: candidate.id,
|
|
4163
4002
|
previousTaskCandidate: JSON.stringify(round.taskCandidate),
|
|
4164
4003
|
currentTaskCandidate: JSON.stringify(currentTaskCandidate)
|
|
4165
4004
|
}, now);
|
|
@@ -4364,7 +4203,7 @@ function taskReviewProducerCollision(provenance, reviewerRoleName) {
|
|
|
4364
4203
|
.sort((left, right) => left.localeCompare(right, undefined, { numeric: true }));
|
|
4365
4204
|
return workItemIds.length === 0
|
|
4366
4205
|
? null
|
|
4367
|
-
: `Reviewer Role must be separate from every
|
|
4206
|
+
: `Reviewer Role must be separate from every WorkItem Candidate producer: `
|
|
4368
4207
|
+ `${reviewerRoleName} (${workItemIds.join(", ")}).`;
|
|
4369
4208
|
}
|
|
4370
4209
|
/**
|
|
@@ -4381,7 +4220,7 @@ function taskReviewProducerCollision(provenance, reviewerRoleName) {
|
|
|
4381
4220
|
function taskReviewProvenance(store, task, options, expected) {
|
|
4382
4221
|
const candidate = actualTaskReviewCandidateForMutation(store, task, options);
|
|
4383
4222
|
if (expected !== undefined && !isSameTaskReviewCandidate(candidate, expected)) {
|
|
4384
|
-
throw usageError(`Task-final ReviewRound frozen
|
|
4223
|
+
throw usageError(`Task-final ReviewRound frozen Task heads changed for its Project set.`);
|
|
4385
4224
|
}
|
|
4386
4225
|
const producerRoles = new Set();
|
|
4387
4226
|
const producerWorkItemIds = new Map();
|
|
@@ -4484,32 +4323,18 @@ function assertNoConflictingTaskReviewRound(rounds, reusableRoundIds = []) {
|
|
|
4484
4323
|
throw usageError(`Another active Task-final ReviewRound already exists: ${conflicting.id}/${conflicting.reviewerRoleName}.`);
|
|
4485
4324
|
}
|
|
4486
4325
|
}
|
|
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
4326
|
/**
|
|
4502
4327
|
* Queues a Task-scoped final ReviewRound. The reviewer Role must be separate
|
|
4503
4328
|
* from the Candidate producer. Returns the round (pending or failed) so the
|
|
4504
4329
|
* caller can surface it to the CLI for workspace preparation and dispatch.
|
|
4505
4330
|
*/
|
|
4506
|
-
function queueTaskReviewRound(store, task,
|
|
4331
|
+
function queueTaskReviewRound(store, task, config, taskCandidate, options, now, requestedBy = "policy", taskFinalContract) {
|
|
4507
4332
|
assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id));
|
|
4508
4333
|
const provenance = taskReviewProvenance(store, task, options, taskCandidate);
|
|
4509
4334
|
if (!isSameTaskReviewCandidate(provenance.candidate, taskCandidate)) {
|
|
4510
|
-
throw usageError(`Task-final ReviewRound
|
|
4335
|
+
throw usageError(`Task-final ReviewRound frozen Task heads changed before queueing.`);
|
|
4511
4336
|
}
|
|
4512
|
-
const pending = createTaskReviewRound(store.nextReviewRoundId(task.id), task.id,
|
|
4337
|
+
const pending = createTaskReviewRound(store.nextReviewRoundId(task.id), task.id, config.roleName, requestedBy, taskCandidate, now, taskFinalContract);
|
|
4513
4338
|
store.saveReviewRound(task.id, pending);
|
|
4514
4339
|
const producerCollision = taskReviewProducerCollision(provenance, config.roleName);
|
|
4515
4340
|
if (producerCollision !== null) {
|
|
@@ -4543,36 +4368,17 @@ function queueTaskReviewRound(store, task, item, candidateId, config, taskCandid
|
|
|
4543
4368
|
* (or a failed round is surfaced as a blocker).
|
|
4544
4369
|
*/
|
|
4545
4370
|
function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
4546
|
-
//
|
|
4547
|
-
//
|
|
4548
|
-
//
|
|
4549
|
-
//
|
|
4550
|
-
|
|
4371
|
+
// A Leader-requested ReviewRound is evidence, not policy. Only an explicit
|
|
4372
|
+
// immutable Task contract creates a durable completion obligation; optional
|
|
4373
|
+
// historical Rounds never cause completion to manufacture another Round for
|
|
4374
|
+
// a later head.
|
|
4375
|
+
if (taskFinalContract === undefined || task.projectBindings.length === 0)
|
|
4376
|
+
return null;
|
|
4551
4377
|
const taskRounds = reviewRoundsByIdentity(store.listReviewRounds(task.id))
|
|
4552
4378
|
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
4553
|
-
&&
|
|
4379
|
+
&& sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)));
|
|
4554
4380
|
const establishedRound = taskRounds.at(-1);
|
|
4555
|
-
|
|
4556
|
-
if (taskFinalContract !== undefined) {
|
|
4557
|
-
config = taskFinalReviewConfig(taskFinalContract);
|
|
4558
|
-
}
|
|
4559
|
-
else if (establishedRound === undefined) {
|
|
4560
|
-
const globalConfig = store.getReviewConfig();
|
|
4561
|
-
// Direct delivery is an explicit low-overhead contract. Mutable global
|
|
4562
|
-
// policy must not create a managed Round during direct completion; risk
|
|
4563
|
-
// that warrants one promotes the Task to integrated delivery. Any already-
|
|
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;
|
|
4570
|
-
}
|
|
4571
|
-
else {
|
|
4572
|
-
config = { roleName: establishedRound.reviewerRoleName, trigger: "final" };
|
|
4573
|
-
}
|
|
4574
|
-
if (config === null || task.projectBindings.length === 0)
|
|
4575
|
-
return null;
|
|
4381
|
+
const config = taskFinalReviewConfig(taskFinalContract);
|
|
4576
4382
|
const taskCandidate = taskReviewProvenance(store, task, options).candidate;
|
|
4577
4383
|
const latest = establishedRound;
|
|
4578
4384
|
if (latest?.status === "running") {
|
|
@@ -4593,10 +4399,7 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4593
4399
|
&& latest.deltaRecheck !== undefined
|
|
4594
4400
|
&& latest.deltaRecheck.disposition === "requires-full-review") {
|
|
4595
4401
|
// 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);
|
|
4402
|
+
const escalated = queueTaskReviewRound(store, task, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
|
|
4600
4403
|
// Record the escalation lineage on the delta Round so the full Review
|
|
4601
4404
|
// is traceable from the non-accepting delta disposition.
|
|
4602
4405
|
if (latest.deltaRecheck.escalatedToReviewRoundId === undefined) {
|
|
@@ -4617,10 +4420,7 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4617
4420
|
: latest;
|
|
4618
4421
|
}
|
|
4619
4422
|
}
|
|
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);
|
|
4423
|
+
return queueTaskReviewRound(store, task, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
|
|
4624
4424
|
}
|
|
4625
4425
|
function resumablePendingFinalTaskReview(store, task, round, config, taskCandidate, taskFinalContract, options) {
|
|
4626
4426
|
if (taskFinalContract === undefined || round.taskFinalReviewContract === undefined) {
|
|
@@ -4685,22 +4485,6 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
4685
4485
|
}
|
|
4686
4486
|
}
|
|
4687
4487
|
}
|
|
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
4488
|
/**
|
|
4705
4489
|
* Leader-only retry of an exact failed Task-final review Run. The old failed
|
|
4706
4490
|
* Run remains the attempt trail, while the semantic ReviewRound is reset to
|
|
@@ -4743,15 +4527,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4743
4527
|
}
|
|
4744
4528
|
const currentTaskCandidate = actualTaskReviewCandidateForMutation(tx, task, options);
|
|
4745
4529
|
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}.`);
|
|
4530
|
+
throw usageError(`Task-final ReviewRound ${round.id} no longer matches the frozen Task heads.`);
|
|
4755
4531
|
}
|
|
4756
4532
|
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
|
|
4757
4533
|
.filter((entry) => (entry.scope ?? "work-item") === "task"));
|
|
@@ -4885,8 +4661,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4885
4661
|
tx.saveReviewRound(task.id, resetRound);
|
|
4886
4662
|
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
4887
4663
|
runId: run.id,
|
|
4888
|
-
reviewRoundId: round.id
|
|
4889
|
-
candidateId: candidate.id
|
|
4664
|
+
reviewRoundId: round.id
|
|
4890
4665
|
}, now);
|
|
4891
4666
|
return { round: resetRound, previousRun: run, created: true };
|
|
4892
4667
|
});
|
|
@@ -5300,8 +5075,8 @@ function yieldRun(args, store, options) {
|
|
|
5300
5075
|
const panelGroup = round.executionGroup;
|
|
5301
5076
|
recordTaskEvent(tx, task.id, "review-group-ready", {
|
|
5302
5077
|
reviewRoundId: round.id,
|
|
5303
|
-
workItemId: round.workItemId,
|
|
5304
|
-
candidateId: round.candidateId,
|
|
5078
|
+
...(round.workItemId === undefined ? {} : { workItemId: round.workItemId }),
|
|
5079
|
+
...(round.candidateId === undefined ? {} : { candidateId: round.candidateId }),
|
|
5305
5080
|
executionGroupId: panelGroup.id,
|
|
5306
5081
|
terminalLanes: String(panelGroup.lanes
|
|
5307
5082
|
.filter(({ status }) => ["yielded", "completed", "failed"].includes(status)).length),
|
|
@@ -5315,8 +5090,8 @@ function yieldRun(args, store, options) {
|
|
|
5315
5090
|
}
|
|
5316
5091
|
recordTaskEvent(tx, task.id, "review.completed", {
|
|
5317
5092
|
reviewRoundId: round.id,
|
|
5318
|
-
workItemId: round.workItemId,
|
|
5319
|
-
candidateId: round.candidateId,
|
|
5093
|
+
...(round.workItemId === undefined ? {} : { workItemId: round.workItemId }),
|
|
5094
|
+
...(round.candidateId === undefined ? {} : { candidateId: round.candidateId }),
|
|
5320
5095
|
reviewBaseCommit: round.reviewBaseCommit,
|
|
5321
5096
|
evidenceCommit: round.evidenceCommit ?? "none",
|
|
5322
5097
|
checks: round.checks?.map(({ name, outcome }) => `${name}:${outcome}`)
|
|
@@ -5614,18 +5389,12 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5614
5389
|
}
|
|
5615
5390
|
throw usageError(`ReviewRound workspace is not ready: ${round.id}.`);
|
|
5616
5391
|
}
|
|
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
5392
|
const taskScope = (round.scope ?? "work-item") === "task";
|
|
5393
|
+
let item;
|
|
5394
|
+
let candidate;
|
|
5625
5395
|
if (taskScope) {
|
|
5626
|
-
// A Task-scoped final ReviewRound
|
|
5627
|
-
//
|
|
5628
|
-
// authoritative review base is the first Project's committed head.
|
|
5396
|
+
// A Task-scoped final ReviewRound is anchored directly to its frozen
|
|
5397
|
+
// Task candidate. It deliberately has no synthetic WorkItem Candidate.
|
|
5629
5398
|
if (round.taskCandidate === undefined) {
|
|
5630
5399
|
throw new TaskFinalReviewDispatchDriftError(`Task ReviewRound ${round.id} has no frozen Task candidate.`);
|
|
5631
5400
|
}
|
|
@@ -5633,8 +5402,21 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5633
5402
|
throw new TaskFinalReviewDispatchDriftError(`Task ReviewRound base does not match its frozen Task candidate: ${round.id}.`);
|
|
5634
5403
|
}
|
|
5635
5404
|
}
|
|
5636
|
-
else
|
|
5637
|
-
|
|
5405
|
+
else {
|
|
5406
|
+
if (round.workItemId === undefined || round.candidateId === undefined) {
|
|
5407
|
+
throw dataError(`WorkItem ReviewRound has no Candidate anchor: ${round.id}.`);
|
|
5408
|
+
}
|
|
5409
|
+
item = tx.getWorkItem(taskId, round.workItemId) ?? undefined;
|
|
5410
|
+
if (item === undefined) {
|
|
5411
|
+
throw dataError(`ReviewRound Work Item not found: ${round.workItemId}.`);
|
|
5412
|
+
}
|
|
5413
|
+
candidate = item.candidates.find(({ id }) => id === round.candidateId);
|
|
5414
|
+
if (candidate === undefined) {
|
|
5415
|
+
throw dataError(`ReviewRound Candidate not found: ${round.candidateId}.`);
|
|
5416
|
+
}
|
|
5417
|
+
if (candidate.gitSnapshot?.reviewBaseCommit !== round.reviewBaseCommit) {
|
|
5418
|
+
throw usageError(`ReviewRound Candidate snapshot changed: ${round.id}.`);
|
|
5419
|
+
}
|
|
5638
5420
|
}
|
|
5639
5421
|
const task = requireTask(tx, taskId);
|
|
5640
5422
|
if ((round.scope ?? "work-item") === "task") {
|
|
@@ -5671,7 +5453,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5671
5453
|
&& frozenCommits.get(project.projectId) !== project.commit;
|
|
5672
5454
|
});
|
|
5673
5455
|
throw new TaskFinalReviewDispatchDriftError(committedIntegrationMoved
|
|
5674
|
-
? "Task-final ReviewRound frozen
|
|
5456
|
+
? "Task-final ReviewRound frozen Task heads changed for its Project set."
|
|
5675
5457
|
: `Final ReviewRound ${round.id} freezes a candidate that is no longer `
|
|
5676
5458
|
+ "the current Task candidate.");
|
|
5677
5459
|
}
|
|
@@ -5732,9 +5514,11 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5732
5514
|
throw new TaskFinalReviewDispatchDriftError(`Task ReviewRound frozen Project heads changed: ${round.id}.`);
|
|
5733
5515
|
}
|
|
5734
5516
|
}
|
|
5735
|
-
const candidateLabel =
|
|
5736
|
-
?
|
|
5737
|
-
:
|
|
5517
|
+
const candidateLabel = taskScope
|
|
5518
|
+
? "frozen Task candidate"
|
|
5519
|
+
: candidate.source.type === "run"
|
|
5520
|
+
? `candidate Run ${candidate.source.runId}`
|
|
5521
|
+
: `revision ${candidate.workItemRevision}`;
|
|
5738
5522
|
const frozenHeads = taskScope
|
|
5739
5523
|
? round.taskCandidate.projects
|
|
5740
5524
|
.map(({ projectId, commit }) => `${projectId}@${commit}`)
|
|
@@ -5772,22 +5556,26 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5772
5556
|
.map(({ projectId }) => (`yui project show ${projectId}; yui project knowledge list ${projectId}`))
|
|
5773
5557
|
.join(" | ");
|
|
5774
5558
|
const rawInput = [
|
|
5775
|
-
|
|
5559
|
+
taskScope
|
|
5560
|
+
? `Review the ${scopeLabel} ${candidateLabel}.`
|
|
5561
|
+
: `Review ${scopeLabel} WorkItem ${item.id} ${candidateLabel}.`,
|
|
5776
5562
|
`ReviewRound: ${round.id}`,
|
|
5777
5563
|
`Review scope: ${taskScope ? "task" : "work-item"}`,
|
|
5778
5564
|
`Review base commit: ${round.reviewBaseCommit}`,
|
|
5779
5565
|
...(taskScope
|
|
5780
|
-
? [`Frozen
|
|
5566
|
+
? [`Frozen Task heads: ${frozenHeads}`]
|
|
5781
5567
|
: [`Candidate snapshot base: ${round.reviewBaseCommit}`]),
|
|
5782
5568
|
`Project Policy pointers: ${projectPolicyPointers || "none"}`,
|
|
5783
5569
|
`Review workspace source: exact workspace attached to this Reviewer Lane`,
|
|
5784
|
-
`Candidate summary: ${candidate.summary}`,
|
|
5785
|
-
`Acceptance criteria: ${
|
|
5570
|
+
`Candidate summary: ${taskScope ? task.title : candidate.summary}`,
|
|
5571
|
+
`Acceptance criteria: ${taskScope
|
|
5572
|
+
? "Task objective, maintained decisions, and Project Policy"
|
|
5573
|
+
: item.acceptance.length === 0 ? "none" : item.acceptance.join("; ")}`,
|
|
5786
5574
|
...(taskScope ? [deltaContext !== "" ? deltaContext : findingContext] : []),
|
|
5787
5575
|
"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
5576
|
"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
5577
|
...(round.scope === "task" && round.deltaRecheck === undefined
|
|
5790
|
-
? ["This is the one final Task Review: inspect every bound Project at the frozen
|
|
5578
|
+
? ["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
5579
|
: []),
|
|
5792
5580
|
"You may freely edit source/tests, run local build or test commands, and optionally commit diagnostic evidence only inside this ReviewRound-owned workspace.",
|
|
5793
5581
|
"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 +5636,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5848
5636
|
action: "review-round",
|
|
5849
5637
|
subject: {
|
|
5850
5638
|
taskId,
|
|
5851
|
-
workItemId: item.id,
|
|
5639
|
+
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5852
5640
|
reviewRoundId: round.id,
|
|
5853
5641
|
executionGroupId: runningGroup.id,
|
|
5854
5642
|
executionLaneId: lane.id
|
|
@@ -5864,7 +5652,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5864
5652
|
workspace: laneWorkspace
|
|
5865
5653
|
}, now);
|
|
5866
5654
|
createdRuns.push(createAgentRun(runId, taskId, laneReviewer.name, roleAgentSessionResumeMode(sessions, effective.agentId, effective), assignment, now, {
|
|
5867
|
-
workItemId: item.id,
|
|
5655
|
+
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5868
5656
|
purpose: "review",
|
|
5869
5657
|
reviewRoundId: round.id,
|
|
5870
5658
|
executionGroupId: runningGroup.id,
|
|
@@ -5901,7 +5689,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5901
5689
|
taskId,
|
|
5902
5690
|
roleName: unboundRun.roleName,
|
|
5903
5691
|
purpose: "review",
|
|
5904
|
-
workItemId: item.id,
|
|
5692
|
+
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5905
5693
|
reviewRoundId: round.id
|
|
5906
5694
|
}, now);
|
|
5907
5695
|
const created = withAgentRunContextSnapshot(unboundRun, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
@@ -5912,7 +5700,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5912
5700
|
tx.saveRole(taskId, updateRoleStatus(laneReviewer, "running", now));
|
|
5913
5701
|
enqueueWork(tx, roleMailbox(taskId, laneReviewer.name), "review-requested", now, [
|
|
5914
5702
|
runRef(taskId, created.id),
|
|
5915
|
-
workItemRef(taskId, item.id)
|
|
5703
|
+
...(item === undefined ? [] : [workItemRef(taskId, item.id)])
|
|
5916
5704
|
]);
|
|
5917
5705
|
recordTaskEvent(tx, taskId, "run.review-dispatched", runLaunchEventPayload(created), now);
|
|
5918
5706
|
}
|
|
@@ -5934,7 +5722,7 @@ export function failPendingReviewRound(taskId, reviewRoundId, summary, store, op
|
|
|
5934
5722
|
const terminal = finishReviewRound(round, "failed", summary, now);
|
|
5935
5723
|
tx.saveReviewRound(taskId, terminal);
|
|
5936
5724
|
enqueueWork(tx, leaderMailbox(taskId), "review-failed", now, [
|
|
5937
|
-
workItemRef(taskId, round.workItemId)
|
|
5725
|
+
...(round.workItemId === undefined ? [] : [workItemRef(taskId, round.workItemId)])
|
|
5938
5726
|
]);
|
|
5939
5727
|
return terminal;
|
|
5940
5728
|
});
|
|
@@ -6068,7 +5856,7 @@ function requirePublishedTreeAuthorization(store, proof) {
|
|
|
6068
5856
|
if (authorization === undefined) {
|
|
6069
5857
|
throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
|
|
6070
5858
|
+ `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
|
|
6071
|
-
? "the
|
|
5859
|
+
? "the Leader-owned Task-main head"
|
|
6072
5860
|
: `Task-final Review ${proof.reviewRoundId}`}.`);
|
|
6073
5861
|
}
|
|
6074
5862
|
return authorization;
|
|
@@ -6337,11 +6125,6 @@ function parseTaskPriority(value) {
|
|
|
6337
6125
|
return value;
|
|
6338
6126
|
throw usageError(`Invalid Task priority: ${value}.`);
|
|
6339
6127
|
}
|
|
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
6128
|
function parseTaskTags(value) {
|
|
6346
6129
|
const tags = [...new Set(value.split(",").map((tag) => tag.trim()).filter(Boolean))];
|
|
6347
6130
|
if (tags.length === 0)
|