@zq-silk/yui 0.8.3 → 0.8.7
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 +66 -19
- package/dist/cli/commandCatalog.js +43 -14
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli/updatePorts.js +6 -0
- package/dist/cli.js +252 -37
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/globalRoleCommands.js +8 -4
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +527 -147
- package/dist/commands/taskCompletionGate.js +36 -24
- package/dist/commands/taskContextCommand.js +11 -4
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +38 -3
- package/dist/commands/taskOverviewCommand.js +2 -1
- package/dist/commands/taskRoleRuntimeStatus.js +2 -1
- package/dist/context/runContextPack.js +9 -5
- package/dist/context/sessionBootstrapManifest.js +158 -11
- package/dist/context/wakeNotification.js +5 -3
- package/dist/controller/clientRuntime.js +15 -15
- package/dist/controller/controller.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +67 -7
- 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 +29 -9
- package/dist/executor/fileRoleLaunchPlanner.js +37 -45
- 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 +263 -54
- package/dist/review/taskFinalReviewContractEvent.js +1 -0
- package/dist/review/taskFinalReviewContractRebind.js +367 -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/exactControlPlane.js +47 -37
- package/dist/runtime/firstProgressStopLoss.js +54 -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/actionability.js +4 -2
- package/dist/scheduler/activeRoleRunDelivery.js +20 -18
- package/dist/scheduler/activeTaskProgress.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +33 -2
- package/dist/scheduler/taskExecutionProjection.js +13 -4
- package/dist/scheduler/wakeReason.js +1 -0
- package/dist/storage/sqliteStore.js +18 -3
- package/dist/storage/taskStore.js +14 -3
- package/dist/task/completionReadiness.js +48 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +153 -55
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/task/taskRecordRetirement.js +72 -0
- package/dist/web/webSnapshot.js +7 -1
- package/dist/workItem/workItem.js +6 -4
- package/i18n/README.zh-CN.md +48 -9
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +73 -31
- package/skills/yui-operator/SKILL.md +58 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
- package/skills/yui-runtime/SKILL.md +6 -6
|
@@ -5,10 +5,11 @@ import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds,
|
|
|
5
5
|
import { contextSnapshotRef } from "../context/contextSnapshot.js";
|
|
6
6
|
import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
|
|
7
7
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
8
|
+
import { createTaskRecordRetirement, isTaskRecordRetired, taskRecordRetirement } from "../task/taskRecordRetirement.js";
|
|
8
9
|
import { clearMatchingLeaderStallAttention, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
|
|
9
10
|
import { readCommandText } from "./textInput.js";
|
|
10
11
|
import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
|
|
11
|
-
import { createRoleSessionSet, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
12
|
+
import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
12
13
|
import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
|
|
13
14
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
14
15
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
@@ -35,7 +36,7 @@ import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coord
|
|
|
35
36
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
36
37
|
import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
37
38
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
38
|
-
import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
39
|
+
import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, taskDeliveryPath, updateTaskMetadata } from "../task/task.js";
|
|
39
40
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
40
41
|
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
41
42
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
@@ -44,6 +45,8 @@ import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenance
|
|
|
44
45
|
import { currentWorkItemCandidate, governingWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
45
46
|
import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
|
|
46
47
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
48
|
+
import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
49
|
+
import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, createTaskFinalReviewContractRebind, resolveRecordedTaskFinalReviewContract, taskFinalReviewContractRebindPayload } from "../review/taskFinalReviewContractRebind.js";
|
|
47
50
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
48
51
|
import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
|
|
49
52
|
import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
|
|
@@ -52,7 +55,7 @@ import { runTaskContextCommand } from "./taskContextCommand.js";
|
|
|
52
55
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
53
56
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
54
57
|
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
55
|
-
import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
58
|
+
import { assertNoOpenInputRequests, isCurrentGlobalOperator, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
56
59
|
import { runGrantCommand } from "./grantCommands.js";
|
|
57
60
|
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
58
61
|
import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
|
|
@@ -81,21 +84,17 @@ function legacyWorkItemReviewConfig(config) {
|
|
|
81
84
|
return config?.trigger === "final" ? null : config;
|
|
82
85
|
}
|
|
83
86
|
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.`);
|
|
87
|
+
return storedTaskFinalReviewContractResolution(store, taskId)?.effective;
|
|
88
|
+
}
|
|
89
|
+
function storedTaskFinalReviewContractResolution(store, taskId) {
|
|
90
|
+
try {
|
|
91
|
+
return resolveRecordedTaskFinalReviewContract(taskId, store.listWorkItems(taskId), store.listReviewRounds(taskId), store.listEvents(taskId));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw dataError(error instanceof Error
|
|
95
|
+
? error.message
|
|
96
|
+
: `Task ${taskId} contains conflicting final-review contracts.`);
|
|
97
97
|
}
|
|
98
|
-
return first;
|
|
99
98
|
}
|
|
100
99
|
/**
|
|
101
100
|
* Resolve one exact Task-local contract before the caller performs any write.
|
|
@@ -367,12 +366,14 @@ function taskProjectCommand(args, store, options) {
|
|
|
367
366
|
return output(`Added Project to ${updated.id}\n`, { task: updated });
|
|
368
367
|
}
|
|
369
368
|
function updateTaskCommand(args, store, options) {
|
|
370
|
-
const optionNames = new Set([
|
|
369
|
+
const optionNames = new Set([
|
|
370
|
+
"--title", "--description", "--priority", "--tags", "--due-at", "--delivery"
|
|
371
|
+
]);
|
|
371
372
|
const flagOptions = new Set([
|
|
372
373
|
"--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at",
|
|
373
374
|
"--require-integration"
|
|
374
375
|
]);
|
|
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].";
|
|
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] [--delivery <direct|integrated>] [--require-integration].";
|
|
376
377
|
const parsed = parseTail(args, optionNames, usage, flagOptions);
|
|
377
378
|
exactPositionals(parsed.positionals, 1, usage);
|
|
378
379
|
if (parsed.options.size === 0)
|
|
@@ -396,19 +397,42 @@ function updateTaskCommand(args, store, options) {
|
|
|
396
397
|
const tags = parsed.options.has("--tags")
|
|
397
398
|
? parseTaskTags(requiredOption(parsed.options, "--tags"))
|
|
398
399
|
: 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");
|
|
399
408
|
const now = clock(options);
|
|
400
409
|
const result = store.transaction((tx) => {
|
|
401
410
|
const current = requireTask(tx, parsed.positionals[0]);
|
|
402
411
|
if (current.status === "archived")
|
|
403
412
|
throw usageError(`Task is archived: ${current.id}.`);
|
|
404
|
-
if (parsed.options.has("--require-integration")
|
|
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") {
|
|
405
421
|
throw usageError(`Task ${current.id} is completed; use task reopen before enabling integration evidence.`);
|
|
406
422
|
}
|
|
423
|
+
if (enableIntegration && current.requireIntegration !== true) {
|
|
424
|
+
assertTaskDeliveryPromotionEligible(tx, current, options.directTaskMainSnapshot);
|
|
425
|
+
}
|
|
407
426
|
if (parsed.options.size === 1
|
|
408
|
-
&&
|
|
427
|
+
&& enableIntegration
|
|
409
428
|
&& current.requireIntegration === true) {
|
|
410
429
|
return { task: current, integrationState: "already-enabled" };
|
|
411
430
|
}
|
|
431
|
+
if (parsed.options.size === 1
|
|
432
|
+
&& requestedDelivery === "direct"
|
|
433
|
+
&& taskDeliveryPath(current) === "direct") {
|
|
434
|
+
return { task: current, integrationState: "already-direct" };
|
|
435
|
+
}
|
|
412
436
|
const updated = updateTaskMetadata(current, {
|
|
413
437
|
...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
|
|
414
438
|
...(parsed.options.has("--description")
|
|
@@ -423,31 +447,93 @@ function updateTaskCommand(args, store, options) {
|
|
|
423
447
|
...(dueAt === undefined
|
|
424
448
|
? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
|
|
425
449
|
: { dueAt }),
|
|
426
|
-
...(
|
|
450
|
+
...(enableIntegration ? { requireIntegration: true } : {})
|
|
427
451
|
}, now);
|
|
428
452
|
tx.saveTask(updated);
|
|
429
453
|
recordTaskEvent(tx, updated.id, "task.updated", {
|
|
430
454
|
status: updated.status,
|
|
431
|
-
...(parsed.options.has("--require-integration")
|
|
432
|
-
? {
|
|
433
|
-
: {
|
|
455
|
+
...(requestedDelivery === undefined && !parsed.options.has("--require-integration")
|
|
456
|
+
? {}
|
|
457
|
+
: {
|
|
458
|
+
completionEvidence: enableIntegration
|
|
459
|
+
? "integration-required"
|
|
460
|
+
: "direct",
|
|
461
|
+
deliveryPath: taskDeliveryPath(updated)
|
|
462
|
+
})
|
|
434
463
|
}, now);
|
|
435
464
|
enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
|
|
436
465
|
return {
|
|
437
466
|
task: updated,
|
|
438
|
-
integrationState:
|
|
467
|
+
integrationState: enableIntegration
|
|
439
468
|
? "enabled"
|
|
440
|
-
: "
|
|
469
|
+
: requestedDelivery === "direct"
|
|
470
|
+
? "direct"
|
|
471
|
+
: "unchanged"
|
|
441
472
|
};
|
|
442
473
|
});
|
|
443
|
-
if (result.integrationState !== "already-enabled"
|
|
474
|
+
if (result.integrationState !== "already-enabled"
|
|
475
|
+
&& result.integrationState !== "already-direct") {
|
|
444
476
|
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
445
477
|
}
|
|
446
478
|
return result.integrationState === "enabled"
|
|
447
|
-
? `Updated task ${result.task.id}\
|
|
479
|
+
? `Updated task ${result.task.id}\nDelivery: integrated (WorkItem, ChangeSet, and committed Integration required)\n`
|
|
448
480
|
: result.integrationState === "already-enabled"
|
|
449
|
-
? `Task ${result.task.id}
|
|
450
|
-
:
|
|
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
|
+
}
|
|
451
537
|
}
|
|
452
538
|
/** Compatibility helper for call sites that cannot yet handle foreground enter. */
|
|
453
539
|
export function runTaskOutputCommand(args, store, options = {}) {
|
|
@@ -488,13 +574,20 @@ function createTaskCommand(args, store, options) {
|
|
|
488
574
|
notifyMailbox(options.runtime, taskMailbox(created.task.id), created.task.id);
|
|
489
575
|
return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
|
|
490
576
|
+ `Assigned role: ${created.leader.name}\n`
|
|
577
|
+
+ `Delivery: ${taskDeliveryPath(created.task)}\n`
|
|
491
578
|
+ (created.task.requireIntegration
|
|
492
579
|
? "Completion: WorkItem, ChangeSet, and committed Integration required\n"
|
|
493
|
-
:
|
|
580
|
+
: created.task.projectBindings.length > 0
|
|
581
|
+
? "Completion: clean committed Task main required; no WorkItem, ChangeSet, IntegrationAttempt, or managed ReviewRound required\n"
|
|
582
|
+
: "Completion: no Project delivery evidence required\n"), {
|
|
583
|
+
task: created.task,
|
|
584
|
+
leader: created.leader,
|
|
585
|
+
deliveryPath: taskDeliveryPath(created.task)
|
|
586
|
+
});
|
|
494
587
|
}
|
|
495
588
|
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"]));
|
|
589
|
+
const usage = "Task create usage: yui task create <title> [--project <project> ...] [--base <project>=<ref> ...] [--delivery <direct|integrated>] [--require-integration].";
|
|
590
|
+
const parsed = parseMultiValueTail(args, new Set(["--delivery"]), new Set(["--project", "--base"]), usage, new Set(["--require-integration"]));
|
|
498
591
|
exactPositionals(parsed.positionals, 1, usage);
|
|
499
592
|
const projectReferences = parsed.multiOptions.get("--project") ?? [];
|
|
500
593
|
const baseOptions = parsed.multiOptions.get("--base") ?? [];
|
|
@@ -510,6 +603,16 @@ function parseTaskCreation(args, store) {
|
|
|
510
603
|
if (new Set(projects.map(({ id }) => id)).size !== projects.length) {
|
|
511
604
|
throw usageError("A Task cannot bind the same Project more than once.");
|
|
512
605
|
}
|
|
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
|
+
}
|
|
513
616
|
const bases = new Map();
|
|
514
617
|
for (const option of baseOptions) {
|
|
515
618
|
const separator = option.indexOf("=");
|
|
@@ -540,7 +643,8 @@ function parseTaskCreation(args, store) {
|
|
|
540
643
|
baseRef: bases.get(project.id) ?? project.developmentBranch
|
|
541
644
|
})),
|
|
542
645
|
defaultProjectIds,
|
|
543
|
-
requireIntegration:
|
|
646
|
+
requireIntegration: requestedDelivery === "integrated"
|
|
647
|
+
|| parsed.options.has("--require-integration")
|
|
544
648
|
};
|
|
545
649
|
}
|
|
546
650
|
function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []) {
|
|
@@ -550,6 +654,7 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
|
|
|
550
654
|
store.saveRole(task.id, leader);
|
|
551
655
|
recordTaskEvent(store, task.id, "task.created", {
|
|
552
656
|
status: task.status,
|
|
657
|
+
deliveryPath: taskDeliveryPath(task),
|
|
553
658
|
...(defaultProjectIds.length === 0
|
|
554
659
|
? {}
|
|
555
660
|
: { defaultProjectIds: defaultProjectIds.join(",") })
|
|
@@ -598,11 +703,16 @@ function showTaskCommand(args, store) {
|
|
|
598
703
|
`Task: ${task.id}`,
|
|
599
704
|
`Title: ${task.title}`,
|
|
600
705
|
`Status: ${task.status}`,
|
|
706
|
+
`Delivery: ${taskDeliveryPath(task)}`,
|
|
601
707
|
...(task.description === undefined ? [] : [`Description: ${task.description}`]),
|
|
602
708
|
...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
|
|
603
709
|
...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
|
|
604
710
|
...(task.dueAt === undefined ? [] : [`Due: ${presentTime(task.dueAt, timeZone)}`]),
|
|
605
|
-
`Completion evidence: ${task.requireIntegration === true
|
|
711
|
+
`Completion evidence: ${task.requireIntegration === true
|
|
712
|
+
? "WorkItem, ChangeSet, and committed Integration required"
|
|
713
|
+
: task.projectBindings.length > 0
|
|
714
|
+
? "clean committed Task main required"
|
|
715
|
+
: "no Project evidence required"}`,
|
|
606
716
|
...(task.completedAt === undefined ? [] : [`Completed: ${presentTime(task.completedAt, timeZone)}`]),
|
|
607
717
|
...(task.completedBy === undefined ? [] : [`Completed by: ${task.completedBy}`]),
|
|
608
718
|
...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
|
|
@@ -631,7 +741,12 @@ function showTaskCommand(args, store) {
|
|
|
631
741
|
`Created: ${presentTime(task.createdAt, timeZone)}`,
|
|
632
742
|
`Updated: ${presentTime(task.updatedAt, timeZone)}`
|
|
633
743
|
].join("\n").concat("\n");
|
|
634
|
-
return output(rendered, {
|
|
744
|
+
return output(rendered, {
|
|
745
|
+
task,
|
|
746
|
+
deliveryPath: taskDeliveryPath(task),
|
|
747
|
+
counts,
|
|
748
|
+
hasBrief: brief !== null
|
|
749
|
+
});
|
|
635
750
|
}
|
|
636
751
|
function activateTaskCommand(args, store, options) {
|
|
637
752
|
exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
|
|
@@ -677,14 +792,18 @@ function completeTaskCommand(args, store, options) {
|
|
|
677
792
|
task,
|
|
678
793
|
changed: false,
|
|
679
794
|
runtimeCleanupTargets: [],
|
|
795
|
+
completionAdvisories: [],
|
|
680
796
|
finalReview: undefined,
|
|
681
797
|
publishedTreeAuthorization: undefined
|
|
682
798
|
};
|
|
683
799
|
}
|
|
684
800
|
const taskFinalContract = preflight.taskFinalReviewContract;
|
|
801
|
+
const actualTaskCandidate = task.projectBindings.length === 0
|
|
802
|
+
? undefined
|
|
803
|
+
: actualTaskReviewCandidateForMutation(tx, task, options);
|
|
685
804
|
const publishedTreeProof = request.acceptedPublishedTreePublicationId === undefined
|
|
686
805
|
? undefined
|
|
687
|
-
: assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof,
|
|
806
|
+
: assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskCandidate);
|
|
688
807
|
const requiresContractHandoff = publishedTreeProof !== undefined
|
|
689
808
|
&& taskFinalContract !== undefined;
|
|
690
809
|
if (requiresContractHandoff && actor !== "leader") {
|
|
@@ -695,6 +814,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
695
814
|
task,
|
|
696
815
|
changed: false,
|
|
697
816
|
runtimeCleanupTargets: [],
|
|
817
|
+
completionAdvisories: [],
|
|
698
818
|
finalReview: undefined,
|
|
699
819
|
publishedTreeAuthorization: {
|
|
700
820
|
event,
|
|
@@ -752,6 +872,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
752
872
|
task,
|
|
753
873
|
changed: false,
|
|
754
874
|
runtimeCleanupTargets: [],
|
|
875
|
+
completionAdvisories: [],
|
|
755
876
|
finalReview,
|
|
756
877
|
resumedPendingFinalReview: pendingFinalReviewIds.has(finalReview.id),
|
|
757
878
|
terminalizedLeaderRun,
|
|
@@ -783,13 +904,29 @@ function completeTaskCommand(args, store, options) {
|
|
|
783
904
|
by: actor,
|
|
784
905
|
projectId: publishedTreeProof.projectId,
|
|
785
906
|
publicationId: publishedTreeProof.publicationId,
|
|
786
|
-
|
|
907
|
+
...(publishedTreeProof.reviewRoundId === undefined
|
|
908
|
+
? {}
|
|
909
|
+
: { reviewRoundId: publishedTreeProof.reviewRoundId }),
|
|
787
910
|
localCommit: publishedTreeProof.localCommit,
|
|
788
911
|
remoteCommit: publishedTreeProof.remoteCommit,
|
|
789
912
|
tree: publishedTreeProof.tree
|
|
790
913
|
}, now);
|
|
791
914
|
}
|
|
792
|
-
recordTaskEvent(tx, task.id, "task.completed", {
|
|
915
|
+
recordTaskEvent(tx, task.id, "task.completed", {
|
|
916
|
+
by: actor,
|
|
917
|
+
summary,
|
|
918
|
+
deliveryPath: taskDeliveryPath(task),
|
|
919
|
+
...(actualTaskCandidate === undefined
|
|
920
|
+
? {}
|
|
921
|
+
: {
|
|
922
|
+
projectHeads: actualTaskCandidate.projects
|
|
923
|
+
.map(({ projectId, commit }) => `${projectId}@${commit}`)
|
|
924
|
+
.join(",")
|
|
925
|
+
}),
|
|
926
|
+
...(readiness.advisories.length === 0
|
|
927
|
+
? {}
|
|
928
|
+
: { cleanupAdvisories: String(readiness.advisories.length) })
|
|
929
|
+
}, now);
|
|
793
930
|
// A terminal Task must never leave a Task-lane signal that can wake it.
|
|
794
931
|
// The durable records remain intact; only the derived mailbox work is
|
|
795
932
|
// discarded at this lifecycle boundary.
|
|
@@ -805,6 +942,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
805
942
|
task: completed,
|
|
806
943
|
changed: true,
|
|
807
944
|
runtimeCleanupTargets,
|
|
945
|
+
completionAdvisories: readiness.advisories,
|
|
808
946
|
finalReview: undefined,
|
|
809
947
|
resumedPendingFinalReview: false,
|
|
810
948
|
terminalizedLeaderRun,
|
|
@@ -841,9 +979,17 @@ function completeTaskCommand(args, store, options) {
|
|
|
841
979
|
[TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW]: result.terminalizedLeaderRun
|
|
842
980
|
});
|
|
843
981
|
}
|
|
844
|
-
|
|
982
|
+
const completionOutput = result.changed
|
|
845
983
|
? `Completed task ${result.task.id}\n`
|
|
846
|
-
: `Task ${result.task.id} is already completed\n
|
|
984
|
+
: `Task ${result.task.id} is already completed\n`;
|
|
985
|
+
const advisoryOutput = result.completionAdvisories.length === 0
|
|
986
|
+
? ""
|
|
987
|
+
: `Cleanup advisories (non-blocking; settle before archive):\n`
|
|
988
|
+
+ result.completionAdvisories.map((advisory) => (`- ${advisory.code} (${advisory.ref.kind} ${advisory.ref.id}): ${advisory.fix}`)).join("\n").concat("\n");
|
|
989
|
+
return output(completionOutput + advisoryOutput, {
|
|
990
|
+
task: result.task,
|
|
991
|
+
completionAdvisories: result.completionAdvisories
|
|
992
|
+
});
|
|
847
993
|
}
|
|
848
994
|
function reopenTaskCommand(args, store, options) {
|
|
849
995
|
exactPositionals(args, 1, "Task reopen usage: yui task reopen <id>.");
|
|
@@ -1203,23 +1349,79 @@ function taskMessageCommand(args, store, options) {
|
|
|
1203
1349
|
}
|
|
1204
1350
|
if (messages.length === 0)
|
|
1205
1351
|
return "No messages found.\n";
|
|
1352
|
+
const retirements = new Map(store.listEvents(task.id).flatMap((event) => {
|
|
1353
|
+
const retirement = taskRecordRetirement(event);
|
|
1354
|
+
return retirement?.recordKind === "message"
|
|
1355
|
+
? [[retirement.recordId, retirement]]
|
|
1356
|
+
: [];
|
|
1357
|
+
}));
|
|
1206
1358
|
const timeZone = store.getConfig().timeZone;
|
|
1207
1359
|
return `${renderTable(`Task messages: ${task.id}`, [
|
|
1208
1360
|
{ header: "Message", minWidth: 7, maxWidth: 18 },
|
|
1361
|
+
{ header: "Status", minWidth: 6, maxWidth: 9 },
|
|
1209
1362
|
{ header: "Author", minWidth: 6, maxWidth: 18 },
|
|
1210
1363
|
{ header: "Created", minWidth: 10, maxWidth: 28 },
|
|
1211
1364
|
{ header: "Body", minWidth: 8, maxWidth: 72 }
|
|
1212
1365
|
], messages.map((message) => [
|
|
1213
1366
|
message.id,
|
|
1367
|
+
retirements.has(message.id) ? "retired" : "active",
|
|
1214
1368
|
taskMessageAuthorLabel(message.author),
|
|
1215
1369
|
presentTime(message.createdAt, timeZone),
|
|
1216
1370
|
message.body
|
|
1217
1371
|
]), defaultTableWidth())}\n`;
|
|
1218
1372
|
}
|
|
1373
|
+
if (command === "retire") {
|
|
1374
|
+
return retireMessage(rest, store, options);
|
|
1375
|
+
}
|
|
1219
1376
|
throw usageError(command === undefined
|
|
1220
1377
|
? "Task message command is required."
|
|
1221
1378
|
: `Unknown command: task message ${command}`);
|
|
1222
1379
|
}
|
|
1380
|
+
function retireMessage(args, store, options) {
|
|
1381
|
+
const usage = "Task message retire usage: yui task message retire <task>/<message> --reason <text>.";
|
|
1382
|
+
const parsed = parseTail(args, new Set(["--reason"]), usage);
|
|
1383
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1384
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
1385
|
+
const reference = taskRecordReference(parsed.positionals[0], "message", "Message reference", options);
|
|
1386
|
+
const now = clock(options);
|
|
1387
|
+
const result = store.transaction((tx) => {
|
|
1388
|
+
const task = requireTask(tx, reference.taskId);
|
|
1389
|
+
assertTaskOpen(task);
|
|
1390
|
+
const actor = taskActor(options, task.id);
|
|
1391
|
+
if (actor === "leader") {
|
|
1392
|
+
throw usageError("Only the user or global Operator may retire a Task Message.");
|
|
1393
|
+
}
|
|
1394
|
+
const message = tx.listMessages(task.id).find(({ id }) => id === reference.localId);
|
|
1395
|
+
if (message === undefined) {
|
|
1396
|
+
throw dataError(`Task Message not found: ${task.id}/${reference.localId}.`);
|
|
1397
|
+
}
|
|
1398
|
+
const events = tx.listEvents(task.id);
|
|
1399
|
+
if (isTaskRecordRetired(events, "message", message.id)) {
|
|
1400
|
+
return { task, message, changed: false };
|
|
1401
|
+
}
|
|
1402
|
+
// Remove an isolated pending wake for this exact directive. A merged batch
|
|
1403
|
+
// is retained because its other signals remain actionable; context and
|
|
1404
|
+
// actionability projections still filter the retired message below.
|
|
1405
|
+
try {
|
|
1406
|
+
settleExactWorkExecution(tx, leaderMailbox(task.id), messageRef(task.id, message.id));
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
// Merged pending work cannot be split without losing unrelated signals.
|
|
1410
|
+
}
|
|
1411
|
+
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
1412
|
+
eventId: tx.nextEventId(task.id),
|
|
1413
|
+
taskId: task.id,
|
|
1414
|
+
recordKind: "message",
|
|
1415
|
+
recordId: message.id,
|
|
1416
|
+
reason,
|
|
1417
|
+
retiredBy: actor
|
|
1418
|
+
}, now));
|
|
1419
|
+
return { task, message, changed: true };
|
|
1420
|
+
});
|
|
1421
|
+
if (result.changed)
|
|
1422
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
1423
|
+
return `Retired Task Message ${result.task.id}/${result.message.id}\n`;
|
|
1424
|
+
}
|
|
1223
1425
|
/**
|
|
1224
1426
|
* Issue 05: force-wake escape hatch. Bypasses the actionability digest and
|
|
1225
1427
|
* enqueues exactly one Leader wakeup with an auditable reason. The reason is
|
|
@@ -2514,9 +2716,7 @@ function retireWork(args, store, options) {
|
|
|
2514
2716
|
if (task.status !== "active") {
|
|
2515
2717
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2516
2718
|
}
|
|
2517
|
-
|
|
2518
|
-
throw usageError("Only the Task Leader may retire a Work Item.");
|
|
2519
|
-
}
|
|
2719
|
+
const actor = taskActor(options, task.id);
|
|
2520
2720
|
if (replacementWorkItemId !== undefined) {
|
|
2521
2721
|
const replacement = requireWorkItem(tx, replacementWorkItemId, options);
|
|
2522
2722
|
if (replacement.taskId !== task.id) {
|
|
@@ -2556,7 +2756,7 @@ function retireWork(args, store, options) {
|
|
|
2556
2756
|
}
|
|
2557
2757
|
}
|
|
2558
2758
|
const next = retireWorkItem(item, {
|
|
2559
|
-
by:
|
|
2759
|
+
by: actor,
|
|
2560
2760
|
summary,
|
|
2561
2761
|
...(replacementWorkItemId === undefined ? {} : { replacementWorkItemId })
|
|
2562
2762
|
}, now);
|
|
@@ -2568,8 +2768,16 @@ function retireWork(args, store, options) {
|
|
|
2568
2768
|
...(replacementWorkItemId === undefined
|
|
2569
2769
|
? {}
|
|
2570
2770
|
: { replacementWorkItemId }),
|
|
2571
|
-
...leaderActionEventPayload(tx, task.id, options)
|
|
2771
|
+
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : { retiredBy: actor })
|
|
2572
2772
|
}, now);
|
|
2773
|
+
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
2774
|
+
eventId: tx.nextEventId(task.id),
|
|
2775
|
+
taskId: task.id,
|
|
2776
|
+
recordKind: "work-item",
|
|
2777
|
+
recordId: next.id,
|
|
2778
|
+
reason: summary,
|
|
2779
|
+
retiredBy: actor
|
|
2780
|
+
}, now));
|
|
2573
2781
|
}
|
|
2574
2782
|
return next;
|
|
2575
2783
|
});
|
|
@@ -2678,6 +2886,8 @@ function reviewWork(args, store, options) {
|
|
|
2678
2886
|
*/
|
|
2679
2887
|
function taskReviewCommand(args, store, options) {
|
|
2680
2888
|
const [command, ...rest] = args;
|
|
2889
|
+
if (command === "rebind")
|
|
2890
|
+
return rebindTaskFinalReviewContract(rest, store, options);
|
|
2681
2891
|
if (command === "request")
|
|
2682
2892
|
return requestTaskReviewRound(rest, store, options);
|
|
2683
2893
|
if (command === "force-fresh")
|
|
@@ -2692,6 +2902,115 @@ function taskReviewCommand(args, store, options) {
|
|
|
2692
2902
|
? "Task review command is required."
|
|
2693
2903
|
: `Unknown command: task review ${command}`);
|
|
2694
2904
|
}
|
|
2905
|
+
export function parseTaskFinalReviewContractRebindRequest(args) {
|
|
2906
|
+
const usage = "Task review rebind usage: yui task review rebind <task> "
|
|
2907
|
+
+ "--from-control <digest> --to-control <digest> "
|
|
2908
|
+
+ "--from-release <release-id> --to-release <release-id>.";
|
|
2909
|
+
const parsed = parseTail(args, new Set(["--from-control", "--to-control", "--from-release", "--to-release"]), usage);
|
|
2910
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
2911
|
+
return {
|
|
2912
|
+
taskId: parsed.positionals[0],
|
|
2913
|
+
fromControlPlaneDigest: requiredOption(parsed.options, "--from-control"),
|
|
2914
|
+
toControlPlaneDigest: requiredOption(parsed.options, "--to-control"),
|
|
2915
|
+
fromReleaseId: requiredOption(parsed.options, "--from-release"),
|
|
2916
|
+
toReleaseId: requiredOption(parsed.options, "--to-release")
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
function rebindTaskFinalReviewContract(args, store, options) {
|
|
2920
|
+
const request = parseTaskFinalReviewContractRebindRequest(args);
|
|
2921
|
+
const environment = options.environment ?? {};
|
|
2922
|
+
const usage = "Task review rebind usage: yui task review rebind <task> "
|
|
2923
|
+
+ "--from-control <digest> --to-control <digest> "
|
|
2924
|
+
+ "--from-release <release-id> --to-release <release-id>.";
|
|
2925
|
+
if (!isCurrentGlobalOperator(store, environment)) {
|
|
2926
|
+
throw usageError("Task-final Review contract rebind requires the authenticated global Operator session.", usage);
|
|
2927
|
+
}
|
|
2928
|
+
const proof = options.taskFinalReviewRebindProof;
|
|
2929
|
+
if (proof === undefined
|
|
2930
|
+
|| proof.schemaVersion !== 1
|
|
2931
|
+
|| proof.taskId !== request.taskId
|
|
2932
|
+
|| proof.fromControlPlaneDigest !== request.fromControlPlaneDigest
|
|
2933
|
+
|| proof.toControlPlaneDigest !== request.toControlPlaneDigest
|
|
2934
|
+
|| proof.fromRelease.releaseId !== request.fromReleaseId
|
|
2935
|
+
|| proof.toRelease.releaseId !== request.toReleaseId) {
|
|
2936
|
+
throw usageError("Task-final Review contract rebind proof is missing or does not match the explicit request.", usage);
|
|
2937
|
+
}
|
|
2938
|
+
const now = clock(options);
|
|
2939
|
+
const result = store.transaction((tx) => {
|
|
2940
|
+
const task = requireTask(tx, request.taskId);
|
|
2941
|
+
if (task.status !== "active") {
|
|
2942
|
+
throw usageError(`Task is not active: ${task.id}.`);
|
|
2943
|
+
}
|
|
2944
|
+
if (task.projectBindings.length === 0) {
|
|
2945
|
+
throw usageError(`Task final-review contract requires a Project-backed Task: ${task.id}.`);
|
|
2946
|
+
}
|
|
2947
|
+
if (!isCurrentGlobalOperator(tx, environment)) {
|
|
2948
|
+
throw usageError("Task-final Review contract rebind Operator identity drifted before commit.");
|
|
2949
|
+
}
|
|
2950
|
+
const resolution = storedTaskFinalReviewContractResolution(tx, task.id);
|
|
2951
|
+
if (resolution === undefined) {
|
|
2952
|
+
throw usageError(`Task final-review contract is missing for ${task.id}.`);
|
|
2953
|
+
}
|
|
2954
|
+
const exactExisting = resolution.rebinds.at(-1);
|
|
2955
|
+
if (resolution.effective.controlPlaneDigest === request.toControlPlaneDigest) {
|
|
2956
|
+
if (exactExisting !== undefined
|
|
2957
|
+
&& exactExisting.fromContract.controlPlaneDigest === request.fromControlPlaneDigest
|
|
2958
|
+
&& exactExisting.toContract.controlPlaneDigest === request.toControlPlaneDigest
|
|
2959
|
+
&& exactExisting.fromRelease.releaseId === request.fromReleaseId
|
|
2960
|
+
&& exactExisting.toRelease.releaseId === request.toReleaseId
|
|
2961
|
+
&& exactExisting.handoverId === proof.handoverId) {
|
|
2962
|
+
return { rebind: exactExisting, changed: false };
|
|
2963
|
+
}
|
|
2964
|
+
throw usageError(`Task final-review contract already targets ${request.toControlPlaneDigest} without the requested proof tuple.`);
|
|
2965
|
+
}
|
|
2966
|
+
if (resolution.effective.controlPlaneDigest !== request.fromControlPlaneDigest) {
|
|
2967
|
+
throw usageError(`Task final-review contract source control-plane digest drifted for ${task.id}.`);
|
|
2968
|
+
}
|
|
2969
|
+
const activeRound = tx.listReviewRounds(task.id).find((round) => ((round.scope ?? "work-item") === "task"
|
|
2970
|
+
&& (round.status === "pending" || round.status === "running")));
|
|
2971
|
+
if (activeRound !== undefined) {
|
|
2972
|
+
throw usageError(`Task final-review contract cannot rebind while ReviewRound ${activeRound.id} is ${activeRound.status}.`);
|
|
2973
|
+
}
|
|
2974
|
+
const activeLeaderRun = tx.getActiveAgentRun(task.id, "leader");
|
|
2975
|
+
if (activeLeaderRun !== null) {
|
|
2976
|
+
throw usageError(`Task final-review contract cannot rebind while Leader Run ${activeLeaderRun.id} is active.`);
|
|
2977
|
+
}
|
|
2978
|
+
const leaderSessions = tx.getTaskRoleSessionSet(task.id, "leader");
|
|
2979
|
+
if (leaderSessions !== null) {
|
|
2980
|
+
if (leaderSessions.inFlight !== null) {
|
|
2981
|
+
throw usageError("Task final-review contract cannot rebind while the Leader runtime has unsettled Run state.");
|
|
2982
|
+
}
|
|
2983
|
+
const liveLeaderSession = Object.values(leaderSessions.sessions).find(({ status }) => status !== "stopped" && status !== "broken");
|
|
2984
|
+
if (liveLeaderSession !== undefined) {
|
|
2985
|
+
throw usageError(`Task final-review contract cannot rebind while Leader Session ${liveLeaderSession.agentId} is ${liveLeaderSession.status}.`);
|
|
2986
|
+
}
|
|
2987
|
+
if (Object.keys(leaderSessions.sessions).length > 0
|
|
2988
|
+
|| leaderSessions.providerBinding !== null) {
|
|
2989
|
+
tx.saveTaskRoleSessionSet(retireTaskRoleSessionsForWorkspace(leaderSessions, now));
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
const rebind = createTaskFinalReviewContractRebind({
|
|
2993
|
+
taskId: task.id,
|
|
2994
|
+
reviewerRoleName: resolution.effective.reviewerRoleName,
|
|
2995
|
+
fromContract: resolution.effective,
|
|
2996
|
+
toControlPlaneDigest: proof.toControlPlaneDigest,
|
|
2997
|
+
fromRelease: proof.fromRelease,
|
|
2998
|
+
toRelease: proof.toRelease,
|
|
2999
|
+
handoverId: proof.handoverId,
|
|
3000
|
+
authorizedBy: `operator:${environment.YUI_AGENT_ID ?? "unknown"}`
|
|
3001
|
+
});
|
|
3002
|
+
const event = recordTaskEventRecord(tx, task.id, TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, taskFinalReviewContractRebindPayload(rebind), now);
|
|
3003
|
+
enqueueWork(tx, leaderMailbox(task.id), wakeReason("review-contract-rebound", event.id), now, [eventRef(task.id, event.id)]);
|
|
3004
|
+
return { rebind, event, changed: true };
|
|
3005
|
+
});
|
|
3006
|
+
if (result.changed) {
|
|
3007
|
+
notifyMailbox(options.runtime, leaderMailbox(request.taskId), request.taskId);
|
|
3008
|
+
}
|
|
3009
|
+
return output(result.changed
|
|
3010
|
+
? `Rebound Task-final Review contract for ${request.taskId} from `
|
|
3011
|
+
+ `${request.fromControlPlaneDigest} to ${request.toControlPlaneDigest}.\n`
|
|
3012
|
+
: `Task-final Review contract rebind is already recorded for ${request.taskId}.\n`, result);
|
|
3013
|
+
}
|
|
2695
3014
|
function resolveReviewExecutionGroup(args, store, options) {
|
|
2696
3015
|
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
3016
|
if (args[0] !== "resolve")
|
|
@@ -2786,8 +3105,8 @@ function resolveReviewExecutionGroup(args, store, options) {
|
|
|
2786
3105
|
/**
|
|
2787
3106
|
* Issue 06: `yui task review finding` — the cross-Round finding ledger CLI.
|
|
2788
3107
|
* Findings are extracted automatically from completed Rounds; these commands
|
|
2789
|
-
* let the Leader inspect the ledger, disposition each finding, and plan
|
|
2790
|
-
*
|
|
3108
|
+
* let the Leader inspect the ledger, disposition each finding, and plan one
|
|
3109
|
+
* convergent repair unit by default. Parallel fan-out is explicit.
|
|
2791
3110
|
*/
|
|
2792
3111
|
function reviewFindingCommand(args, store, options) {
|
|
2793
3112
|
const [command, ...rest] = args;
|
|
@@ -2858,11 +3177,15 @@ function disposeReviewFindingCommand(args, store, options) {
|
|
|
2858
3177
|
return output(`Dispositioned ${result.id} as ${result.disposition}.\n`);
|
|
2859
3178
|
}
|
|
2860
3179
|
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"]));
|
|
3180
|
+
const usage = "Task review finding repair-wave usage: yui task review finding repair-wave <task> [--strategy <consolidated|parallel>] [--create].";
|
|
3181
|
+
const parsed = parseTail(args, new Set(["--strategy"]), usage, new Set(["--create"]));
|
|
2863
3182
|
exactPositionals(parsed.positionals, 1, usage);
|
|
2864
3183
|
const task = requireTask(store, parsed.positionals[0]);
|
|
2865
|
-
const
|
|
3184
|
+
const strategy = parsed.options.get("--strategy") ?? "consolidated";
|
|
3185
|
+
if (strategy !== "consolidated" && strategy !== "parallel") {
|
|
3186
|
+
throw usageError(`Review repair strategy is invalid: ${strategy}.`, usage);
|
|
3187
|
+
}
|
|
3188
|
+
const groups = repairGroupsForStrategy(planRepairGroups(store, task.id), strategy);
|
|
2866
3189
|
if (groups.length === 0) {
|
|
2867
3190
|
return output(`No open P1/P2 findings need repair for ${task.id}.\n`);
|
|
2868
3191
|
}
|
|
@@ -2903,7 +3226,7 @@ function planReviewRepairWave(args, store, options) {
|
|
|
2903
3226
|
});
|
|
2904
3227
|
const lines = created.map(({ group, item, changed }) => (`wave ${group.groupKey}: ${item.id} ${changed ? "created" : "already open"} `
|
|
2905
3228
|
+ `(${group.findingIds.join(", ")})`));
|
|
2906
|
-
return output(`Review repair wave for ${task.id} (${groups.length} group(s)):\n${lines.join("\n")}\n`, { groups: created });
|
|
3229
|
+
return output(`Review repair wave for ${task.id} (${strategy}, ${groups.length} group(s)):\n${lines.join("\n")}\n`, { strategy, groups: created });
|
|
2907
3230
|
}
|
|
2908
3231
|
const lines = groups.map((group, index) => {
|
|
2909
3232
|
const findings = group.findings
|
|
@@ -2912,7 +3235,24 @@ function planReviewRepairWave(args, store, options) {
|
|
|
2912
3235
|
return `wave ${index + 1}: ${findings}`
|
|
2913
3236
|
+ ` (paths: ${group.affectedPaths.join(", ") || "none"}; invariants: ${group.invariants.join(", ")})`;
|
|
2914
3237
|
});
|
|
2915
|
-
return output(`Repair wave for ${task.id} (${groups.length} group(s)
|
|
3238
|
+
return output(`Repair wave for ${task.id} (${strategy}, ${groups.length} group(s)):\n${lines.join("\n")}\n`
|
|
3239
|
+
+ (strategy === "consolidated"
|
|
3240
|
+
? "Default: keep all findings in one WorkItem; use --strategy parallel only for proven independent ownership.\n"
|
|
3241
|
+
: "Parallel strategy explicitly selected; each disjoint group may become one WorkItem.\n"));
|
|
3242
|
+
}
|
|
3243
|
+
function repairGroupsForStrategy(groups, strategy) {
|
|
3244
|
+
if (strategy === "parallel" || groups.length <= 1)
|
|
3245
|
+
return groups;
|
|
3246
|
+
const findings = groups.flatMap(({ findings }) => findings)
|
|
3247
|
+
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
|
|
3248
|
+
return [{
|
|
3249
|
+
groupKey: findings.map(({ id }) => id).join("+"),
|
|
3250
|
+
findings,
|
|
3251
|
+
findingIds: findings.map(({ id }) => id),
|
|
3252
|
+
affectedPaths: [...new Set(groups.flatMap(({ affectedPaths }) => affectedPaths))].sort(),
|
|
3253
|
+
affectedSymbols: [...new Set(groups.flatMap(({ affectedSymbols }) => affectedSymbols))].sort(),
|
|
3254
|
+
invariants: [...new Set(groups.flatMap(({ invariants }) => invariants))].sort()
|
|
3255
|
+
}];
|
|
2916
3256
|
}
|
|
2917
3257
|
function reviewRepairProjectIds(task, affectedPaths) {
|
|
2918
3258
|
const bindings = task.projectBindings;
|
|
@@ -3155,9 +3495,9 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3155
3495
|
const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
|
|
3156
3496
|
/**
|
|
3157
3497
|
* 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.
|
|
3498
|
+
* terminal Round durably proves that no semantic review was produced. The
|
|
3499
|
+
* source Round, Run, findings, workspace, and terminal report remain immutable
|
|
3500
|
+
* history; the linking Event is both the audit record and the idempotence key.
|
|
3161
3501
|
*/
|
|
3162
3502
|
function forceFreshTaskReviewRound(args, store, options) {
|
|
3163
3503
|
const usage = "Task review force-fresh usage: yui task review force-fresh <task>/<review-round>.";
|
|
@@ -3206,9 +3546,9 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3206
3546
|
}
|
|
3207
3547
|
return { round: replacement, source, created: false };
|
|
3208
3548
|
}
|
|
3209
|
-
const
|
|
3210
|
-
if (
|
|
3211
|
-
throw usageError(`ReviewRound ${source.id} is not eligible for force-fresh: ${
|
|
3549
|
+
const recovery = classifyForceFreshReviewRecovery(tx, source);
|
|
3550
|
+
if (recovery.kind === "semantic-or-ambiguous") {
|
|
3551
|
+
throw usageError(`ReviewRound ${source.id} is not eligible for force-fresh: ${recovery.reason}`);
|
|
3212
3552
|
}
|
|
3213
3553
|
if (source.taskCandidate === undefined) {
|
|
3214
3554
|
throw dataError(`ReviewRound ${source.id} has no frozen Task candidate.`);
|
|
@@ -3275,7 +3615,7 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3275
3615
|
candidateId: created.candidateId,
|
|
3276
3616
|
reviewerRoleName: created.reviewerRoleName,
|
|
3277
3617
|
taskCandidate: JSON.stringify(created.taskCandidate),
|
|
3278
|
-
reason: "source-round-
|
|
3618
|
+
reason: "source-round-terminal-without-semantic-review",
|
|
3279
3619
|
leaderActionRunId: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader"
|
|
3280
3620
|
}, now);
|
|
3281
3621
|
return { round: created, source, created: true };
|
|
@@ -3284,76 +3624,23 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3284
3624
|
? `Fresh Task-final Review requested as ${result.round.id} after ${result.source.id}\n`
|
|
3285
3625
|
: `Fresh Task-final Review already requested as ${result.round.id} after ${result.source.id}\n`, { reviewRound: result.round, sourceReviewRound: result.source });
|
|
3286
3626
|
}
|
|
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));
|
|
3627
|
+
/**
|
|
3628
|
+
* Conservatively classifies an immutable Task-final Review as replaceable.
|
|
3629
|
+
* A failed Round keeps the existing no-semantic-evidence behavior. A completed
|
|
3630
|
+
* Round needs stronger, mutually corroborating evidence: an explicit internal
|
|
3631
|
+
* context/workspace failure, its exact yielded Run and receipt, matching Lane
|
|
3632
|
+
* output, and the mechanically emitted empty completion Event.
|
|
3633
|
+
* This is command eligibility only; it never rewrites the source outcome or
|
|
3634
|
+
* changes the global semantic classifier used by the finding ledger.
|
|
3635
|
+
*/
|
|
3636
|
+
export function classifyForceFreshReviewRecovery(store, round) {
|
|
3637
|
+
const classification = classifyReviewRoundOutcome(round, store);
|
|
3638
|
+
return classification?.kind === "non-semantic"
|
|
3639
|
+
? { kind: "non-semantic-terminal", reason: classification.reason }
|
|
3640
|
+
: {
|
|
3641
|
+
kind: "semantic-or-ambiguous",
|
|
3642
|
+
reason: classification?.reason ?? `source status is ${round.status}, not terminal.`
|
|
3643
|
+
};
|
|
3357
3644
|
}
|
|
3358
3645
|
/**
|
|
3359
3646
|
* Issue 07: re-validates the CLI-computed delta preflight inside the store
|
|
@@ -3369,8 +3656,8 @@ function validateDeltaRecheckRequest(store, taskId, reviewerRoleName, candidate,
|
|
|
3369
3656
|
const previous = store.getReviewRound(taskId, preflight.record.previousReviewRoundId);
|
|
3370
3657
|
if (previous === null
|
|
3371
3658
|
|| (previous.scope ?? "work-item") !== "task"
|
|
3372
|
-
|| previous
|
|
3373
|
-
throw usageError(`Delta-recheck previous ReviewRound is not a completed Task-final Review: `
|
|
3659
|
+
|| !isSemanticReviewRound(previous, store)) {
|
|
3660
|
+
throw usageError(`Delta-recheck previous ReviewRound is not a semantic completed Task-final Review: `
|
|
3374
3661
|
+ `${preflight.record.previousReviewRoundId}.`);
|
|
3375
3662
|
}
|
|
3376
3663
|
if (previous.reviewerRoleName !== reviewerRoleName) {
|
|
@@ -3577,10 +3864,64 @@ function taskRunCommand(args, store, options) {
|
|
|
3577
3864
|
return yieldRunStatus(rest, store, options);
|
|
3578
3865
|
if (command === "checkpoint")
|
|
3579
3866
|
return output(checkpointRun(rest, store, options));
|
|
3867
|
+
if (command === "retire")
|
|
3868
|
+
return retireRun(rest, store, options);
|
|
3580
3869
|
throw usageError(command === undefined
|
|
3581
3870
|
? "Task run command is required."
|
|
3582
3871
|
: `Unknown command: task run ${command}`);
|
|
3583
3872
|
}
|
|
3873
|
+
function retireRun(args, store, options) {
|
|
3874
|
+
const usage = "Task run retire usage: yui task run retire <task>/<run> --reason <text>.";
|
|
3875
|
+
const parsed = parseTail(args, new Set(["--reason"]), usage);
|
|
3876
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
3877
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
3878
|
+
const reference = taskRecordReference(parsed.positionals[0], "agentRun", "Agent Run reference", options);
|
|
3879
|
+
const now = clock(options);
|
|
3880
|
+
const result = store.transaction((tx) => {
|
|
3881
|
+
const task = requireTask(tx, reference.taskId);
|
|
3882
|
+
assertTaskOpen(task);
|
|
3883
|
+
const actor = taskActor(options, task.id);
|
|
3884
|
+
if (actor === "leader") {
|
|
3885
|
+
throw usageError("Only the user or global Operator may retire an Agent Run.");
|
|
3886
|
+
}
|
|
3887
|
+
let run = tx.getAgentRun(task.id, reference.localId);
|
|
3888
|
+
if (run === null)
|
|
3889
|
+
throw dataError(`Agent Run not found: ${task.id}/${reference.localId}.`);
|
|
3890
|
+
const events = tx.listEvents(task.id);
|
|
3891
|
+
if (isTaskRecordRetired(events, "agent-run", run.id)) {
|
|
3892
|
+
return { task, run, changed: false };
|
|
3893
|
+
}
|
|
3894
|
+
if (run.status === "active") {
|
|
3895
|
+
const terminal = terminalizeExactTaskRun(tx, {
|
|
3896
|
+
taskId: task.id,
|
|
3897
|
+
roleName: run.roleName,
|
|
3898
|
+
agentId: run.effective.agentId,
|
|
3899
|
+
runId: run.id,
|
|
3900
|
+
receiptId: agentRunDeliveryReceiptId(run),
|
|
3901
|
+
outcome: { status: "failed", summary: `Agent Run retired: ${reason}` }
|
|
3902
|
+
}, now);
|
|
3903
|
+
if (terminal.disposition !== "applied" || terminal.run === null) {
|
|
3904
|
+
throw usageError(`Agent Run changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
3905
|
+
}
|
|
3906
|
+
run = terminal.run;
|
|
3907
|
+
}
|
|
3908
|
+
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
3909
|
+
eventId: tx.nextEventId(task.id),
|
|
3910
|
+
taskId: task.id,
|
|
3911
|
+
recordKind: "agent-run",
|
|
3912
|
+
recordId: run.id,
|
|
3913
|
+
reason,
|
|
3914
|
+
retiredBy: actor
|
|
3915
|
+
}, now));
|
|
3916
|
+
return { task, run, changed: true };
|
|
3917
|
+
});
|
|
3918
|
+
if (result.changed)
|
|
3919
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
3920
|
+
return output(`Retired Agent Run ${result.task.id}/${result.run.id}\n`, {
|
|
3921
|
+
agentRun: result.run,
|
|
3922
|
+
retired: true
|
|
3923
|
+
});
|
|
3924
|
+
}
|
|
3584
3925
|
function runContextCommand(args, store, options) {
|
|
3585
3926
|
const [first, ...rest] = args;
|
|
3586
3927
|
if (first === "expand") {
|
|
@@ -3654,6 +3995,7 @@ function listRuns(args, store, options) {
|
|
|
3654
3995
|
const runs = store.listAgentRuns(item.taskId).filter((run) => run.workItemId === item.id);
|
|
3655
3996
|
if (runs.length === 0)
|
|
3656
3997
|
return "No runs found.\n";
|
|
3998
|
+
const events = store.listEvents(item.taskId);
|
|
3657
3999
|
return `${renderTable(`Runs: ${item.id}`, [
|
|
3658
4000
|
{ header: "Run", minWidth: 6, maxWidth: 20 },
|
|
3659
4001
|
{ header: "Role", minWidth: 4, maxWidth: 22 },
|
|
@@ -3663,6 +4005,7 @@ function listRuns(args, store, options) {
|
|
|
3663
4005
|
{ header: "Profile", minWidth: 7, maxWidth: 8 },
|
|
3664
4006
|
{ header: "Permission", minWidth: 8, maxWidth: 16 },
|
|
3665
4007
|
{ header: "Status", minWidth: 6, maxWidth: 12 },
|
|
4008
|
+
{ header: "History", minWidth: 7, maxWidth: 9 },
|
|
3666
4009
|
{ header: "Summary", minWidth: 8, maxWidth: 58 }
|
|
3667
4010
|
], runs.map((run) => [
|
|
3668
4011
|
run.id,
|
|
@@ -3673,6 +4016,7 @@ function listRuns(args, store, options) {
|
|
|
3673
4016
|
run.effective.profileAccess,
|
|
3674
4017
|
run.effective.permission.strategy,
|
|
3675
4018
|
run.status,
|
|
4019
|
+
isTaskRecordRetired(events, "agent-run", run.id) ? "retired" : "active",
|
|
3676
4020
|
run.summary ?? "-"
|
|
3677
4021
|
]), defaultTableWidth())}\n`;
|
|
3678
4022
|
}
|
|
@@ -3986,7 +4330,7 @@ function retryRun(args, store, options) {
|
|
|
3986
4330
|
}
|
|
3987
4331
|
function actualTaskReviewCandidateForMutation(store, task, options) {
|
|
3988
4332
|
if (options.actualTaskReviewCandidate === undefined) {
|
|
3989
|
-
throw usageError(`Actual Task Project heads were not verified for
|
|
4333
|
+
throw usageError(`Actual Task Project heads were not verified for delivery: ${task.id}.`);
|
|
3990
4334
|
}
|
|
3991
4335
|
let actual;
|
|
3992
4336
|
try {
|
|
@@ -4202,8 +4546,8 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4202
4546
|
// Any Task-final ReviewRound is durable completion evidence/obligation.
|
|
4203
4547
|
// Once one exists, later changes to the mutable global review config cannot
|
|
4204
4548
|
// weaken the requirement or change its reviewer. Before the first such
|
|
4205
|
-
// Round, the current global `final` config
|
|
4206
|
-
//
|
|
4549
|
+
// Round, the current global `final` config establishes the initial Round
|
|
4550
|
+
// only for integrated delivery.
|
|
4207
4551
|
const taskRounds = reviewRoundsByIdentity(store.listReviewRounds(task.id))
|
|
4208
4552
|
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
4209
4553
|
&& (taskFinalContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract))));
|
|
@@ -4214,7 +4558,15 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4214
4558
|
}
|
|
4215
4559
|
else if (establishedRound === undefined) {
|
|
4216
4560
|
const globalConfig = store.getReviewConfig();
|
|
4217
|
-
|
|
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;
|
|
4218
4570
|
}
|
|
4219
4571
|
else {
|
|
4220
4572
|
config = { roleName: establishedRound.reviewerRoleName, trigger: "final" };
|
|
@@ -4259,7 +4611,10 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
4259
4611
|
return escalated;
|
|
4260
4612
|
}
|
|
4261
4613
|
else {
|
|
4262
|
-
return latest.status === "completed"
|
|
4614
|
+
return latest.status === "completed"
|
|
4615
|
+
&& classifyReviewRoundOutcome(latest, store)?.kind === "semantic"
|
|
4616
|
+
? null
|
|
4617
|
+
: latest;
|
|
4263
4618
|
}
|
|
4264
4619
|
}
|
|
4265
4620
|
const anchor = taskFinalContract === undefined
|
|
@@ -4672,14 +5027,21 @@ function showRun(args, store, options) {
|
|
|
4672
5027
|
const facts = readRunRecoveryFacts(tx, run.taskId, run.id);
|
|
4673
5028
|
if (facts === null)
|
|
4674
5029
|
throw usageError(`Agent Run not found: ${run.taskId}/${run.id}.`, usage);
|
|
4675
|
-
|
|
5030
|
+
const retirement = tx.listEvents(run.taskId)
|
|
5031
|
+
.map(taskRecordRetirement)
|
|
5032
|
+
.find((entry) => entry?.recordKind === "agent-run" && entry.recordId === run.id) ?? null;
|
|
5033
|
+
return { run, recovery: projectRunRecovery(facts), retirement };
|
|
4676
5034
|
});
|
|
4677
5035
|
if (asJson) {
|
|
4678
5036
|
return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
|
|
4679
5037
|
}
|
|
4680
|
-
return {
|
|
5038
|
+
return {
|
|
5039
|
+
kind: "output",
|
|
5040
|
+
output: renderRunShow(data.run, data.recovery, data.retirement),
|
|
5041
|
+
data
|
|
5042
|
+
};
|
|
4681
5043
|
}
|
|
4682
|
-
function renderRunShow(run, recovery) {
|
|
5044
|
+
function renderRunShow(run, recovery, retirement) {
|
|
4683
5045
|
const lines = [
|
|
4684
5046
|
`Run: ${run.id}`,
|
|
4685
5047
|
`Task: ${run.taskId}`,
|
|
@@ -4687,6 +5049,10 @@ function renderRunShow(run, recovery) {
|
|
|
4687
5049
|
`Purpose: ${run.purpose}`,
|
|
4688
5050
|
`Mode: ${run.mode}`,
|
|
4689
5051
|
`Status: ${run.status}`,
|
|
5052
|
+
...(retirement === null ? [] : [
|
|
5053
|
+
`History: retired by ${retirement.retiredBy}`,
|
|
5054
|
+
`Retirement reason: ${retirement.reason}`
|
|
5055
|
+
]),
|
|
4690
5056
|
`Effective: ${run.effective.agentId}/${run.effective.adapterId} r${run.effective.sourceDesiredRevision}`,
|
|
4691
5057
|
`Created: ${run.createdAt}`,
|
|
4692
5058
|
...(run.pushedAt === undefined ? [] : [`Pushed: ${run.pushedAt}`]),
|
|
@@ -5380,8 +5746,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5380
5746
|
let deltaContext = "";
|
|
5381
5747
|
if (taskScope && round.deltaRecheck !== undefined) {
|
|
5382
5748
|
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}.`);
|
|
5749
|
+
if (previousRound === null || !isSemanticReviewRound(previousRound, tx)) {
|
|
5750
|
+
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck previous semantic ReviewRound is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
|
|
5385
5751
|
}
|
|
5386
5752
|
const diffByProject = options.deltaRecheckDiff;
|
|
5387
5753
|
if (diffByProject === undefined) {
|
|
@@ -5669,7 +6035,9 @@ function publishedTreeAuthorizationPayload(actor, proof) {
|
|
|
5669
6035
|
by: actor,
|
|
5670
6036
|
projectId: proof.projectId,
|
|
5671
6037
|
publicationId: proof.publicationId,
|
|
5672
|
-
|
|
6038
|
+
...(proof.reviewRoundId === undefined
|
|
6039
|
+
? { reviewAnchor: publishedTreeReviewAnchor(proof) }
|
|
6040
|
+
: { reviewRoundId: proof.reviewRoundId }),
|
|
5673
6041
|
localCommit: proof.localCommit,
|
|
5674
6042
|
remoteCommit: proof.remoteCommit,
|
|
5675
6043
|
tree: proof.tree
|
|
@@ -5685,7 +6053,8 @@ function matchingPublishedTreeAuthorization(store, proof) {
|
|
|
5685
6053
|
&& (event.payload.by === "user" || event.payload.by === "operator")
|
|
5686
6054
|
&& event.payload.projectId === proof.projectId
|
|
5687
6055
|
&& event.payload.publicationId === proof.publicationId
|
|
5688
|
-
&& event.payload.
|
|
6056
|
+
&& (event.payload.reviewAnchor ?? event.payload.reviewRoundId)
|
|
6057
|
+
=== publishedTreeReviewAnchor(proof)
|
|
5689
6058
|
&& event.payload.localCommit === proof.localCommit
|
|
5690
6059
|
&& event.payload.remoteCommit === proof.remoteCommit
|
|
5691
6060
|
&& event.payload.tree === proof.tree) {
|
|
@@ -5698,10 +6067,15 @@ function requirePublishedTreeAuthorization(store, proof) {
|
|
|
5698
6067
|
const authorization = matchingPublishedTreeAuthorization(store, proof);
|
|
5699
6068
|
if (authorization === undefined) {
|
|
5700
6069
|
throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
|
|
5701
|
-
+ `${proof.taskId}/${proof.publicationId} at
|
|
6070
|
+
+ `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
|
|
6071
|
+
? "the direct Task-main head"
|
|
6072
|
+
: `Task-final Review ${proof.reviewRoundId}`}.`);
|
|
5702
6073
|
}
|
|
5703
6074
|
return authorization;
|
|
5704
6075
|
}
|
|
6076
|
+
function publishedTreeReviewAnchor(proof) {
|
|
6077
|
+
return proof.reviewRoundId ?? "direct";
|
|
6078
|
+
}
|
|
5705
6079
|
/** Keeps a free-text run-fact note bounded so an event payload stays compact. */
|
|
5706
6080
|
function truncateEventNote(note) {
|
|
5707
6081
|
const normalized = note.trim();
|
|
@@ -5822,7 +6196,8 @@ function taskRecordReference(value, kind, label, options) {
|
|
|
5822
6196
|
function assertWorkItemDependenciesCompleted(store, item) {
|
|
5823
6197
|
for (const dependencyId of item.dependsOn) {
|
|
5824
6198
|
const dependency = store.getWorkItem(item.taskId, dependencyId);
|
|
5825
|
-
if (dependency === null
|
|
6199
|
+
if (dependency === null
|
|
6200
|
+
|| (dependency.status !== "completed" && dependency.status !== "retired")) {
|
|
5826
6201
|
throw usageError(`Work Item dependency is not completed: ${dependencyId}.`);
|
|
5827
6202
|
}
|
|
5828
6203
|
}
|
|
@@ -5962,6 +6337,11 @@ function parseTaskPriority(value) {
|
|
|
5962
6337
|
return value;
|
|
5963
6338
|
throw usageError(`Invalid Task priority: ${value}.`);
|
|
5964
6339
|
}
|
|
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
|
+
}
|
|
5965
6345
|
function parseTaskTags(value) {
|
|
5966
6346
|
const tags = [...new Set(value.split(",").map((tag) => tag.trim()).filter(Boolean))];
|
|
5967
6347
|
if (tags.length === 0)
|