@zq-silk/yui 0.8.2 → 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 +61 -9
- package/dist/cli/commandCatalog.js +31 -14
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli.js +187 -22
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +648 -74
- package/dist/commands/taskCompletionGate.js +166 -2
- package/dist/commands/taskContextCommand.js +6 -1
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +36 -3
- package/dist/context/runContextPack.js +19 -4
- package/dist/context/sessionBootstrapManifest.js +83 -2
- 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/gitWorkspace.js +7 -4
- package/dist/repository/taskBaseFreshness.js +4 -2
- 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/agentRun.js +2 -2
- 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 +2 -0
- package/dist/storage/sqliteStore.js +10 -1
- package/dist/storage/taskStore.js +12 -1
- package/dist/task/completionReadiness.js +91 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +146 -51
- package/dist/task/publicationReference.js +1 -0
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/web/webSnapshot.js +7 -1
- package/dist/workItem/workItem.js +12 -0
- package/dist/workspace/workItemChangeSetManager.js +2 -1
- package/i18n/README.zh-CN.md +28 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +79 -32
- package/skills/yui-operator/SKILL.md +51 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
- package/skills/yui-runtime/SKILL.md +7 -2
|
@@ -7,7 +7,8 @@ import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageErr
|
|
|
7
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
|
-
import {
|
|
10
|
+
import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
|
|
11
|
+
import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
11
12
|
import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
|
|
12
13
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
13
14
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
@@ -34,14 +35,17 @@ import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coord
|
|
|
34
35
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
35
36
|
import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
36
37
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
37
|
-
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";
|
|
38
39
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
40
|
+
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
39
41
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
40
42
|
import { resolveProject } from "../repository/project.js";
|
|
41
43
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
42
|
-
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
44
|
+
import { currentWorkItemCandidate, governingWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
43
45
|
import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
|
|
44
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";
|
|
45
49
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
46
50
|
import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
|
|
47
51
|
import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
|
|
@@ -50,7 +54,7 @@ import { runTaskContextCommand } from "./taskContextCommand.js";
|
|
|
50
54
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
51
55
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
52
56
|
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
53
|
-
import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
57
|
+
import { assertNoOpenInputRequests, isCurrentGlobalOperator, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
54
58
|
import { runGrantCommand } from "./grantCommands.js";
|
|
55
59
|
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
56
60
|
import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
|
|
@@ -79,17 +83,17 @@ function legacyWorkItemReviewConfig(config) {
|
|
|
79
83
|
return config?.trigger === "final" ? null : config;
|
|
80
84
|
}
|
|
81
85
|
function storedTaskFinalReviewContract(store, taskId) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.`);
|
|
91
96
|
}
|
|
92
|
-
return first;
|
|
93
97
|
}
|
|
94
98
|
/**
|
|
95
99
|
* Resolve one exact Task-local contract before the caller performs any write.
|
|
@@ -97,7 +101,7 @@ function storedTaskFinalReviewContract(store, taskId) {
|
|
|
97
101
|
* acceptance, and completion mutation must present the same verified
|
|
98
102
|
* capability; shared review-config drift is intentionally irrelevant.
|
|
99
103
|
*/
|
|
100
|
-
function taskFinalReviewContractForMutation(store, taskId, options) {
|
|
104
|
+
function taskFinalReviewContractForMutation(store, taskId, options, authorization = {}) {
|
|
101
105
|
const supplied = options.taskFinalReviewContract;
|
|
102
106
|
if (supplied !== undefined) {
|
|
103
107
|
validateTaskFinalReviewContract(supplied);
|
|
@@ -113,6 +117,8 @@ function taskFinalReviewContractForMutation(store, taskId, options) {
|
|
|
113
117
|
if (stored === undefined)
|
|
114
118
|
return supplied;
|
|
115
119
|
if (supplied === undefined) {
|
|
120
|
+
if (authorization.allowStoredWithoutSupplied === true)
|
|
121
|
+
return stored;
|
|
116
122
|
throw usageError(`Task final-review contract is missing for ${taskId}.`);
|
|
117
123
|
}
|
|
118
124
|
if (!sameTaskFinalReviewContract(stored, supplied)) {
|
|
@@ -121,8 +127,8 @@ function taskFinalReviewContractForMutation(store, taskId, options) {
|
|
|
121
127
|
return stored;
|
|
122
128
|
}
|
|
123
129
|
export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
124
|
-
const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote].";
|
|
125
|
-
const parsed = parseTail(args, new Set(["--summary", "--summary-file"]), usage, new Set(["--refresh-remote"]));
|
|
130
|
+
const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote] [--accept-published-tree <publication-id>].";
|
|
131
|
+
const parsed = parseTail(args, new Set(["--summary", "--summary-file", "--accept-published-tree"]), usage, new Set(["--refresh-remote"]));
|
|
126
132
|
exactPositionals(parsed.positionals, 1, usage);
|
|
127
133
|
const inlineSummary = parsed.options.get("--summary");
|
|
128
134
|
const summaryFile = parsed.options.get("--summary-file");
|
|
@@ -130,7 +136,14 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
|
130
136
|
throw usageError(`Specify exactly one of --summary or --summary-file.`, usage);
|
|
131
137
|
}
|
|
132
138
|
const summary = summaryOverride ?? readCommandText(inlineSummary, summaryFile, "--summary", usage);
|
|
133
|
-
|
|
139
|
+
const acceptedPublishedTreePublicationId = parsed.options.get("--accept-published-tree");
|
|
140
|
+
return {
|
|
141
|
+
taskId: parsed.positionals[0],
|
|
142
|
+
summary,
|
|
143
|
+
...(acceptedPublishedTreePublicationId === undefined
|
|
144
|
+
? {}
|
|
145
|
+
: { acceptedPublishedTreePublicationId })
|
|
146
|
+
};
|
|
134
147
|
}
|
|
135
148
|
/**
|
|
136
149
|
* Check every read-only completion blocker before a caller resolves remote
|
|
@@ -144,7 +157,7 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
|
144
157
|
* terminalization precondition before attempting completion. All blockers
|
|
145
158
|
* are reported in one error instead of one per attempt.
|
|
146
159
|
*/
|
|
147
|
-
export function preflightTaskCompletion(taskId, store, options = {}) {
|
|
160
|
+
export function preflightTaskCompletion(taskId, store, options = {}, request = {}) {
|
|
148
161
|
const task = requireTask(store, taskId);
|
|
149
162
|
const actor = taskActor(options, task.id);
|
|
150
163
|
if (task.status === "completed") {
|
|
@@ -157,7 +170,15 @@ export function preflightTaskCompletion(taskId, store, options = {}) {
|
|
|
157
170
|
// Resolve and authenticate the durable Task-local gate before any remote
|
|
158
171
|
// fetch or Integration write. All checks below mirror the transactional
|
|
159
172
|
// completion path, which remains the final CAS fence after reconciliation.
|
|
160
|
-
|
|
173
|
+
// A human/global Operator cannot present the exact managed Leader contract.
|
|
174
|
+
// For the explicit published-tree path only, let that caller authenticate
|
|
175
|
+
// the stored contract far enough to persist an exact authorization fact.
|
|
176
|
+
// The same command must return before any contract-governed completion
|
|
177
|
+
// mutation; the exact Leader later consumes the authorization with the real
|
|
178
|
+
// contract capability.
|
|
179
|
+
const authorizingPublishedTree = request.acceptedPublishedTreePublicationId !== undefined
|
|
180
|
+
&& actor !== "leader";
|
|
181
|
+
const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options, { allowStoredWithoutSupplied: authorizingPublishedTree });
|
|
161
182
|
const activeTaskReview = store.listReviewRounds(task.id).some((round) => ((round.scope ?? "work-item") === "task"
|
|
162
183
|
&& (round.status === "pending" || round.status === "running")));
|
|
163
184
|
// Issue 06: one shared readiness projection enumerates every blocker.
|
|
@@ -344,12 +365,14 @@ function taskProjectCommand(args, store, options) {
|
|
|
344
365
|
return output(`Added Project to ${updated.id}\n`, { task: updated });
|
|
345
366
|
}
|
|
346
367
|
function updateTaskCommand(args, store, options) {
|
|
347
|
-
const optionNames = new Set([
|
|
368
|
+
const optionNames = new Set([
|
|
369
|
+
"--title", "--description", "--priority", "--tags", "--due-at", "--delivery"
|
|
370
|
+
]);
|
|
348
371
|
const flagOptions = new Set([
|
|
349
372
|
"--clear-description", "--clear-priority", "--clear-tags", "--clear-due-at",
|
|
350
373
|
"--require-integration"
|
|
351
374
|
]);
|
|
352
|
-
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].";
|
|
353
376
|
const parsed = parseTail(args, optionNames, usage, flagOptions);
|
|
354
377
|
exactPositionals(parsed.positionals, 1, usage);
|
|
355
378
|
if (parsed.options.size === 0)
|
|
@@ -373,19 +396,42 @@ function updateTaskCommand(args, store, options) {
|
|
|
373
396
|
const tags = parsed.options.has("--tags")
|
|
374
397
|
? parseTaskTags(requiredOption(parsed.options, "--tags"))
|
|
375
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");
|
|
376
407
|
const now = clock(options);
|
|
377
408
|
const result = store.transaction((tx) => {
|
|
378
409
|
const current = requireTask(tx, parsed.positionals[0]);
|
|
379
410
|
if (current.status === "archived")
|
|
380
411
|
throw usageError(`Task is archived: ${current.id}.`);
|
|
381
|
-
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") {
|
|
382
420
|
throw usageError(`Task ${current.id} is completed; use task reopen before enabling integration evidence.`);
|
|
383
421
|
}
|
|
422
|
+
if (enableIntegration && current.requireIntegration !== true) {
|
|
423
|
+
assertTaskDeliveryPromotionEligible(tx, current, options.directTaskMainSnapshot);
|
|
424
|
+
}
|
|
384
425
|
if (parsed.options.size === 1
|
|
385
|
-
&&
|
|
426
|
+
&& enableIntegration
|
|
386
427
|
&& current.requireIntegration === true) {
|
|
387
428
|
return { task: current, integrationState: "already-enabled" };
|
|
388
429
|
}
|
|
430
|
+
if (parsed.options.size === 1
|
|
431
|
+
&& requestedDelivery === "direct"
|
|
432
|
+
&& taskDeliveryPath(current) === "direct") {
|
|
433
|
+
return { task: current, integrationState: "already-direct" };
|
|
434
|
+
}
|
|
389
435
|
const updated = updateTaskMetadata(current, {
|
|
390
436
|
...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
|
|
391
437
|
...(parsed.options.has("--description")
|
|
@@ -400,31 +446,93 @@ function updateTaskCommand(args, store, options) {
|
|
|
400
446
|
...(dueAt === undefined
|
|
401
447
|
? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
|
|
402
448
|
: { dueAt }),
|
|
403
|
-
...(
|
|
449
|
+
...(enableIntegration ? { requireIntegration: true } : {})
|
|
404
450
|
}, now);
|
|
405
451
|
tx.saveTask(updated);
|
|
406
452
|
recordTaskEvent(tx, updated.id, "task.updated", {
|
|
407
453
|
status: updated.status,
|
|
408
|
-
...(parsed.options.has("--require-integration")
|
|
409
|
-
? {
|
|
410
|
-
: {
|
|
454
|
+
...(requestedDelivery === undefined && !parsed.options.has("--require-integration")
|
|
455
|
+
? {}
|
|
456
|
+
: {
|
|
457
|
+
completionEvidence: enableIntegration
|
|
458
|
+
? "integration-required"
|
|
459
|
+
: "direct",
|
|
460
|
+
deliveryPath: taskDeliveryPath(updated)
|
|
461
|
+
})
|
|
411
462
|
}, now);
|
|
412
463
|
enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
|
|
413
464
|
return {
|
|
414
465
|
task: updated,
|
|
415
|
-
integrationState:
|
|
466
|
+
integrationState: enableIntegration
|
|
416
467
|
? "enabled"
|
|
417
|
-
: "
|
|
468
|
+
: requestedDelivery === "direct"
|
|
469
|
+
? "direct"
|
|
470
|
+
: "unchanged"
|
|
418
471
|
};
|
|
419
472
|
});
|
|
420
|
-
if (result.integrationState !== "already-enabled"
|
|
473
|
+
if (result.integrationState !== "already-enabled"
|
|
474
|
+
&& result.integrationState !== "already-direct") {
|
|
421
475
|
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
422
476
|
}
|
|
423
477
|
return result.integrationState === "enabled"
|
|
424
|
-
? `Updated task ${result.task.id}\
|
|
478
|
+
? `Updated task ${result.task.id}\nDelivery: integrated (WorkItem, ChangeSet, and committed Integration required)\n`
|
|
425
479
|
: result.integrationState === "already-enabled"
|
|
426
|
-
? `Task ${result.task.id}
|
|
427
|
-
:
|
|
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
|
+
}
|
|
428
536
|
}
|
|
429
537
|
/** Compatibility helper for call sites that cannot yet handle foreground enter. */
|
|
430
538
|
export function runTaskOutputCommand(args, store, options = {}) {
|
|
@@ -465,13 +573,20 @@ function createTaskCommand(args, store, options) {
|
|
|
465
573
|
notifyMailbox(options.runtime, taskMailbox(created.task.id), created.task.id);
|
|
466
574
|
return output(`Created Draft task ${created.task.id}: ${created.task.title}\n`
|
|
467
575
|
+ `Assigned role: ${created.leader.name}\n`
|
|
576
|
+
+ `Delivery: ${taskDeliveryPath(created.task)}\n`
|
|
468
577
|
+ (created.task.requireIntegration
|
|
469
578
|
? "Completion: WorkItem, ChangeSet, and committed Integration required\n"
|
|
470
|
-
:
|
|
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
|
+
});
|
|
471
586
|
}
|
|
472
587
|
function parseTaskCreation(args, store) {
|
|
473
|
-
const usage = "Task create usage: yui task create <title> [--project <project> ...] [--base <project>=<ref> ...] [--require-integration].";
|
|
474
|
-
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"]));
|
|
475
590
|
exactPositionals(parsed.positionals, 1, usage);
|
|
476
591
|
const projectReferences = parsed.multiOptions.get("--project") ?? [];
|
|
477
592
|
const baseOptions = parsed.multiOptions.get("--base") ?? [];
|
|
@@ -487,6 +602,16 @@ function parseTaskCreation(args, store) {
|
|
|
487
602
|
if (new Set(projects.map(({ id }) => id)).size !== projects.length) {
|
|
488
603
|
throw usageError("A Task cannot bind the same Project more than once.");
|
|
489
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
|
+
}
|
|
490
615
|
const bases = new Map();
|
|
491
616
|
for (const option of baseOptions) {
|
|
492
617
|
const separator = option.indexOf("=");
|
|
@@ -517,7 +642,8 @@ function parseTaskCreation(args, store) {
|
|
|
517
642
|
baseRef: bases.get(project.id) ?? project.developmentBranch
|
|
518
643
|
})),
|
|
519
644
|
defaultProjectIds,
|
|
520
|
-
requireIntegration:
|
|
645
|
+
requireIntegration: requestedDelivery === "integrated"
|
|
646
|
+
|| parsed.options.has("--require-integration")
|
|
521
647
|
};
|
|
522
648
|
}
|
|
523
649
|
function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []) {
|
|
@@ -527,6 +653,7 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
|
|
|
527
653
|
store.saveRole(task.id, leader);
|
|
528
654
|
recordTaskEvent(store, task.id, "task.created", {
|
|
529
655
|
status: task.status,
|
|
656
|
+
deliveryPath: taskDeliveryPath(task),
|
|
530
657
|
...(defaultProjectIds.length === 0
|
|
531
658
|
? {}
|
|
532
659
|
: { defaultProjectIds: defaultProjectIds.join(",") })
|
|
@@ -575,11 +702,16 @@ function showTaskCommand(args, store) {
|
|
|
575
702
|
`Task: ${task.id}`,
|
|
576
703
|
`Title: ${task.title}`,
|
|
577
704
|
`Status: ${task.status}`,
|
|
705
|
+
`Delivery: ${taskDeliveryPath(task)}`,
|
|
578
706
|
...(task.description === undefined ? [] : [`Description: ${task.description}`]),
|
|
579
707
|
...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
|
|
580
708
|
...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
|
|
581
709
|
...(task.dueAt === undefined ? [] : [`Due: ${presentTime(task.dueAt, timeZone)}`]),
|
|
582
|
-
`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"}`,
|
|
583
715
|
...(task.completedAt === undefined ? [] : [`Completed: ${presentTime(task.completedAt, timeZone)}`]),
|
|
584
716
|
...(task.completedBy === undefined ? [] : [`Completed by: ${task.completedBy}`]),
|
|
585
717
|
...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
|
|
@@ -608,7 +740,12 @@ function showTaskCommand(args, store) {
|
|
|
608
740
|
`Created: ${presentTime(task.createdAt, timeZone)}`,
|
|
609
741
|
`Updated: ${presentTime(task.updatedAt, timeZone)}`
|
|
610
742
|
].join("\n").concat("\n");
|
|
611
|
-
return output(rendered, {
|
|
743
|
+
return output(rendered, {
|
|
744
|
+
task,
|
|
745
|
+
deliveryPath: taskDeliveryPath(task),
|
|
746
|
+
counts,
|
|
747
|
+
hasBrief: brief !== null
|
|
748
|
+
});
|
|
612
749
|
}
|
|
613
750
|
function activateTaskCommand(args, store, options) {
|
|
614
751
|
exactPositionals(args, 1, "Task activate usage: yui task activate <task>.");
|
|
@@ -647,17 +784,47 @@ function completeTaskCommand(args, store, options) {
|
|
|
647
784
|
const summary = request.summary;
|
|
648
785
|
const now = clock(options);
|
|
649
786
|
const result = store.transaction((tx) => {
|
|
650
|
-
const preflight = preflightTaskCompletion(request.taskId, tx, options);
|
|
787
|
+
const preflight = preflightTaskCompletion(request.taskId, tx, options, request);
|
|
651
788
|
const { task, actor } = preflight;
|
|
652
789
|
if (preflight.completed) {
|
|
653
790
|
return {
|
|
654
791
|
task,
|
|
655
792
|
changed: false,
|
|
656
793
|
runtimeCleanupTargets: [],
|
|
657
|
-
|
|
794
|
+
completionAdvisories: [],
|
|
795
|
+
finalReview: undefined,
|
|
796
|
+
publishedTreeAuthorization: undefined
|
|
658
797
|
};
|
|
659
798
|
}
|
|
660
799
|
const taskFinalContract = preflight.taskFinalReviewContract;
|
|
800
|
+
const actualTaskCandidate = task.projectBindings.length === 0
|
|
801
|
+
? undefined
|
|
802
|
+
: actualTaskReviewCandidateForMutation(tx, task, options);
|
|
803
|
+
const publishedTreeProof = request.acceptedPublishedTreePublicationId === undefined
|
|
804
|
+
? undefined
|
|
805
|
+
: assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskCandidate);
|
|
806
|
+
const requiresContractHandoff = publishedTreeProof !== undefined
|
|
807
|
+
&& taskFinalContract !== undefined;
|
|
808
|
+
if (requiresContractHandoff && actor !== "leader") {
|
|
809
|
+
const existing = matchingPublishedTreeAuthorization(tx, publishedTreeProof);
|
|
810
|
+
const event = existing ?? recordTaskEventRecord(tx, task.id, TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT, publishedTreeAuthorizationPayload(actor, publishedTreeProof), now);
|
|
811
|
+
enqueueWork(tx, leaderMailbox(task.id), wakeReason("published-tree-authorized", event.id), now, [eventRef(task.id, event.id)]);
|
|
812
|
+
return {
|
|
813
|
+
task,
|
|
814
|
+
changed: false,
|
|
815
|
+
runtimeCleanupTargets: [],
|
|
816
|
+
completionAdvisories: [],
|
|
817
|
+
finalReview: undefined,
|
|
818
|
+
publishedTreeAuthorization: {
|
|
819
|
+
event,
|
|
820
|
+
proof: publishedTreeProof,
|
|
821
|
+
created: existing === undefined
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
if (publishedTreeProof !== undefined && actor === "leader") {
|
|
826
|
+
requirePublishedTreeAuthorization(tx, publishedTreeProof);
|
|
827
|
+
}
|
|
661
828
|
const roles = tx.listRoles(task.id);
|
|
662
829
|
const activeRuns = roles
|
|
663
830
|
.map((role) => ({ role, run: tx.getActiveAgentRun(task.id, role.name) }))
|
|
@@ -704,9 +871,11 @@ function completeTaskCommand(args, store, options) {
|
|
|
704
871
|
task,
|
|
705
872
|
changed: false,
|
|
706
873
|
runtimeCleanupTargets: [],
|
|
874
|
+
completionAdvisories: [],
|
|
707
875
|
finalReview,
|
|
708
876
|
resumedPendingFinalReview: pendingFinalReviewIds.has(finalReview.id),
|
|
709
|
-
terminalizedLeaderRun
|
|
877
|
+
terminalizedLeaderRun,
|
|
878
|
+
publishedTreeAuthorization: undefined
|
|
710
879
|
};
|
|
711
880
|
}
|
|
712
881
|
// Issue 06: re-validate the full completion readiness inside the
|
|
@@ -729,7 +898,34 @@ function completeTaskCommand(args, store, options) {
|
|
|
729
898
|
tx.clearOperatorNotification(task.id);
|
|
730
899
|
tx.saveOperatorNotification(createTaskTerminalNotification(task.id, "completed", actor, summary, now));
|
|
731
900
|
enqueueWork(tx, { kind: "operator" }, "task-terminal", now, [taskRef(task.id)]);
|
|
732
|
-
|
|
901
|
+
if (publishedTreeProof !== undefined) {
|
|
902
|
+
recordTaskEvent(tx, task.id, "task.completion-published-tree-accepted", {
|
|
903
|
+
by: actor,
|
|
904
|
+
projectId: publishedTreeProof.projectId,
|
|
905
|
+
publicationId: publishedTreeProof.publicationId,
|
|
906
|
+
...(publishedTreeProof.reviewRoundId === undefined
|
|
907
|
+
? {}
|
|
908
|
+
: { reviewRoundId: publishedTreeProof.reviewRoundId }),
|
|
909
|
+
localCommit: publishedTreeProof.localCommit,
|
|
910
|
+
remoteCommit: publishedTreeProof.remoteCommit,
|
|
911
|
+
tree: publishedTreeProof.tree
|
|
912
|
+
}, now);
|
|
913
|
+
}
|
|
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);
|
|
733
929
|
// A terminal Task must never leave a Task-lane signal that can wake it.
|
|
734
930
|
// The durable records remain intact; only the derived mailbox work is
|
|
735
931
|
// discarded at this lifecycle boundary.
|
|
@@ -745,11 +941,24 @@ function completeTaskCommand(args, store, options) {
|
|
|
745
941
|
task: completed,
|
|
746
942
|
changed: true,
|
|
747
943
|
runtimeCleanupTargets,
|
|
944
|
+
completionAdvisories: readiness.advisories,
|
|
748
945
|
finalReview: undefined,
|
|
749
946
|
resumedPendingFinalReview: false,
|
|
750
|
-
terminalizedLeaderRun
|
|
947
|
+
terminalizedLeaderRun,
|
|
948
|
+
publishedTreeAuthorization: undefined
|
|
751
949
|
};
|
|
752
950
|
});
|
|
951
|
+
if (result.publishedTreeAuthorization !== undefined) {
|
|
952
|
+
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
953
|
+
const authorization = result.publishedTreeAuthorization;
|
|
954
|
+
return output(authorization.created
|
|
955
|
+
? `Authorized published-tree completion for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`
|
|
956
|
+
: `Published-tree completion is already authorized for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`, {
|
|
957
|
+
task: result.task,
|
|
958
|
+
authorizationEvent: authorization.event,
|
|
959
|
+
publishedTreeProof: authorization.proof
|
|
960
|
+
});
|
|
961
|
+
}
|
|
753
962
|
if (result.changed) {
|
|
754
963
|
for (const target of result.runtimeCleanupTargets) {
|
|
755
964
|
// Cleanup owns an independent lifecycle lane. Never fall back to the
|
|
@@ -769,9 +978,17 @@ function completeTaskCommand(args, store, options) {
|
|
|
769
978
|
[TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW]: result.terminalizedLeaderRun
|
|
770
979
|
});
|
|
771
980
|
}
|
|
772
|
-
|
|
981
|
+
const completionOutput = result.changed
|
|
773
982
|
? `Completed task ${result.task.id}\n`
|
|
774
|
-
: `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
|
+
});
|
|
775
992
|
}
|
|
776
993
|
function reopenTaskCommand(args, store, options) {
|
|
777
994
|
exactPositionals(args, 1, "Task reopen usage: yui task reopen <id>.");
|
|
@@ -2606,8 +2823,12 @@ function reviewWork(args, store, options) {
|
|
|
2606
2823
|
*/
|
|
2607
2824
|
function taskReviewCommand(args, store, options) {
|
|
2608
2825
|
const [command, ...rest] = args;
|
|
2826
|
+
if (command === "rebind")
|
|
2827
|
+
return rebindTaskFinalReviewContract(rest, store, options);
|
|
2609
2828
|
if (command === "request")
|
|
2610
2829
|
return requestTaskReviewRound(rest, store, options);
|
|
2830
|
+
if (command === "force-fresh")
|
|
2831
|
+
return forceFreshTaskReviewRound(rest, store, options);
|
|
2611
2832
|
if (command === "retry")
|
|
2612
2833
|
return retryFailedTaskReviewRound(rest, store, options);
|
|
2613
2834
|
if (command === "group")
|
|
@@ -2618,6 +2839,115 @@ function taskReviewCommand(args, store, options) {
|
|
|
2618
2839
|
? "Task review command is required."
|
|
2619
2840
|
: `Unknown command: task review ${command}`);
|
|
2620
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
|
+
}
|
|
2621
2951
|
function resolveReviewExecutionGroup(args, store, options) {
|
|
2622
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> ...].";
|
|
2623
2953
|
if (args[0] !== "resolve")
|
|
@@ -2712,8 +3042,8 @@ function resolveReviewExecutionGroup(args, store, options) {
|
|
|
2712
3042
|
/**
|
|
2713
3043
|
* Issue 06: `yui task review finding` — the cross-Round finding ledger CLI.
|
|
2714
3044
|
* Findings are extracted automatically from completed Rounds; these commands
|
|
2715
|
-
* let the Leader inspect the ledger, disposition each finding, and plan
|
|
2716
|
-
*
|
|
3045
|
+
* let the Leader inspect the ledger, disposition each finding, and plan one
|
|
3046
|
+
* convergent repair unit by default. Parallel fan-out is explicit.
|
|
2717
3047
|
*/
|
|
2718
3048
|
function reviewFindingCommand(args, store, options) {
|
|
2719
3049
|
const [command, ...rest] = args;
|
|
@@ -2784,11 +3114,15 @@ function disposeReviewFindingCommand(args, store, options) {
|
|
|
2784
3114
|
return output(`Dispositioned ${result.id} as ${result.disposition}.\n`);
|
|
2785
3115
|
}
|
|
2786
3116
|
function planReviewRepairWave(args, store, options) {
|
|
2787
|
-
const usage = "Task review finding repair-wave usage: yui task review finding repair-wave <task> [--create].";
|
|
2788
|
-
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"]));
|
|
2789
3119
|
exactPositionals(parsed.positionals, 1, usage);
|
|
2790
3120
|
const task = requireTask(store, parsed.positionals[0]);
|
|
2791
|
-
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);
|
|
2792
3126
|
if (groups.length === 0) {
|
|
2793
3127
|
return output(`No open P1/P2 findings need repair for ${task.id}.\n`);
|
|
2794
3128
|
}
|
|
@@ -2829,7 +3163,7 @@ function planReviewRepairWave(args, store, options) {
|
|
|
2829
3163
|
});
|
|
2830
3164
|
const lines = created.map(({ group, item, changed }) => (`wave ${group.groupKey}: ${item.id} ${changed ? "created" : "already open"} `
|
|
2831
3165
|
+ `(${group.findingIds.join(", ")})`));
|
|
2832
|
-
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 });
|
|
2833
3167
|
}
|
|
2834
3168
|
const lines = groups.map((group, index) => {
|
|
2835
3169
|
const findings = group.findings
|
|
@@ -2838,7 +3172,24 @@ function planReviewRepairWave(args, store, options) {
|
|
|
2838
3172
|
return `wave ${index + 1}: ${findings}`
|
|
2839
3173
|
+ ` (paths: ${group.affectedPaths.join(", ") || "none"}; invariants: ${group.invariants.join(", ")})`;
|
|
2840
3174
|
});
|
|
2841
|
-
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
|
+
}];
|
|
2842
3193
|
}
|
|
2843
3194
|
function reviewRepairProjectIds(task, affectedPaths) {
|
|
2844
3195
|
const bindings = task.projectBindings;
|
|
@@ -3078,6 +3429,156 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3078
3429
|
: `Task-final delta-recheck requested as ${round.id} (rechecks ${round.deltaRecheck.previousReviewRoundId})\n`
|
|
3079
3430
|
: `Task-final Review is already ${round.status}: ${round.id}\n`, { reviewRound: round });
|
|
3080
3431
|
}
|
|
3432
|
+
const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
|
|
3433
|
+
/**
|
|
3434
|
+
* Creates a distinct full Task-final ReviewRound only when the exact previous
|
|
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.
|
|
3438
|
+
*/
|
|
3439
|
+
function forceFreshTaskReviewRound(args, store, options) {
|
|
3440
|
+
const usage = "Task review force-fresh usage: yui task review force-fresh <task>/<review-round>.";
|
|
3441
|
+
exactPositionals(args, 1, usage);
|
|
3442
|
+
const reference = taskRecordReference(args[0], "reviewRound", "ReviewRound reference", options);
|
|
3443
|
+
const now = clock(options);
|
|
3444
|
+
const result = store.transaction((tx) => {
|
|
3445
|
+
const source = tx.getReviewRound(reference.taskId, reference.localId);
|
|
3446
|
+
if (source === null) {
|
|
3447
|
+
throw dataError(`ReviewRound not found: ${reference.taskId}/${reference.localId}.`);
|
|
3448
|
+
}
|
|
3449
|
+
const task = requireTask(tx, reference.taskId);
|
|
3450
|
+
if (task.status !== "active")
|
|
3451
|
+
throw usageError(`Task is not active: ${task.id}.`);
|
|
3452
|
+
if (taskActor(options, task.id) !== "leader") {
|
|
3453
|
+
throw usageError("Only the Task Leader may force a fresh Task-final ReviewRound.");
|
|
3454
|
+
}
|
|
3455
|
+
if ((source.scope ?? "work-item") !== "task") {
|
|
3456
|
+
throw usageError(`ReviewRound ${source.id} is not a Task-final ReviewRound.`);
|
|
3457
|
+
}
|
|
3458
|
+
const replacementEvents = tx.listEvents(task.id).filter((event) => (event.type === TASK_FINAL_FORCE_FRESH_EVENT
|
|
3459
|
+
&& event.payload.sourceReviewRoundId === source.id));
|
|
3460
|
+
if (replacementEvents.length > 1) {
|
|
3461
|
+
throw dataError(`ReviewRound ${source.id} has duplicate force-fresh audit events.`);
|
|
3462
|
+
}
|
|
3463
|
+
const replacementEvent = replacementEvents[0];
|
|
3464
|
+
if (replacementEvent !== undefined) {
|
|
3465
|
+
const replacementId = replacementEvent.payload.reviewRoundId;
|
|
3466
|
+
const replacement = replacementId === undefined
|
|
3467
|
+
? null
|
|
3468
|
+
: tx.getReviewRound(task.id, replacementId);
|
|
3469
|
+
if (replacement === null
|
|
3470
|
+
|| replacement.id === source.id
|
|
3471
|
+
|| (replacement.scope ?? "work-item") !== "task"
|
|
3472
|
+
|| replacement.workItemId !== source.workItemId
|
|
3473
|
+
|| replacement.candidateId !== source.candidateId
|
|
3474
|
+
|| replacement.reviewerRoleName !== source.reviewerRoleName
|
|
3475
|
+
|| replacement.deltaRecheck !== undefined
|
|
3476
|
+
|| !sameTaskFinalReviewContract(replacement.taskFinalReviewContract, source.taskFinalReviewContract)
|
|
3477
|
+
|| !isSameTaskReviewCandidate(replacement.taskCandidate, source.taskCandidate)
|
|
3478
|
+
|| replacementEvent.payload.workItemId !== replacement.workItemId
|
|
3479
|
+
|| replacementEvent.payload.candidateId !== replacement.candidateId
|
|
3480
|
+
|| replacementEvent.payload.reviewerRoleName !== replacement.reviewerRoleName
|
|
3481
|
+
|| replacementEvent.payload.taskCandidate !== JSON.stringify(replacement.taskCandidate)) {
|
|
3482
|
+
throw dataError(`Force-fresh audit for ${source.id} does not match its replacement Round.`);
|
|
3483
|
+
}
|
|
3484
|
+
return { round: replacement, source, created: false };
|
|
3485
|
+
}
|
|
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}`);
|
|
3489
|
+
}
|
|
3490
|
+
if (source.taskCandidate === undefined) {
|
|
3491
|
+
throw dataError(`ReviewRound ${source.id} has no frozen Task candidate.`);
|
|
3492
|
+
}
|
|
3493
|
+
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
3494
|
+
if (!sameTaskFinalReviewContract(source.taskFinalReviewContract, taskFinalContract)) {
|
|
3495
|
+
throw usageError(`Task final-review contract does not match ReviewRound ${source.id}.`);
|
|
3496
|
+
}
|
|
3497
|
+
if (source.executionGroup !== undefined
|
|
3498
|
+
&& (source.executionGroup.strategy.mode !== "fixed"
|
|
3499
|
+
|| source.executionGroup.strategy.count !== 1
|
|
3500
|
+
|| source.executionGroup.lanes.length !== 1
|
|
3501
|
+
|| source.executionGroup.lanes[0].roleName !== source.reviewerRoleName)) {
|
|
3502
|
+
throw usageError(`ReviewRound ${source.id} is not a single-Reviewer full Review; force-fresh is refused.`);
|
|
3503
|
+
}
|
|
3504
|
+
const item = tx.getWorkItem(task.id, source.workItemId);
|
|
3505
|
+
const candidate = item?.candidates.find(({ id }) => id === source.candidateId);
|
|
3506
|
+
if (item === null || item === undefined || candidate === undefined) {
|
|
3507
|
+
throw dataError(`Final Review anchor Candidate is no longer available: `
|
|
3508
|
+
+ `${source.workItemId}/${source.candidateId}.`);
|
|
3509
|
+
}
|
|
3510
|
+
const provenance = taskReviewProvenance(tx, task, options);
|
|
3511
|
+
if (!isSameTaskReviewCandidate(source.taskCandidate, provenance.candidate)) {
|
|
3512
|
+
throw usageError(`Final ReviewRound ${source.id} freezes a candidate that is no longer the current Task candidate.`);
|
|
3513
|
+
}
|
|
3514
|
+
const producerCollision = taskReviewProducerCollision(provenance, source.reviewerRoleName);
|
|
3515
|
+
if (producerCollision !== null)
|
|
3516
|
+
throw usageError(producerCollision);
|
|
3517
|
+
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
|
|
3518
|
+
.filter((entry) => (entry.scope ?? "work-item") === "task"));
|
|
3519
|
+
const sourceIndex = taskRounds.findIndex(({ id }) => id === source.id);
|
|
3520
|
+
if (sourceIndex < 0) {
|
|
3521
|
+
throw dataError(`Final ReviewRound is not in Task history: ${source.id}.`);
|
|
3522
|
+
}
|
|
3523
|
+
const laterRound = taskRounds.slice(sourceIndex + 1).at(-1);
|
|
3524
|
+
if (laterRound !== undefined) {
|
|
3525
|
+
throw usageError(`A newer Task-final ReviewRound already exists after ${source.id}: `
|
|
3526
|
+
+ `${laterRound.id}/${laterRound.status}.`);
|
|
3527
|
+
}
|
|
3528
|
+
assertNoConflictingTaskReviewRound(taskRounds, source.id);
|
|
3529
|
+
assertTaskReviewRequestLane(tx, task.id, source.reviewerRoleName);
|
|
3530
|
+
let reviewer = tx.getRole(task.id, source.reviewerRoleName);
|
|
3531
|
+
if (reviewer === null) {
|
|
3532
|
+
if (tx.getGlobalRole(source.reviewerRoleName) === null) {
|
|
3533
|
+
throw usageError(`Global Role not found: ${source.reviewerRoleName}.`);
|
|
3534
|
+
}
|
|
3535
|
+
reviewer = createTaskRole(tx, task, source.reviewerRoleName, undefined, now, source.reviewerRoleName);
|
|
3536
|
+
tx.saveRole(task.id, reviewer);
|
|
3537
|
+
}
|
|
3538
|
+
let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.workItemId, source.candidateId, source.reviewerRoleName, "leader", source.taskCandidate, now, taskFinalContract);
|
|
3539
|
+
const group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
|
|
3540
|
+
purpose: "review",
|
|
3541
|
+
target: executionTargetForReviewRound(task, created, item, candidate),
|
|
3542
|
+
strategy: { mode: "fixed", count: 1 },
|
|
3543
|
+
lanes: [{ roleName: reviewer.name, reviewRoundId: created.id }]
|
|
3544
|
+
}, now);
|
|
3545
|
+
created = { ...created, executionGroup: group };
|
|
3546
|
+
tx.saveReviewRound(task.id, created);
|
|
3547
|
+
recordTaskEvent(tx, task.id, TASK_FINAL_FORCE_FRESH_EVENT, {
|
|
3548
|
+
sourceReviewRoundId: source.id,
|
|
3549
|
+
...(source.reviewerRunId === undefined ? {} : { sourceReviewerRunId: source.reviewerRunId }),
|
|
3550
|
+
reviewRoundId: created.id,
|
|
3551
|
+
workItemId: created.workItemId,
|
|
3552
|
+
candidateId: created.candidateId,
|
|
3553
|
+
reviewerRoleName: created.reviewerRoleName,
|
|
3554
|
+
taskCandidate: JSON.stringify(created.taskCandidate),
|
|
3555
|
+
reason: "source-round-terminal-without-semantic-review",
|
|
3556
|
+
leaderActionRunId: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader"
|
|
3557
|
+
}, now);
|
|
3558
|
+
return { round: created, source, created: true };
|
|
3559
|
+
});
|
|
3560
|
+
return output(result.created
|
|
3561
|
+
? `Fresh Task-final Review requested as ${result.round.id} after ${result.source.id}\n`
|
|
3562
|
+
: `Fresh Task-final Review already requested as ${result.round.id} after ${result.source.id}\n`, { reviewRound: result.round, sourceReviewRound: result.source });
|
|
3563
|
+
}
|
|
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
|
+
};
|
|
3581
|
+
}
|
|
3081
3582
|
/**
|
|
3082
3583
|
* Issue 07: re-validates the CLI-computed delta preflight inside the store
|
|
3083
3584
|
* transaction. The previous Round must be a completed acceptance (a full
|
|
@@ -3092,8 +3593,8 @@ function validateDeltaRecheckRequest(store, taskId, reviewerRoleName, candidate,
|
|
|
3092
3593
|
const previous = store.getReviewRound(taskId, preflight.record.previousReviewRoundId);
|
|
3093
3594
|
if (previous === null
|
|
3094
3595
|
|| (previous.scope ?? "work-item") !== "task"
|
|
3095
|
-
|| previous
|
|
3096
|
-
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: `
|
|
3097
3598
|
+ `${preflight.record.previousReviewRoundId}.`);
|
|
3098
3599
|
}
|
|
3099
3600
|
if (previous.reviewerRoleName !== reviewerRoleName) {
|
|
@@ -3307,13 +3808,16 @@ function taskRunCommand(args, store, options) {
|
|
|
3307
3808
|
function runContextCommand(args, store, options) {
|
|
3308
3809
|
const [first, ...rest] = args;
|
|
3309
3810
|
if (first === "expand") {
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3811
|
+
const usage = "Task run context expand usage: yui task run context expand <task>/<run> <ref-id> [--store <store>] [--mode full].";
|
|
3812
|
+
const parsed = parseTail(rest, new Set(["--store", "--mode"]), usage);
|
|
3813
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
3814
|
+
const mode = parsed.options.get("--mode");
|
|
3815
|
+
if (mode !== undefined && mode !== "full") {
|
|
3816
|
+
throw usageError("Run Context expansion mode must be full.", usage);
|
|
3313
3817
|
}
|
|
3314
|
-
const { taskId, runId } = parseRunContextReference(
|
|
3818
|
+
const { taskId, runId } = parseRunContextReference(parsed.positionals[0]);
|
|
3315
3819
|
authorizeRunContext(store, taskId, runId, options.environment);
|
|
3316
|
-
const expanded = store.transaction((tx) => expandRunContextRef(tx, taskId, runId,
|
|
3820
|
+
const expanded = store.transaction((tx) => expandRunContextRef(tx, taskId, runId, parsed.positionals[1], optionalNonEmptyOption(parsed.options, "--store")));
|
|
3317
3821
|
return output(`${JSON.stringify(expanded, null, 2)}\n`, { context: expanded });
|
|
3318
3822
|
}
|
|
3319
3823
|
if (first === "delta") {
|
|
@@ -3706,7 +4210,7 @@ function retryRun(args, store, options) {
|
|
|
3706
4210
|
}
|
|
3707
4211
|
function actualTaskReviewCandidateForMutation(store, task, options) {
|
|
3708
4212
|
if (options.actualTaskReviewCandidate === undefined) {
|
|
3709
|
-
throw usageError(`Actual Task Project heads were not verified for
|
|
4213
|
+
throw usageError(`Actual Task Project heads were not verified for delivery: ${task.id}.`);
|
|
3710
4214
|
}
|
|
3711
4215
|
let actual;
|
|
3712
4216
|
try {
|
|
@@ -3922,8 +4426,8 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
3922
4426
|
// Any Task-final ReviewRound is durable completion evidence/obligation.
|
|
3923
4427
|
// Once one exists, later changes to the mutable global review config cannot
|
|
3924
4428
|
// weaken the requirement or change its reviewer. Before the first such
|
|
3925
|
-
// Round, the current global `final` config
|
|
3926
|
-
//
|
|
4429
|
+
// Round, the current global `final` config establishes the initial Round
|
|
4430
|
+
// only for integrated delivery.
|
|
3927
4431
|
const taskRounds = reviewRoundsByIdentity(store.listReviewRounds(task.id))
|
|
3928
4432
|
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
3929
4433
|
&& (taskFinalContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract))));
|
|
@@ -3934,7 +4438,15 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
3934
4438
|
}
|
|
3935
4439
|
else if (establishedRound === undefined) {
|
|
3936
4440
|
const globalConfig = store.getReviewConfig();
|
|
3937
|
-
|
|
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;
|
|
3938
4450
|
}
|
|
3939
4451
|
else {
|
|
3940
4452
|
config = { roleName: establishedRound.reviewerRoleName, trigger: "final" };
|
|
@@ -3979,7 +4491,10 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
3979
4491
|
return escalated;
|
|
3980
4492
|
}
|
|
3981
4493
|
else {
|
|
3982
|
-
return latest.status === "completed"
|
|
4494
|
+
return latest.status === "completed"
|
|
4495
|
+
&& classifyReviewRoundOutcome(latest, store)?.kind === "semantic"
|
|
4496
|
+
? null
|
|
4497
|
+
: latest;
|
|
3983
4498
|
}
|
|
3984
4499
|
}
|
|
3985
4500
|
const anchor = taskFinalContract === undefined
|
|
@@ -4051,15 +4566,20 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
4051
4566
|
}
|
|
4052
4567
|
}
|
|
4053
4568
|
function latestTaskReviewContractAnchor(store, task, taskFinalContract) {
|
|
4054
|
-
const
|
|
4055
|
-
.
|
|
4056
|
-
|
|
4569
|
+
const anchor = store.listWorkItems(task.id)
|
|
4570
|
+
.flatMap((item) => {
|
|
4571
|
+
const candidate = governingWorkItemCandidate(item);
|
|
4572
|
+
return candidate !== undefined && sameTaskFinalReviewContract(candidate.taskFinalReviewContract, taskFinalContract)
|
|
4573
|
+
? [{ item, candidate }]
|
|
4574
|
+
: [];
|
|
4575
|
+
})
|
|
4576
|
+
.sort((left, right) => (left.item.updatedAt.localeCompare(right.item.updatedAt)
|
|
4577
|
+
|| left.item.id.localeCompare(right.item.id)))
|
|
4057
4578
|
.at(-1);
|
|
4058
|
-
|
|
4059
|
-
if (item === undefined || candidate === undefined) {
|
|
4579
|
+
if (anchor === undefined) {
|
|
4060
4580
|
throw usageError(`Task ${task.id} has no WorkItem Candidate to anchor its final Review.`);
|
|
4061
4581
|
}
|
|
4062
|
-
return
|
|
4582
|
+
return anchor;
|
|
4063
4583
|
}
|
|
4064
4584
|
/**
|
|
4065
4585
|
* Leader-only retry of an exact failed Task-final review Run. The old failed
|
|
@@ -5095,8 +5615,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5095
5615
|
let deltaContext = "";
|
|
5096
5616
|
if (taskScope && round.deltaRecheck !== undefined) {
|
|
5097
5617
|
const previousRound = tx.getReviewRound(taskId, round.deltaRecheck.previousReviewRoundId);
|
|
5098
|
-
if (previousRound === null || previousRound
|
|
5099
|
-
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}.`);
|
|
5100
5620
|
}
|
|
5101
5621
|
const diffByProject = options.deltaRecheckDiff;
|
|
5102
5622
|
if (diffByProject === undefined) {
|
|
@@ -5379,6 +5899,52 @@ function recordTaskEventRecord(store, taskId, type, payload, now) {
|
|
|
5379
5899
|
store.saveEvent(taskId, event);
|
|
5380
5900
|
return event;
|
|
5381
5901
|
}
|
|
5902
|
+
function publishedTreeAuthorizationPayload(actor, proof) {
|
|
5903
|
+
return {
|
|
5904
|
+
by: actor,
|
|
5905
|
+
projectId: proof.projectId,
|
|
5906
|
+
publicationId: proof.publicationId,
|
|
5907
|
+
...(proof.reviewRoundId === undefined
|
|
5908
|
+
? { reviewAnchor: publishedTreeReviewAnchor(proof) }
|
|
5909
|
+
: { reviewRoundId: proof.reviewRoundId }),
|
|
5910
|
+
localCommit: proof.localCommit,
|
|
5911
|
+
remoteCommit: proof.remoteCommit,
|
|
5912
|
+
tree: proof.tree
|
|
5913
|
+
};
|
|
5914
|
+
}
|
|
5915
|
+
function matchingPublishedTreeAuthorization(store, proof) {
|
|
5916
|
+
const events = store.listEvents(proof.taskId);
|
|
5917
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
5918
|
+
const event = events[index];
|
|
5919
|
+
if (event.type === "task.completed" || event.type === "task.reopened")
|
|
5920
|
+
return undefined;
|
|
5921
|
+
if (event.type === TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT
|
|
5922
|
+
&& (event.payload.by === "user" || event.payload.by === "operator")
|
|
5923
|
+
&& event.payload.projectId === proof.projectId
|
|
5924
|
+
&& event.payload.publicationId === proof.publicationId
|
|
5925
|
+
&& (event.payload.reviewAnchor ?? event.payload.reviewRoundId)
|
|
5926
|
+
=== publishedTreeReviewAnchor(proof)
|
|
5927
|
+
&& event.payload.localCommit === proof.localCommit
|
|
5928
|
+
&& event.payload.remoteCommit === proof.remoteCommit
|
|
5929
|
+
&& event.payload.tree === proof.tree) {
|
|
5930
|
+
return event;
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
return undefined;
|
|
5934
|
+
}
|
|
5935
|
+
function requirePublishedTreeAuthorization(store, proof) {
|
|
5936
|
+
const authorization = matchingPublishedTreeAuthorization(store, proof);
|
|
5937
|
+
if (authorization === undefined) {
|
|
5938
|
+
throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
|
|
5939
|
+
+ `${proof.taskId}/${proof.publicationId} at ${proof.reviewRoundId === undefined
|
|
5940
|
+
? "the direct Task-main head"
|
|
5941
|
+
: `Task-final Review ${proof.reviewRoundId}`}.`);
|
|
5942
|
+
}
|
|
5943
|
+
return authorization;
|
|
5944
|
+
}
|
|
5945
|
+
function publishedTreeReviewAnchor(proof) {
|
|
5946
|
+
return proof.reviewRoundId ?? "direct";
|
|
5947
|
+
}
|
|
5382
5948
|
/** Keeps a free-text run-fact note bounded so an event payload stays compact. */
|
|
5383
5949
|
function truncateEventNote(note) {
|
|
5384
5950
|
const normalized = note.trim();
|
|
@@ -5639,6 +6205,11 @@ function parseTaskPriority(value) {
|
|
|
5639
6205
|
return value;
|
|
5640
6206
|
throw usageError(`Invalid Task priority: ${value}.`);
|
|
5641
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
|
+
}
|
|
5642
6213
|
function parseTaskTags(value) {
|
|
5643
6214
|
const tags = [...new Set(value.split(",").map((tag) => tag.trim()).filter(Boolean))];
|
|
5644
6215
|
if (tags.length === 0)
|
|
@@ -6327,6 +6898,9 @@ function workItemRef(taskId, id) {
|
|
|
6327
6898
|
function messageRef(taskId, id) {
|
|
6328
6899
|
return { type: "message", taskId, id };
|
|
6329
6900
|
}
|
|
6901
|
+
function eventRef(taskId, id) {
|
|
6902
|
+
return { type: "event", taskId, id };
|
|
6903
|
+
}
|
|
6330
6904
|
function notifyMailbox(runtime, target, compatibilityTaskId) {
|
|
6331
6905
|
if (runtime?.notifyMailboxChanged !== undefined) {
|
|
6332
6906
|
runtime.notifyMailboxChanged(target);
|