@zq-silk/yui 0.8.3 → 0.8.6
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 +40 -22
- package/README.md +46 -16
- package/dist/cli/commandCatalog.js +23 -11
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli.js +154 -16
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +386 -138
- package/dist/commands/taskCompletionGate.js +36 -24
- package/dist/commands/taskContextCommand.js +6 -1
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +36 -3
- package/dist/context/sessionBootstrapManifest.js +82 -1
- package/dist/controller/clientRuntime.js +7 -7
- package/dist/controller/controller.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
- package/dist/controller/handoverCandidate.js +10 -3
- package/dist/controller/sessionNotify.js +4 -22
- package/dist/executor/agentAdapter.js +2 -2
- package/dist/executor/agentExecutor.js +25 -5
- package/dist/executor/fileRoleLaunchPlanner.js +16 -11
- package/dist/integration/gitIntegrationService.js +50 -2
- package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
- package/dist/observability/executionAudit.js +47 -1
- package/dist/observability/faultClassification.js +6 -4
- package/dist/observability/orchestrationMetrics.js +196 -0
- package/dist/operator/operatorSessionHistory.js +36 -0
- package/dist/release/releaseHandover.js +7 -5
- package/dist/release/runtimeRelease.js +15 -0
- package/dist/repository/taskWorkspaceCoordinator.js +13 -10
- package/dist/review/deltaRecheck.js +3 -2
- package/dist/review/reviewFindingLedger.js +5 -4
- package/dist/review/reviewOutcomeClassifier.js +252 -54
- package/dist/review/taskFinalReviewContractEvent.js +1 -0
- package/dist/review/taskFinalReviewContractRebind.js +350 -0
- package/dist/run/runIdentity.js +10 -70
- package/dist/runtime/agentHost.js +3 -4
- package/dist/runtime/codexAppServerRuntime.js +6 -0
- package/dist/runtime/firstProgressStopLoss.js +52 -0
- package/dist/runtime/launchBroker.js +10 -2
- package/dist/runtime/runtimeDeadlines.js +14 -0
- package/dist/runtime/sessionTitle.js +24 -12
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +10 -3
- package/dist/scheduler/activeRoleRunDelivery.js +20 -18
- package/dist/scheduler/leaderWakeupProcessor.js +33 -2
- package/dist/scheduler/wakeReason.js +1 -0
- package/dist/storage/sqliteStore.js +8 -1
- package/dist/storage/taskStore.js +10 -1
- package/dist/task/completionReadiness.js +48 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +145 -52
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/web/webSnapshot.js +7 -1
- package/i18n/README.zh-CN.md +28 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +73 -31
- package/skills/yui-operator/SKILL.md +51 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
|
@@ -8,7 +8,7 @@ import { createTaskEvent } from "../event/taskEvent.js";
|
|
|
8
8
|
import { clearMatchingLeaderStallAttention, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
|
|
9
9
|
import { readCommandText } from "./textInput.js";
|
|
10
10
|
import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
|
|
11
|
-
import { createRoleSessionSet, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
11
|
+
import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
12
12
|
import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
|
|
13
13
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
14
14
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
@@ -35,7 +35,7 @@ import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coord
|
|
|
35
35
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
36
36
|
import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
37
37
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
38
|
-
import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
38
|
+
import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, taskDeliveryPath, updateTaskMetadata } from "../task/task.js";
|
|
39
39
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
40
40
|
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
41
41
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
@@ -44,6 +44,8 @@ import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenance
|
|
|
44
44
|
import { currentWorkItemCandidate, governingWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
45
45
|
import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
|
|
46
46
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
47
|
+
import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
48
|
+
import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, createTaskFinalReviewContractRebind, resolveRecordedTaskFinalReviewContract, taskFinalReviewContractRebindPayload } from "../review/taskFinalReviewContractRebind.js";
|
|
47
49
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
48
50
|
import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
|
|
49
51
|
import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
|
|
@@ -52,7 +54,7 @@ import { runTaskContextCommand } from "./taskContextCommand.js";
|
|
|
52
54
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
53
55
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
54
56
|
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
55
|
-
import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
57
|
+
import { assertNoOpenInputRequests, isCurrentGlobalOperator, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
56
58
|
import { runGrantCommand } from "./grantCommands.js";
|
|
57
59
|
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
58
60
|
import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
|
|
@@ -81,21 +83,17 @@ function legacyWorkItemReviewConfig(config) {
|
|
|
81
83
|
return config?.trigger === "final" ? null : config;
|
|
82
84
|
}
|
|
83
85
|
function storedTaskFinalReviewContract(store, taskId) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
validateTaskFinalReviewContract(first);
|
|
95
|
-
if (contracts.some((contract) => !sameTaskFinalReviewContract(first, contract))) {
|
|
96
|
-
throw dataError(`Task ${taskId} contains conflicting final-review contracts.`);
|
|
86
|
+
return storedTaskFinalReviewContractResolution(store, taskId)?.effective;
|
|
87
|
+
}
|
|
88
|
+
function storedTaskFinalReviewContractResolution(store, taskId) {
|
|
89
|
+
try {
|
|
90
|
+
return resolveRecordedTaskFinalReviewContract(taskId, store.listWorkItems(taskId), store.listReviewRounds(taskId), store.listEvents(taskId));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw dataError(error instanceof Error
|
|
94
|
+
? error.message
|
|
95
|
+
: `Task ${taskId} contains conflicting final-review contracts.`);
|
|
97
96
|
}
|
|
98
|
-
return first;
|
|
99
97
|
}
|
|
100
98
|
/**
|
|
101
99
|
* Resolve one exact Task-local contract before the caller performs any write.
|
|
@@ -367,12 +365,14 @@ function taskProjectCommand(args, store, options) {
|
|
|
367
365
|
return output(`Added Project to ${updated.id}\n`, { task: updated });
|
|
368
366
|
}
|
|
369
367
|
function updateTaskCommand(args, store, options) {
|
|
370
|
-
const optionNames = new Set([
|
|
368
|
+
const optionNames = new Set([
|
|
369
|
+
"--title", "--description", "--priority", "--tags", "--due-at", "--delivery"
|
|
370
|
+
]);
|
|
371
371
|
const flagOptions = new Set([
|
|
372
372
|
"--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at",
|
|
373
373
|
"--require-integration"
|
|
374
374
|
]);
|
|
375
|
-
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] [--require-integration].";
|
|
375
|
+
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] [--delivery <direct|integrated>] [--require-integration].";
|
|
376
376
|
const parsed = parseTail(args, optionNames, usage, flagOptions);
|
|
377
377
|
exactPositionals(parsed.positionals, 1, usage);
|
|
378
378
|
if (parsed.options.size === 0)
|
|
@@ -396,19 +396,42 @@ function updateTaskCommand(args, store, options) {
|
|
|
396
396
|
const tags = parsed.options.has("--tags")
|
|
397
397
|
? parseTaskTags(requiredOption(parsed.options, "--tags"))
|
|
398
398
|
: undefined;
|
|
399
|
+
const requestedDelivery = parsed.options.has("--delivery")
|
|
400
|
+
? parseTaskDelivery(requiredOption(parsed.options, "--delivery"))
|
|
401
|
+
: undefined;
|
|
402
|
+
if (requestedDelivery === "direct" && parsed.options.has("--require-integration")) {
|
|
403
|
+
throw usageError("--delivery direct conflicts with --require-integration.", usage);
|
|
404
|
+
}
|
|
405
|
+
const enableIntegration = requestedDelivery === "integrated"
|
|
406
|
+
|| parsed.options.has("--require-integration");
|
|
399
407
|
const now = clock(options);
|
|
400
408
|
const result = store.transaction((tx) => {
|
|
401
409
|
const current = requireTask(tx, parsed.positionals[0]);
|
|
402
410
|
if (current.status === "archived")
|
|
403
411
|
throw usageError(`Task is archived: ${current.id}.`);
|
|
404
|
-
if (parsed.options.has("--require-integration")
|
|
412
|
+
if ((requestedDelivery !== undefined || parsed.options.has("--require-integration"))
|
|
413
|
+
&& current.projectBindings.length === 0) {
|
|
414
|
+
throw usageError(`Task ${current.id} has no Project; delivery selection is not applicable.`);
|
|
415
|
+
}
|
|
416
|
+
if (requestedDelivery === "direct" && current.requireIntegration === true) {
|
|
417
|
+
throw usageError(`Task ${current.id} already uses integrated delivery and cannot be downgraded to direct.`);
|
|
418
|
+
}
|
|
419
|
+
if (enableIntegration && current.status === "completed") {
|
|
405
420
|
throw usageError(`Task ${current.id} is completed; use task reopen before enabling integration evidence.`);
|
|
406
421
|
}
|
|
422
|
+
if (enableIntegration && current.requireIntegration !== true) {
|
|
423
|
+
assertTaskDeliveryPromotionEligible(tx, current, options.directTaskMainSnapshot);
|
|
424
|
+
}
|
|
407
425
|
if (parsed.options.size === 1
|
|
408
|
-
&&
|
|
426
|
+
&& enableIntegration
|
|
409
427
|
&& current.requireIntegration === true) {
|
|
410
428
|
return { task: current, integrationState: "already-enabled" };
|
|
411
429
|
}
|
|
430
|
+
if (parsed.options.size === 1
|
|
431
|
+
&& requestedDelivery === "direct"
|
|
432
|
+
&& taskDeliveryPath(current) === "direct") {
|
|
433
|
+
return { task: current, integrationState: "already-direct" };
|
|
434
|
+
}
|
|
412
435
|
const updated = updateTaskMetadata(current, {
|
|
413
436
|
...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
|
|
414
437
|
...(parsed.options.has("--description")
|
|
@@ -423,31 +446,93 @@ function updateTaskCommand(args, store, options) {
|
|
|
423
446
|
...(dueAt === undefined
|
|
424
447
|
? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
|
|
425
448
|
: { dueAt }),
|
|
426
|
-
...(
|
|
449
|
+
...(enableIntegration ? { requireIntegration: true } : {})
|
|
427
450
|
}, now);
|
|
428
451
|
tx.saveTask(updated);
|
|
429
452
|
recordTaskEvent(tx, updated.id, "task.updated", {
|
|
430
453
|
status: updated.status,
|
|
431
|
-
...(parsed.options.has("--require-integration")
|
|
432
|
-
? {
|
|
433
|
-
: {
|
|
454
|
+
...(requestedDelivery === undefined && !parsed.options.has("--require-integration")
|
|
455
|
+
? {}
|
|
456
|
+
: {
|
|
457
|
+
completionEvidence: enableIntegration
|
|
458
|
+
? "integration-required"
|
|
459
|
+
: "direct",
|
|
460
|
+
deliveryPath: taskDeliveryPath(updated)
|
|
461
|
+
})
|
|
434
462
|
}, now);
|
|
435
463
|
enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
|
|
436
464
|
return {
|
|
437
465
|
task: updated,
|
|
438
|
-
integrationState:
|
|
466
|
+
integrationState: enableIntegration
|
|
439
467
|
? "enabled"
|
|
440
|
-
: "
|
|
468
|
+
: requestedDelivery === "direct"
|
|
469
|
+
? "direct"
|
|
470
|
+
: "unchanged"
|
|
441
471
|
};
|
|
442
472
|
});
|
|
443
|
-
if (result.integrationState !== "already-enabled"
|
|
473
|
+
if (result.integrationState !== "already-enabled"
|
|
474
|
+
&& result.integrationState !== "already-direct") {
|
|
444
475
|
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
445
476
|
}
|
|
446
477
|
return result.integrationState === "enabled"
|
|
447
|
-
? `Updated task ${result.task.id}\
|
|
478
|
+
? `Updated task ${result.task.id}\nDelivery: integrated (WorkItem, ChangeSet, and committed Integration required)\n`
|
|
448
479
|
: result.integrationState === "already-enabled"
|
|
449
|
-
? `Task ${result.task.id}
|
|
450
|
-
:
|
|
480
|
+
? `Task ${result.task.id} already uses integrated delivery\n`
|
|
481
|
+
: result.integrationState === "already-direct"
|
|
482
|
+
? `Task ${result.task.id} already uses direct delivery\n`
|
|
483
|
+
: result.integrationState === "direct"
|
|
484
|
+
? `Updated task ${result.task.id}\nDelivery: direct\n`
|
|
485
|
+
: `Updated task ${result.task.id}\n`;
|
|
486
|
+
}
|
|
487
|
+
function assertTaskDeliveryPromotionEligible(store, task, snapshot) {
|
|
488
|
+
const evidence = [
|
|
489
|
+
...store.listWorkItems(task.id).map(({ id }) => `WorkItem ${id}`),
|
|
490
|
+
...store.listChangeSets(task.id).map(({ id }) => `ChangeSet ${id}`),
|
|
491
|
+
...store.listIntegrationAttempts(task.id).map(({ id }) => `IntegrationAttempt ${id}`),
|
|
492
|
+
...store.listReviewRounds(task.id).map(({ id }) => `ReviewRound ${id}`)
|
|
493
|
+
];
|
|
494
|
+
if (evidence.length > 0) {
|
|
495
|
+
throw usageError(`Task ${task.id} cannot promote to integrated delivery after delivery evidence exists: `
|
|
496
|
+
+ `${evidence.join(", ")}. Create an integrated replacement Task or keep the current direct contract.`);
|
|
497
|
+
}
|
|
498
|
+
if (task.status !== "draft" && task.status !== "active") {
|
|
499
|
+
throw usageError(`Task ${task.id} must be Draft or Active to promote delivery; current status is ${task.status}.`);
|
|
500
|
+
}
|
|
501
|
+
const workspace = store.getTaskWorkspace(task.id);
|
|
502
|
+
if (task.status === "draft" && workspace === null)
|
|
503
|
+
return;
|
|
504
|
+
if (snapshot === undefined) {
|
|
505
|
+
throw usageError(`Task ${task.id} delivery promotion requires a CLI-verified clean Task-main snapshot.`);
|
|
506
|
+
}
|
|
507
|
+
if (workspace === null
|
|
508
|
+
|| workspace.owner.type !== "task"
|
|
509
|
+
|| workspace.owner.taskId !== task.id) {
|
|
510
|
+
throw usageError(`Task has no authoritative main workspace: ${task.id}.`);
|
|
511
|
+
}
|
|
512
|
+
const snapshotIds = snapshot.schemaVersion === 1 && Array.isArray(snapshot.projects)
|
|
513
|
+
? snapshot.projects.map(({ projectId }) => projectId)
|
|
514
|
+
: [];
|
|
515
|
+
if (snapshotIds.length !== task.projectBindings.length
|
|
516
|
+
|| new Set(snapshotIds).size !== snapshotIds.length) {
|
|
517
|
+
throw usageError(`Task-main promotion snapshot does not match bound Projects: ${task.id}.`);
|
|
518
|
+
}
|
|
519
|
+
for (const binding of task.projectBindings) {
|
|
520
|
+
const project = snapshot.projects.find(({ projectId }) => projectId === binding.projectId);
|
|
521
|
+
const entry = workspace.entries.find(({ projectId }) => projectId === binding.projectId);
|
|
522
|
+
if (project === undefined
|
|
523
|
+
|| entry === undefined
|
|
524
|
+
|| entry.access !== "write"
|
|
525
|
+
|| project.directory !== entry.directory
|
|
526
|
+
|| project.branch !== entry.branch
|
|
527
|
+
|| project.baseCommit !== entry.baseCommit) {
|
|
528
|
+
throw usageError(`Task-main promotion snapshot changed before mutation: ${task.id}/${binding.projectId}.`);
|
|
529
|
+
}
|
|
530
|
+
if (project.headCommit !== project.baseCommit) {
|
|
531
|
+
throw usageError(`Task ${task.id} main already advanced for Project ${binding.projectId}; `
|
|
532
|
+
+ "cannot promote without losing ChangeSet provenance. Create an integrated replacement "
|
|
533
|
+
+ "Task or keep the current direct contract.");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
451
536
|
}
|
|
452
537
|
/** Compatibility helper for call sites that cannot yet handle foreground enter. */
|
|
453
538
|
export function runTaskOutputCommand(args, store, options = {}) {
|
|
@@ -488,13 +573,20 @@ function createTaskCommand(args, store, options) {
|
|
|
488
573
|
notifyMailbox(options.runtime, taskMailbox(created.task.id), created.task.id);
|
|
489
574
|
return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
|
|
490
575
|
+ `Assigned role: ${created.leader.name}\n`
|
|
576
|
+
+ `Delivery: ${taskDeliveryPath(created.task)}\n`
|
|
491
577
|
+ (created.task.requireIntegration
|
|
492
578
|
? "Completion: WorkItem, ChangeSet, and committed Integration required\n"
|
|
493
|
-
:
|
|
579
|
+
: created.task.projectBindings.length > 0
|
|
580
|
+
? "Completion: clean committed Task main required; no WorkItem, ChangeSet, IntegrationAttempt, or managed ReviewRound required\n"
|
|
581
|
+
: "Completion: no Project delivery evidence required\n"), {
|
|
582
|
+
task: created.task,
|
|
583
|
+
leader: created.leader,
|
|
584
|
+
deliveryPath: taskDeliveryPath(created.task)
|
|
585
|
+
});
|
|
494
586
|
}
|
|
495
587
|
function parseTaskCreation(args, store) {
|
|
496
|
-
const usage = "Task create usage: yui task create <title> [--project <project> ...] [--base <project>=<ref> ...] [--require-integration].";
|
|
497
|
-
const parsed = parseMultiValueTail(args, new Set(), new Set(["--project", "--base"]), usage, new Set(["--require-integration"]));
|
|
588
|
+
const usage = "Task create usage: yui task create <title> [--project <project> ...] [--base <project>=<ref> ...] [--delivery <direct|integrated>] [--require-integration].";
|
|
589
|
+
const parsed = parseMultiValueTail(args, new Set(["--delivery"]), new Set(["--project", "--base"]), usage, new Set(["--require-integration"]));
|
|
498
590
|
exactPositionals(parsed.positionals, 1, usage);
|
|
499
591
|
const projectReferences = parsed.multiOptions.get("--project") ?? [];
|
|
500
592
|
const baseOptions = parsed.multiOptions.get("--base") ?? [];
|
|
@@ -510,6 +602,16 @@ function parseTaskCreation(args, store) {
|
|
|
510
602
|
if (new Set(projects.map(({ id }) => id)).size !== projects.length) {
|
|
511
603
|
throw usageError("A Task cannot bind the same Project more than once.");
|
|
512
604
|
}
|
|
605
|
+
const requestedDelivery = parsed.options.has("--delivery")
|
|
606
|
+
? parseTaskDelivery(requiredOption(parsed.options, "--delivery"))
|
|
607
|
+
: undefined;
|
|
608
|
+
if ((requestedDelivery !== undefined || parsed.options.has("--require-integration"))
|
|
609
|
+
&& projects.length === 0) {
|
|
610
|
+
throw usageError("Delivery selection requires at least one --project.", usage);
|
|
611
|
+
}
|
|
612
|
+
if (requestedDelivery === "direct" && parsed.options.has("--require-integration")) {
|
|
613
|
+
throw usageError("--delivery direct conflicts with --require-integration.", usage);
|
|
614
|
+
}
|
|
513
615
|
const bases = new Map();
|
|
514
616
|
for (const option of baseOptions) {
|
|
515
617
|
const separator = option.indexOf("=");
|
|
@@ -540,7 +642,8 @@ function parseTaskCreation(args, store) {
|
|
|
540
642
|
baseRef: bases.get(project.id) ?? project.developmentBranch
|
|
541
643
|
})),
|
|
542
644
|
defaultProjectIds,
|
|
543
|
-
requireIntegration:
|
|
645
|
+
requireIntegration: requestedDelivery === "integrated"
|
|
646
|
+
|| parsed.options.has("--require-integration")
|
|
544
647
|
};
|
|
545
648
|
}
|
|
546
649
|
function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []) {
|
|
@@ -550,6 +653,7 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
|
|
|
550
653
|
store.saveRole(task.id, leader);
|
|
551
654
|
recordTaskEvent(store, task.id, "task.created", {
|
|
552
655
|
status: task.status,
|
|
656
|
+
deliveryPath: taskDeliveryPath(task),
|
|
553
657
|
...(defaultProjectIds.length === 0
|
|
554
658
|
? {}
|
|
555
659
|
: { defaultProjectIds: defaultProjectIds.join(",") })
|
|
@@ -598,11 +702,16 @@ function showTaskCommand(args, store) {
|
|
|
598
702
|
`Task: ${task.id}`,
|
|
599
703
|
`Title: ${task.title}`,
|
|
600
704
|
`Status: ${task.status}`,
|
|
705
|
+
`Delivery: ${taskDeliveryPath(task)}`,
|
|
601
706
|
...(task.description === undefined ? [] : [`Description: ${task.description}`]),
|
|
602
707
|
...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
|
|
603
708
|
...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
|
|
604
709
|
...(task.dueAt === undefined ? [] : [`Due: ${presentTime(task.dueAt, timeZone)}`]),
|
|
605
|
-
`Completion evidence: ${task.requireIntegration === true
|
|
710
|
+
`Completion evidence: ${task.requireIntegration === true
|
|
711
|
+
? "WorkItem, ChangeSet, and committed Integration required"
|
|
712
|
+
: task.projectBindings.length > 0
|
|
713
|
+
? "clean committed Task main required"
|
|
714
|
+
: "no Project evidence required"}`,
|
|
606
715
|
...(task.completedAt === undefined ? [] : [`Completed: ${presentTime(task.completedAt, timeZone)}`]),
|
|
607
716
|
...(task.completedBy === undefined ? [] : [`Completed by: ${task.completedBy}`]),
|
|
608
717
|
...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
|
|
@@ -631,7 +740,12 @@ function showTaskCommand(args, store) {
|
|
|
631
740
|
`Created: ${presentTime(task.createdAt, timeZone)}`,
|
|
632
741
|
`Updated: ${presentTime(task.updatedAt, timeZone)}`
|
|
633
742
|
].join("\n").concat("\n");
|
|
634
|
-
return output(rendered, {
|
|
743
|
+
return output(rendered, {
|
|
744
|
+
task,
|
|
745
|
+
deliveryPath: taskDeliveryPath(task),
|
|
746
|
+
counts,
|
|
747
|
+
hasBrief: brief !== null
|
|
748
|
+
});
|
|
635
749
|
}
|
|
636
750
|
function activateTaskCommand(args, store, options) {
|
|
637
751
|
exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
|
|
@@ -677,14 +791,18 @@ function completeTaskCommand(args, store, options) {
|
|
|
677
791
|
task,
|
|
678
792
|
changed: false,
|
|
679
793
|
runtimeCleanupTargets: [],
|
|
794
|
+
completionAdvisories: [],
|
|
680
795
|
finalReview: undefined,
|
|
681
796
|
publishedTreeAuthorization: undefined
|
|
682
797
|
};
|
|
683
798
|
}
|
|
684
799
|
const taskFinalContract = preflight.taskFinalReviewContract;
|
|
800
|
+
const actualTaskCandidate = task.projectBindings.length === 0
|
|
801
|
+
? undefined
|
|
802
|
+
: actualTaskReviewCandidateForMutation(tx, task, options);
|
|
685
803
|
const publishedTreeProof = request.acceptedPublishedTreePublicationId === undefined
|
|
686
804
|
? undefined
|
|
687
|
-
: assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof,
|
|
805
|
+
: assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskCandidate);
|
|
688
806
|
const requiresContractHandoff = publishedTreeProof !== undefined
|
|
689
807
|
&& taskFinalContract !== undefined;
|
|
690
808
|
if (requiresContractHandoff && actor !== "leader") {
|
|
@@ -695,6 +813,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
695
813
|
task,
|
|
696
814
|
changed: false,
|
|
697
815
|
runtimeCleanupTargets: [],
|
|
816
|
+
completionAdvisories: [],
|
|
698
817
|
finalReview: undefined,
|
|
699
818
|
publishedTreeAuthorization: {
|
|
700
819
|
event,
|
|
@@ -752,6 +871,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
752
871
|
task,
|
|
753
872
|
changed: false,
|
|
754
873
|
runtimeCleanupTargets: [],
|
|
874
|
+
completionAdvisories: [],
|
|
755
875
|
finalReview,
|
|
756
876
|
resumedPendingFinalReview: pendingFinalReviewIds.has(finalReview.id),
|
|
757
877
|
terminalizedLeaderRun,
|
|
@@ -783,13 +903,29 @@ function completeTaskCommand(args, store, options) {
|
|
|
783
903
|
by: actor,
|
|
784
904
|
projectId: publishedTreeProof.projectId,
|
|
785
905
|
publicationId: publishedTreeProof.publicationId,
|
|
786
|
-
|
|
906
|
+
...(publishedTreeProof.reviewRoundId === undefined
|
|
907
|
+
? {}
|
|
908
|
+
: { reviewRoundId: publishedTreeProof.reviewRoundId }),
|
|
787
909
|
localCommit: publishedTreeProof.localCommit,
|
|
788
910
|
remoteCommit: publishedTreeProof.remoteCommit,
|
|
789
911
|
tree: publishedTreeProof.tree
|
|
790
912
|
}, now);
|
|
791
913
|
}
|
|
792
|
-
recordTaskEvent(tx, task.id, "task.completed", {
|
|
914
|
+
recordTaskEvent(tx, task.id, "task.completed", {
|
|
915
|
+
by: actor,
|
|
916
|
+
summary,
|
|
917
|
+
deliveryPath: taskDeliveryPath(task),
|
|
918
|
+
...(actualTaskCandidate === undefined
|
|
919
|
+
? {}
|
|
920
|
+
: {
|
|
921
|
+
projectHeads: actualTaskCandidate.projects
|
|
922
|
+
.map(({ projectId, commit }) => `${projectId}@${commit}`)
|
|
923
|
+
.join(",")
|
|
924
|
+
}),
|
|
925
|
+
...(readiness.advisories.length === 0
|
|
926
|
+
? {}
|
|
927
|
+
: { cleanupAdvisories: String(readiness.advisories.length) })
|
|
928
|
+
}, now);
|
|
793
929
|
// A terminal Task must never leave a Task-lane signal that can wake it.
|
|
794
930
|
// The durable records remain intact; only the derived mailbox work is
|
|
795
931
|
// discarded at this lifecycle boundary.
|
|
@@ -805,6 +941,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
805
941
|
task: completed,
|
|
806
942
|
changed: true,
|
|
807
943
|
runtimeCleanupTargets,
|
|
944
|
+
completionAdvisories: readiness.advisories,
|
|
808
945
|
finalReview: undefined,
|
|
809
946
|
resumedPendingFinalReview: false,
|
|
810
947
|
terminalizedLeaderRun,
|
|
@@ -841,9 +978,17 @@ function completeTaskCommand(args, store, options) {
|
|
|
841
978
|
[TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW]: result.terminalizedLeaderRun
|
|
842
979
|
});
|
|
843
980
|
}
|
|
844
|
-
|
|
981
|
+
const completionOutput = result.changed
|
|
845
982
|
? `Completed task ${result.task.id}\n`
|
|
846
|
-
: `Task ${result.task.id} is already completed\n
|
|
983
|
+
: `Task ${result.task.id} is already completed\n`;
|
|
984
|
+
const advisoryOutput = result.completionAdvisories.length === 0
|
|
985
|
+
? ""
|
|
986
|
+
: `Cleanup advisories (non-blocking; settle before archive):\n`
|
|
987
|
+
+ result.completionAdvisories.map((advisory) => (`- ${advisory.code} (${advisory.ref.kind} ${advisory.ref.id}): ${advisory.fix}`)).join("\n").concat("\n");
|
|
988
|
+
return output(completionOutput + advisoryOutput, {
|
|
989
|
+
task: result.task,
|
|
990
|
+
completionAdvisories: result.completionAdvisories
|
|
991
|
+
});
|
|
847
992
|
}
|
|
848
993
|
function reopenTaskCommand(args, store, options) {
|
|
849
994
|
exactPositionals(args, 1, "Task reopen usage: yui task reopen <id>.");
|
|
@@ -2678,6 +2823,8 @@ function reviewWork(args, store, options) {
|
|
|
2678
2823
|
*/
|
|
2679
2824
|
function taskReviewCommand(args, store, options) {
|
|
2680
2825
|
const [command, ...rest] = args;
|
|
2826
|
+
if (command === "rebind")
|
|
2827
|
+
return rebindTaskFinalReviewContract(rest, store, options);
|
|
2681
2828
|
if (command === "request")
|
|
2682
2829
|
return requestTaskReviewRound(rest, store, options);
|
|
2683
2830
|
if (command === "force-fresh")
|
|
@@ -2692,6 +2839,115 @@ function taskReviewCommand(args, store, options) {
|
|
|
2692
2839
|
? "Task review command is required."
|
|
2693
2840
|
: `Unknown command: task review ${command}`);
|
|
2694
2841
|
}
|
|
2842
|
+
export function parseTaskFinalReviewContractRebindRequest(args) {
|
|
2843
|
+
const usage = "Task review rebind usage: yui task review rebind <task> "
|
|
2844
|
+
+ "--from-control <digest> --to-control <digest> "
|
|
2845
|
+
+ "--from-release <release-id> --to-release <release-id>.";
|
|
2846
|
+
const parsed = parseTail(args, new Set(["--from-control", "--to-control", "--from-release", "--to-release"]), usage);
|
|
2847
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
2848
|
+
return {
|
|
2849
|
+
taskId: parsed.positionals[0],
|
|
2850
|
+
fromControlPlaneDigest: requiredOption(parsed.options, "--from-control"),
|
|
2851
|
+
toControlPlaneDigest: requiredOption(parsed.options, "--to-control"),
|
|
2852
|
+
fromReleaseId: requiredOption(parsed.options, "--from-release"),
|
|
2853
|
+
toReleaseId: requiredOption(parsed.options, "--to-release")
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
function rebindTaskFinalReviewContract(args, store, options) {
|
|
2857
|
+
const request = parseTaskFinalReviewContractRebindRequest(args);
|
|
2858
|
+
const environment = options.environment ?? {};
|
|
2859
|
+
const usage = "Task review rebind usage: yui task review rebind <task> "
|
|
2860
|
+
+ "--from-control <digest> --to-control <digest> "
|
|
2861
|
+
+ "--from-release <release-id> --to-release <release-id>.";
|
|
2862
|
+
if (!isCurrentGlobalOperator(store, environment)) {
|
|
2863
|
+
throw usageError("Task-final Review contract rebind requires the authenticated global Operator session.", usage);
|
|
2864
|
+
}
|
|
2865
|
+
const proof = options.taskFinalReviewRebindProof;
|
|
2866
|
+
if (proof === undefined
|
|
2867
|
+
|| proof.schemaVersion !== 1
|
|
2868
|
+
|| proof.taskId !== request.taskId
|
|
2869
|
+
|| proof.fromControlPlaneDigest !== request.fromControlPlaneDigest
|
|
2870
|
+
|| proof.toControlPlaneDigest !== request.toControlPlaneDigest
|
|
2871
|
+
|| proof.fromRelease.releaseId !== request.fromReleaseId
|
|
2872
|
+
|| proof.toRelease.releaseId !== request.toReleaseId) {
|
|
2873
|
+
throw usageError("Task-final Review contract rebind proof is missing or does not match the explicit request.", usage);
|
|
2874
|
+
}
|
|
2875
|
+
const now = clock(options);
|
|
2876
|
+
const result = store.transaction((tx) => {
|
|
2877
|
+
const task = requireTask(tx, request.taskId);
|
|
2878
|
+
if (task.status !== "active") {
|
|
2879
|
+
throw usageError(`Task is not active: ${task.id}.`);
|
|
2880
|
+
}
|
|
2881
|
+
if (task.projectBindings.length === 0) {
|
|
2882
|
+
throw usageError(`Task final-review contract requires a Project-backed Task: ${task.id}.`);
|
|
2883
|
+
}
|
|
2884
|
+
if (!isCurrentGlobalOperator(tx, environment)) {
|
|
2885
|
+
throw usageError("Task-final Review contract rebind Operator identity drifted before commit.");
|
|
2886
|
+
}
|
|
2887
|
+
const resolution = storedTaskFinalReviewContractResolution(tx, task.id);
|
|
2888
|
+
if (resolution === undefined) {
|
|
2889
|
+
throw usageError(`Task final-review contract is missing for ${task.id}.`);
|
|
2890
|
+
}
|
|
2891
|
+
const exactExisting = resolution.rebinds.at(-1);
|
|
2892
|
+
if (resolution.effective.controlPlaneDigest === request.toControlPlaneDigest) {
|
|
2893
|
+
if (exactExisting !== undefined
|
|
2894
|
+
&& exactExisting.fromContract.controlPlaneDigest === request.fromControlPlaneDigest
|
|
2895
|
+
&& exactExisting.toContract.controlPlaneDigest === request.toControlPlaneDigest
|
|
2896
|
+
&& exactExisting.fromRelease.releaseId === request.fromReleaseId
|
|
2897
|
+
&& exactExisting.toRelease.releaseId === request.toReleaseId
|
|
2898
|
+
&& exactExisting.handoverId === proof.handoverId) {
|
|
2899
|
+
return { rebind: exactExisting, changed: false };
|
|
2900
|
+
}
|
|
2901
|
+
throw usageError(`Task final-review contract already targets ${request.toControlPlaneDigest} without the requested proof tuple.`);
|
|
2902
|
+
}
|
|
2903
|
+
if (resolution.effective.controlPlaneDigest !== request.fromControlPlaneDigest) {
|
|
2904
|
+
throw usageError(`Task final-review contract source control-plane digest drifted for ${task.id}.`);
|
|
2905
|
+
}
|
|
2906
|
+
const activeRound = tx.listReviewRounds(task.id).find((round) => ((round.scope ?? "work-item") === "task"
|
|
2907
|
+
&& (round.status === "pending" || round.status === "running")));
|
|
2908
|
+
if (activeRound !== undefined) {
|
|
2909
|
+
throw usageError(`Task final-review contract cannot rebind while ReviewRound ${activeRound.id} is ${activeRound.status}.`);
|
|
2910
|
+
}
|
|
2911
|
+
const activeLeaderRun = tx.getActiveAgentRun(task.id, "leader");
|
|
2912
|
+
if (activeLeaderRun !== null) {
|
|
2913
|
+
throw usageError(`Task final-review contract cannot rebind while Leader Run ${activeLeaderRun.id} is active.`);
|
|
2914
|
+
}
|
|
2915
|
+
const leaderSessions = tx.getTaskRoleSessionSet(task.id, "leader");
|
|
2916
|
+
if (leaderSessions !== null) {
|
|
2917
|
+
if (leaderSessions.inFlight !== null) {
|
|
2918
|
+
throw usageError("Task final-review contract cannot rebind while the Leader runtime has unsettled Run state.");
|
|
2919
|
+
}
|
|
2920
|
+
const liveLeaderSession = Object.values(leaderSessions.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
|
|
2921
|
+
if (liveLeaderSession !== undefined) {
|
|
2922
|
+
throw usageError(`Task final-review contract cannot rebind while Leader Session ${liveLeaderSession.agentId} is ${liveLeaderSession.status}.`);
|
|
2923
|
+
}
|
|
2924
|
+
if (Object.keys(leaderSessions.sessions).length > 0
|
|
2925
|
+
|| leaderSessions.providerBinding !== null) {
|
|
2926
|
+
tx.saveTaskRoleSessionSet(retireTaskRoleSessionsForWorkspace(leaderSessions, now));
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
const rebind = createTaskFinalReviewContractRebind({
|
|
2930
|
+
taskId: task.id,
|
|
2931
|
+
reviewerRoleName: resolution.effective.reviewerRoleName,
|
|
2932
|
+
fromContract: resolution.effective,
|
|
2933
|
+
toControlPlaneDigest: proof.toControlPlaneDigest,
|
|
2934
|
+
fromRelease: proof.fromRelease,
|
|
2935
|
+
toRelease: proof.toRelease,
|
|
2936
|
+
handoverId: proof.handoverId,
|
|
2937
|
+
authorizedBy: `operator:${environment.YUI_AGENT_ID ?? "unknown"}`
|
|
2938
|
+
});
|
|
2939
|
+
const event = recordTaskEventRecord(tx, task.id, TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, taskFinalReviewContractRebindPayload(rebind), now);
|
|
2940
|
+
enqueueWork(tx, leaderMailbox(task.id), wakeReason("review-contract-rebound", event.id), now, [eventRef(task.id, event.id)]);
|
|
2941
|
+
return { rebind, event, changed: true };
|
|
2942
|
+
});
|
|
2943
|
+
if (result.changed) {
|
|
2944
|
+
notifyMailbox(options.runtime, leaderMailbox(request.taskId), request.taskId);
|
|
2945
|
+
}
|
|
2946
|
+
return output(result.changed
|
|
2947
|
+
? `Rebound Task-final Review contract for ${request.taskId} from `
|
|
2948
|
+
+ `${request.fromControlPlaneDigest} to ${request.toControlPlaneDigest}.\n`
|
|
2949
|
+
: `Task-final Review contract rebind is already recorded for ${request.taskId}.\n`, result);
|
|
2950
|
+
}
|
|
2695
2951
|
function resolveReviewExecutionGroup(args, store, options) {
|
|
2696
2952
|
const usage = "Task review group resolve usage: yui task review group resolve <task>/<review-round> --decision <accept|reject|blocked> --summary <text> [--lane <lane-id> ...].";
|
|
2697
2953
|
if (args[0] !== "resolve")
|
|
@@ -2786,8 +3042,8 @@ function resolveReviewExecutionGroup(args, store, options) {
|
|
|
2786
3042
|
/**
|
|
2787
3043
|
* Issue 06: `yui task review finding` — the cross-Round finding ledger CLI.
|
|
2788
3044
|
* Findings are extracted automatically from completed Rounds; these commands
|
|
2789
|
-
* let the Leader inspect the ledger, disposition each finding, and plan
|
|
2790
|
-
*
|
|
3045
|
+
* let the Leader inspect the ledger, disposition each finding, and plan one
|
|
3046
|
+
* convergent repair unit by default. Parallel fan-out is explicit.
|
|
2791
3047
|
*/
|
|
2792
3048
|
function reviewFindingCommand(args, store, options) {
|
|
2793
3049
|
const [command, ...rest] = args;
|
|
@@ -2858,11 +3114,15 @@ function disposeReviewFindingCommand(args, store, options) {
|
|
|
2858
3114
|
return output(`Dispositioned ${result.id} as ${result.disposition}.\n`);
|
|
2859
3115
|
}
|
|
2860
3116
|
function planReviewRepairWave(args, store, options) {
|
|
2861
|
-
const usage = "Task review finding repair-wave usage: yui task review finding repair-wave <task> [--create].";
|
|
2862
|
-
const parsed = parseTail(args, new Set(), usage, new Set(["--create"]));
|
|
3117
|
+
const usage = "Task review finding repair-wave usage: yui task review finding repair-wave <task> [--strategy <consolidated|parallel>] [--create].";
|
|
3118
|
+
const parsed = parseTail(args, new Set(["--strategy"]), usage, new Set(["--create"]));
|
|
2863
3119
|
exactPositionals(parsed.positionals, 1, usage);
|
|
2864
3120
|
const task = requireTask(store, parsed.positionals[0]);
|
|
2865
|
-
const
|
|
3121
|
+
const strategy = parsed.options.get("--strategy") ?? "consolidated";
|
|
3122
|
+
if (strategy !== "consolidated" && strategy !== "parallel") {
|
|
3123
|
+
throw usageError(`Review repair strategy is invalid: ${strategy}.`, usage);
|
|
3124
|
+
}
|
|
3125
|
+
const groups = repairGroupsForStrategy(planRepairGroups(store, task.id), strategy);
|
|
2866
3126
|
if (groups.length === 0) {
|
|
2867
3127
|
return output(`No open P1/P2 findings need repair for ${task.id}.\n`);
|
|
2868
3128
|
}
|
|
@@ -2903,7 +3163,7 @@ function planReviewRepairWave(args, store, options) {
|
|
|
2903
3163
|
});
|
|
2904
3164
|
const lines = created.map(({ group, item, changed }) => (`wave ${group.groupKey}: ${item.id} ${changed ? "created" : "already open"} `
|
|
2905
3165
|
+ `(${group.findingIds.join(", ")})`));
|
|
2906
|
-
return output(`Review repair wave for ${task.id} (${groups.length} group(s)):\n${lines.join("\n")}\n`, { groups: created });
|
|
3166
|
+
return output(`Review repair wave for ${task.id} (${strategy}, ${groups.length} group(s)):\n${lines.join("\n")}\n`, { strategy, groups: created });
|
|
2907
3167
|
}
|
|
2908
3168
|
const lines = groups.map((group, index) => {
|
|
2909
3169
|
const findings = group.findings
|
|
@@ -2912,7 +3172,24 @@ function planReviewRepairWave(args, store, options) {
|
|
|
2912
3172
|
return `wave ${index + 1}: ${findings}`
|
|
2913
3173
|
+ ` (paths: ${group.affectedPaths.join(", ") || "none"}; invariants: ${group.invariants.join(", ")})`;
|
|
2914
3174
|
});
|
|
2915
|
-
return output(`Repair wave for ${task.id} (${groups.length} group(s)
|
|
3175
|
+
return output(`Repair wave for ${task.id} (${strategy}, ${groups.length} group(s)):\n${lines.join("\n")}\n`
|
|
3176
|
+
+ (strategy === "consolidated"
|
|
3177
|
+
? "Default: keep all findings in one WorkItem; use --strategy parallel only for proven independent ownership.\n"
|
|
3178
|
+
: "Parallel strategy explicitly selected; each disjoint group may become one WorkItem.\n"));
|
|
3179
|
+
}
|
|
3180
|
+
function repairGroupsForStrategy(groups, strategy) {
|
|
3181
|
+
if (strategy === "parallel" || groups.length <= 1)
|
|
3182
|
+
return groups;
|
|
3183
|
+
const findings = groups.flatMap(({ findings }) => findings)
|
|
3184
|
+
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
|
|
3185
|
+
return [{
|
|
3186
|
+
groupKey: findings.map(({ id }) => id).join("+"),
|
|
3187
|
+
findings,
|
|
3188
|
+
findingIds: findings.map(({ id }) => id),
|
|
3189
|
+
affectedPaths: [...new Set(groups.flatMap(({ affectedPaths }) => affectedPaths))].sort(),
|
|
3190
|
+
affectedSymbols: [...new Set(groups.flatMap(({ affectedSymbols }) => affectedSymbols))].sort(),
|
|
3191
|
+
invariants: [...new Set(groups.flatMap(({ invariants }) => invariants))].sort()
|
|
3192
|
+
}];
|
|
2916
3193
|
}
|
|
2917
3194
|
function reviewRepairProjectIds(task, affectedPaths) {
|
|
2918
3195
|
const bindings = task.projectBindings;
|
|
@@ -3155,9 +3432,9 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3155
3432
|
const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
|
|
3156
3433
|
/**
|
|
3157
3434
|
* Creates a distinct full Task-final ReviewRound only when the exact previous
|
|
3158
|
-
* Round
|
|
3159
|
-
* Run, findings, workspace, and terminal report remain immutable
|
|
3160
|
-
* linking Event is both the audit record and the idempotence key.
|
|
3435
|
+
* terminal Round durably proves that no semantic review was produced. The
|
|
3436
|
+
* source Round, Run, findings, workspace, and terminal report remain immutable
|
|
3437
|
+
* history; the linking Event is both the audit record and the idempotence key.
|
|
3161
3438
|
*/
|
|
3162
3439
|
function forceFreshTaskReviewRound(args, store, options) {
|
|
3163
3440
|
const usage = "Task review force-fresh usage: yui task review force-fresh <task>/<review-round>.";
|
|
@@ -3206,9 +3483,9 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3206
3483
|
}
|
|
3207
3484
|
return { round: replacement, source, created: false };
|
|
3208
3485
|
}
|
|
3209
|
-
const
|
|
3210
|
-
if (
|
|
3211
|
-
throw usageError(`ReviewRound ${source.id} is not eligible for force-fresh: ${
|
|
3486
|
+
const recovery = classifyForceFreshReviewRecovery(tx, source);
|
|
3487
|
+
if (recovery.kind === "semantic-or-ambiguous") {
|
|
3488
|
+
throw usageError(`ReviewRound ${source.id} is not eligible for force-fresh: ${recovery.reason}`);
|
|
3212
3489
|
}
|
|
3213
3490
|
if (source.taskCandidate === undefined) {
|
|
3214
3491
|
throw dataError(`ReviewRound ${source.id} has no frozen Task candidate.`);
|
|
@@ -3275,7 +3552,7 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3275
3552
|
candidateId: created.candidateId,
|
|
3276
3553
|
reviewerRoleName: created.reviewerRoleName,
|
|
3277
3554
|
taskCandidate: JSON.stringify(created.taskCandidate),
|
|
3278
|
-
reason: "source-round-
|
|
3555
|
+
reason: "source-round-terminal-without-semantic-review",
|
|
3279
3556
|
leaderActionRunId: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader"
|
|
3280
3557
|
}, now);
|
|
3281
3558
|
return { round: created, source, created: true };
|
|
@@ -3284,76 +3561,23 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3284
3561
|
? `Fresh Task-final Review requested as ${result.round.id} after ${result.source.id}\n`
|
|
3285
3562
|
: `Fresh Task-final Review already requested as ${result.round.id} after ${result.source.id}\n`, { reviewRound: result.round, sourceReviewRound: result.source });
|
|
3286
3563
|
}
|
|
3287
|
-
/**
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
}
|
|
3305
|
-
const semanticLane = round.executionGroup?.lanes.find((lane) => (lane.status === "yielded"
|
|
3306
|
-
|| lane.status === "completed"
|
|
3307
|
-
|| lane.result?.report !== undefined
|
|
3308
|
-
|| (lane.result?.checks ?? []).length > 0
|
|
3309
|
-
|| (lane.result?.findings ?? []).length > 0
|
|
3310
|
-
|| (lane.result?.evidence ?? []).length > 0
|
|
3311
|
-
|| lane.result?.evidenceCommit !== undefined));
|
|
3312
|
-
if (semanticLane !== undefined) {
|
|
3313
|
-
return `Reviewer Lane ${semanticLane.id} delivered semantic evidence.`;
|
|
3314
|
-
}
|
|
3315
|
-
const yieldedRun = store.listAgentRuns(round.taskId).find((run) => (run.purpose === "review"
|
|
3316
|
-
&& run.reviewRoundId === round.id
|
|
3317
|
-
&& run.status === "yielded"));
|
|
3318
|
-
if (yieldedRun !== undefined)
|
|
3319
|
-
return `Reviewer Run ${yieldedRun.id} yielded a report.`;
|
|
3320
|
-
const finding = store.listReviewFindings(round.taskId).find((entry) => (entry.firstReviewRoundId === round.id || entry.lastReviewRoundId === round.id));
|
|
3321
|
-
if (finding !== undefined)
|
|
3322
|
-
return `Review finding ${finding.id} references the Round.`;
|
|
3323
|
-
const semanticEvent = store.listEvents(round.taskId).find((event) => (event.type === "review.completed" && event.payload.reviewRoundId === round.id));
|
|
3324
|
-
if (semanticEvent !== undefined)
|
|
3325
|
-
return `Review completion Event ${semanticEvent.id} exists.`;
|
|
3326
|
-
if (looksLikeStructuredReviewReport(round.report ?? "")) {
|
|
3327
|
-
return "the Round stores a structured reviewer report.";
|
|
3328
|
-
}
|
|
3329
|
-
return null;
|
|
3330
|
-
}
|
|
3331
|
-
function runtimeFailureSummaryHasReviewerOutput(summary) {
|
|
3332
|
-
const match = /(?:^|\n)last_assistant_message:[ \t]*([\s\S]*)$/u.exec(summary);
|
|
3333
|
-
const output = match?.[1];
|
|
3334
|
-
return output !== undefined && output.trim().length > 0;
|
|
3335
|
-
}
|
|
3336
|
-
function looksLikeStructuredReviewReport(report) {
|
|
3337
|
-
let parsed;
|
|
3338
|
-
try {
|
|
3339
|
-
parsed = JSON.parse(report);
|
|
3340
|
-
}
|
|
3341
|
-
catch {
|
|
3342
|
-
return false;
|
|
3343
|
-
}
|
|
3344
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
3345
|
-
return false;
|
|
3346
|
-
const record = parsed;
|
|
3347
|
-
return [
|
|
3348
|
-
"summary",
|
|
3349
|
-
"report",
|
|
3350
|
-
"checks",
|
|
3351
|
-
"findings",
|
|
3352
|
-
"evidence",
|
|
3353
|
-
"evidenceCommit",
|
|
3354
|
-
"deltaDisposition",
|
|
3355
|
-
"deltaReasoning"
|
|
3356
|
-
].some((key) => Object.hasOwn(record, key));
|
|
3564
|
+
/**
|
|
3565
|
+
* Conservatively classifies an immutable Task-final Review as replaceable.
|
|
3566
|
+
* A failed Round keeps the existing no-semantic-evidence behavior. A completed
|
|
3567
|
+
* Round needs stronger, mutually corroborating evidence: an explicit internal
|
|
3568
|
+
* context/workspace failure, its exact yielded Run and receipt, matching Lane
|
|
3569
|
+
* output, and the mechanically emitted empty completion Event.
|
|
3570
|
+
* This is command eligibility only; it never rewrites the source outcome or
|
|
3571
|
+
* changes the global semantic classifier used by the finding ledger.
|
|
3572
|
+
*/
|
|
3573
|
+
export function classifyForceFreshReviewRecovery(store, round) {
|
|
3574
|
+
const classification = classifyReviewRoundOutcome(round, store);
|
|
3575
|
+
return classification?.kind === "non-semantic"
|
|
3576
|
+
? { kind: "non-semantic-terminal", reason: classification.reason }
|
|
3577
|
+
: {
|
|
3578
|
+
kind: "semantic-or-ambiguous",
|
|
3579
|
+
reason: classification?.reason ?? `source status is ${round.status}, not terminal.`
|
|
3580
|
+
};
|
|
3357
3581
|
}
|
|
3358
3582
|
/**
|
|
3359
3583
|
* Issue 07: re-validates the CLI-computed delta preflight inside the store
|
|
@@ -3369,8 +3593,8 @@ function validateDeltaRecheckRequest(store, taskId, reviewerRoleName, candidate,
|
|
|
3369
3593
|
const previous = store.getReviewRound(taskId, preflight.record.previousReviewRoundId);
|
|
3370
3594
|
if (previous === null
|
|
3371
3595
|
|| (previous.scope ?? "work-item") !== "task"
|
|
3372
|
-
|| previous
|
|
3373
|
-
throw usageError(`Delta-recheck previous ReviewRound is not a completed Task-final Review: `
|
|
3596
|
+
|| !isSemanticReviewRound(previous, store)) {
|
|
3597
|
+
throw usageError(`Delta-recheck previous ReviewRound is not a semantic completed Task-final Review: `
|
|
3374
3598
|
+ `${preflight.record.previousReviewRoundId}.`);
|
|
3375
3599
|
}
|
|
3376
3600
|
if (previous.reviewerRoleName !== reviewerRoleName) {
|
|
@@ -3986,7 +4210,7 @@ function retryRun(args, store, options) {
|
|
|
3986
4210
|
}
|
|
3987
4211
|
function actualTaskReviewCandidateForMutation(store, task, options) {
|
|
3988
4212
|
if (options.actualTaskReviewCandidate === undefined) {
|
|
3989
|
-
throw usageError(`Actual Task Project heads were not verified for
|
|
4213
|
+
throw usageError(`Actual Task Project heads were not verified for delivery: ${task.id}.`);
|
|
3990
4214
|
}
|
|
3991
4215
|
let actual;
|
|
3992
4216
|
try {
|
|
@@ -4202,8 +4426,8 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4202
4426
|
// Any Task-final ReviewRound is durable completion evidence/obligation.
|
|
4203
4427
|
// Once one exists, later changes to the mutable global review config cannot
|
|
4204
4428
|
// weaken the requirement or change its reviewer. Before the first such
|
|
4205
|
-
// Round, the current global `final` config
|
|
4206
|
-
//
|
|
4429
|
+
// Round, the current global `final` config establishes the initial Round
|
|
4430
|
+
// only for integrated delivery.
|
|
4207
4431
|
const taskRounds = reviewRoundsByIdentity(store.listReviewRounds(task.id))
|
|
4208
4432
|
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
4209
4433
|
&& (taskFinalContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract))));
|
|
@@ -4214,7 +4438,15 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4214
4438
|
}
|
|
4215
4439
|
else if (establishedRound === undefined) {
|
|
4216
4440
|
const globalConfig = store.getReviewConfig();
|
|
4217
|
-
|
|
4441
|
+
// Direct delivery is an explicit low-overhead contract. Mutable global
|
|
4442
|
+
// policy must not create a managed Round during direct completion; risk
|
|
4443
|
+
// that warrants one promotes the Task to integrated delivery. Any already-
|
|
4444
|
+
// established Task Round or immutable contract remains authoritative
|
|
4445
|
+
// through the branches above.
|
|
4446
|
+
config = taskDeliveryPath(task) === "integrated"
|
|
4447
|
+
&& globalConfig?.trigger === "final"
|
|
4448
|
+
? globalConfig
|
|
4449
|
+
: null;
|
|
4218
4450
|
}
|
|
4219
4451
|
else {
|
|
4220
4452
|
config = { roleName: establishedRound.reviewerRoleName, trigger: "final" };
|
|
@@ -4259,7 +4491,10 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4259
4491
|
return escalated;
|
|
4260
4492
|
}
|
|
4261
4493
|
else {
|
|
4262
|
-
return latest.status === "completed"
|
|
4494
|
+
return latest.status === "completed"
|
|
4495
|
+
&& classifyReviewRoundOutcome(latest, store)?.kind === "semantic"
|
|
4496
|
+
? null
|
|
4497
|
+
: latest;
|
|
4263
4498
|
}
|
|
4264
4499
|
}
|
|
4265
4500
|
const anchor = taskFinalContract === undefined
|
|
@@ -5380,8 +5615,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5380
5615
|
let deltaContext = "";
|
|
5381
5616
|
if (taskScope && round.deltaRecheck !== undefined) {
|
|
5382
5617
|
const previousRound = tx.getReviewRound(taskId, round.deltaRecheck.previousReviewRoundId);
|
|
5383
|
-
if (previousRound === null || previousRound
|
|
5384
|
-
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck previous ReviewRound is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
|
|
5618
|
+
if (previousRound === null || !isSemanticReviewRound(previousRound, tx)) {
|
|
5619
|
+
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck previous semantic ReviewRound is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
|
|
5385
5620
|
}
|
|
5386
5621
|
const diffByProject = options.deltaRecheckDiff;
|
|
5387
5622
|
if (diffByProject === undefined) {
|
|
@@ -5669,7 +5904,9 @@ function publishedTreeAuthorizationPayload(actor, proof) {
|
|
|
5669
5904
|
by: actor,
|
|
5670
5905
|
projectId: proof.projectId,
|
|
5671
5906
|
publicationId: proof.publicationId,
|
|
5672
|
-
|
|
5907
|
+
...(proof.reviewRoundId === undefined
|
|
5908
|
+
? { reviewAnchor: publishedTreeReviewAnchor(proof) }
|
|
5909
|
+
: { reviewRoundId: proof.reviewRoundId }),
|
|
5673
5910
|
localCommit: proof.localCommit,
|
|
5674
5911
|
remoteCommit: proof.remoteCommit,
|
|
5675
5912
|
tree: proof.tree
|
|
@@ -5685,7 +5922,8 @@ function matchingPublishedTreeAuthorization(store, proof) {
|
|
|
5685
5922
|
&& (event.payload.by === "user" || event.payload.by === "operator")
|
|
5686
5923
|
&& event.payload.projectId === proof.projectId
|
|
5687
5924
|
&& event.payload.publicationId === proof.publicationId
|
|
5688
|
-
&& event.payload.
|
|
5925
|
+
&& (event.payload.reviewAnchor ?? event.payload.reviewRoundId)
|
|
5926
|
+
=== publishedTreeReviewAnchor(proof)
|
|
5689
5927
|
&& event.payload.localCommit === proof.localCommit
|
|
5690
5928
|
&& event.payload.remoteCommit === proof.remoteCommit
|
|
5691
5929
|
&& event.payload.tree === proof.tree) {
|
|
@@ -5698,10 +5936,15 @@ function requirePublishedTreeAuthorization(store, proof) {
|
|
|
5698
5936
|
const authorization = matchingPublishedTreeAuthorization(store, proof);
|
|
5699
5937
|
if (authorization === undefined) {
|
|
5700
5938
|
throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
|
|
5701
|
-
+ `${proof.taskId}/${proof.publicationId} at
|
|
5939
|
+
+ `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
|
|
5940
|
+
? "the direct Task-main head"
|
|
5941
|
+
: `Task-final Review ${proof.reviewRoundId}`}.`);
|
|
5702
5942
|
}
|
|
5703
5943
|
return authorization;
|
|
5704
5944
|
}
|
|
5945
|
+
function publishedTreeReviewAnchor(proof) {
|
|
5946
|
+
return proof.reviewRoundId ?? "direct";
|
|
5947
|
+
}
|
|
5705
5948
|
/** Keeps a free-text run-fact note bounded so an event payload stays compact. */
|
|
5706
5949
|
function truncateEventNote(note) {
|
|
5707
5950
|
const normalized = note.trim();
|
|
@@ -5962,6 +6205,11 @@ function parseTaskPriority(value) {
|
|
|
5962
6205
|
return value;
|
|
5963
6206
|
throw usageError(`Invalid Task priority: ${value}.`);
|
|
5964
6207
|
}
|
|
6208
|
+
function parseTaskDelivery(value) {
|
|
6209
|
+
if (value === "direct" || value === "integrated")
|
|
6210
|
+
return value;
|
|
6211
|
+
throw usageError(`Task delivery is invalid: ${value}. Use direct or integrated.`);
|
|
6212
|
+
}
|
|
5965
6213
|
function parseTaskTags(value) {
|
|
5966
6214
|
const tags = [...new Set(value.split(",").map((tag) => tag.trim()).filter(Boolean))];
|
|
5967
6215
|
if (tags.length === 0)
|