@zq-silk/yui 0.8.1 → 0.8.3
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 +27 -28
- package/README.md +57 -46
- package/dist/cli/commandCatalog.js +57 -17
- package/dist/cli/interactionPolicy.js +4 -10
- package/dist/cli/invocationRouter.js +2 -1
- package/dist/cli.js +106 -27
- package/dist/commands/taskCommands.js +458 -77
- package/dist/commands/taskCompletionGate.js +152 -0
- package/dist/context/runContextPack.js +19 -4
- package/dist/context/sessionBootstrapManifest.js +1 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +389 -70
- package/dist/controller/resourceInventory.js +9 -5
- package/dist/controller/runtime.js +80 -7
- package/dist/controller/runtimeLaunchCoordinator.js +18 -78
- package/dist/controller/structuredProviderObservation.js +273 -0
- package/dist/executor/agentAdapter.js +40 -0
- package/dist/executor/agentExecutor.js +31 -7
- package/dist/executor/executorRegistry.js +11 -49
- package/dist/executor/fileRoleLaunchPlanner.js +115 -37
- package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
- package/dist/repository/gitWorkspace.js +7 -4
- package/dist/repository/taskBaseFreshness.js +4 -2
- package/dist/run/agentRun.js +4 -4
- package/dist/runtime/agentHost.js +767 -158
- package/dist/runtime/builtinAgentDrivers.js +1 -5
- package/dist/runtime/codexAppServerRuntime.js +67 -60
- package/dist/runtime/exactControlPlane.js +7 -2
- package/dist/runtime/index.js +6 -2
- package/dist/runtime/launchBroker.js +30 -8
- package/dist/runtime/providerAuthorityFence.js +24 -0
- package/dist/runtime/providerControl.js +63 -0
- package/dist/runtime/providerRecoveryDecision.js +55 -0
- package/dist/runtime/providerRuntimeIdentity.js +269 -19
- package/dist/runtime/runtimeBinding.js +20 -11
- package/dist/runtime/structuredProviderHost.js +476 -0
- package/dist/runtime/tmuxAdapters.js +143 -42
- package/dist/scheduler/activeRoleRunDelivery.js +206 -120
- package/dist/scheduler/leaderWakeupProcessor.js +141 -16
- package/dist/scheduler/wakeReason.js +1 -0
- package/dist/storage/migration/productionRegistry.js +111 -0
- package/dist/storage/sqliteStore.js +2 -0
- package/dist/storage/taskStore.js +3 -1
- package/dist/task/completionReadiness.js +43 -0
- package/dist/task/nextAction.js +6 -4
- package/dist/task/publicationReference.js +1 -0
- package/dist/tmux/tmuxManager.js +1 -1
- package/dist/workItem/workItem.js +12 -0
- package/dist/workspace/workItemChangeSetManager.js +2 -1
- package/i18n/README.zh-CN.md +11 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -3
- package/skills/yui-runtime/SKILL.md +7 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { createRunAssignment } from "../context/runContextContract.js";
|
|
4
4
|
import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeRunContextSnapshot } from "../context/runContextPack.js";
|
|
@@ -7,7 +7,9 @@ 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, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
12
|
+
import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
|
|
11
13
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
12
14
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
13
15
|
import { formatTimestamp } from "../output/timePresentation.js";
|
|
@@ -16,7 +18,7 @@ import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js
|
|
|
16
18
|
import { cancelInputRequest } from "../input/inputRequest.js";
|
|
17
19
|
import { recoverExactAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
|
|
18
20
|
import { resetTaskRoleSessionGeneration } from "../lifecycle/taskRoleSessionReset.js";
|
|
19
|
-
import {
|
|
21
|
+
import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
|
|
20
22
|
import { agentRunDeliveryReceiptId, createAgentRun, withAgentRunContextSnapshot } from "../run/agentRun.js";
|
|
21
23
|
import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
|
|
22
24
|
import { matchYieldReceipt } from "../run/yieldReceipt.js";
|
|
@@ -35,10 +37,11 @@ import { blockingProviderContinuations, projectProviderContinuations } from "../
|
|
|
35
37
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
36
38
|
import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
37
39
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
40
|
+
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
38
41
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
39
42
|
import { resolveProject } from "../repository/project.js";
|
|
40
43
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
41
|
-
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";
|
|
42
45
|
import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
|
|
43
46
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
44
47
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
@@ -79,8 +82,12 @@ function legacyWorkItemReviewConfig(config) {
|
|
|
79
82
|
}
|
|
80
83
|
function storedTaskFinalReviewContract(store, taskId) {
|
|
81
84
|
const contracts = store.listWorkItems(taskId)
|
|
82
|
-
.flatMap((
|
|
83
|
-
|
|
85
|
+
.flatMap((item) => {
|
|
86
|
+
const candidate = governingWorkItemCandidate(item);
|
|
87
|
+
return candidate?.taskFinalReviewContract === undefined
|
|
88
|
+
? []
|
|
89
|
+
: [candidate.taskFinalReviewContract];
|
|
90
|
+
});
|
|
84
91
|
const first = contracts[0];
|
|
85
92
|
if (first === undefined)
|
|
86
93
|
return undefined;
|
|
@@ -96,7 +103,7 @@ function storedTaskFinalReviewContract(store, taskId) {
|
|
|
96
103
|
* acceptance, and completion mutation must present the same verified
|
|
97
104
|
* capability; shared review-config drift is intentionally irrelevant.
|
|
98
105
|
*/
|
|
99
|
-
function taskFinalReviewContractForMutation(store, taskId, options) {
|
|
106
|
+
function taskFinalReviewContractForMutation(store, taskId, options, authorization = {}) {
|
|
100
107
|
const supplied = options.taskFinalReviewContract;
|
|
101
108
|
if (supplied !== undefined) {
|
|
102
109
|
validateTaskFinalReviewContract(supplied);
|
|
@@ -112,6 +119,8 @@ function taskFinalReviewContractForMutation(store, taskId, options) {
|
|
|
112
119
|
if (stored === undefined)
|
|
113
120
|
return supplied;
|
|
114
121
|
if (supplied === undefined) {
|
|
122
|
+
if (authorization.allowStoredWithoutSupplied === true)
|
|
123
|
+
return stored;
|
|
115
124
|
throw usageError(`Task final-review contract is missing for ${taskId}.`);
|
|
116
125
|
}
|
|
117
126
|
if (!sameTaskFinalReviewContract(stored, supplied)) {
|
|
@@ -120,8 +129,8 @@ function taskFinalReviewContractForMutation(store, taskId, options) {
|
|
|
120
129
|
return stored;
|
|
121
130
|
}
|
|
122
131
|
export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
123
|
-
const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote].";
|
|
124
|
-
const parsed = parseTail(args, new Set(["--summary", "--summary-file"]), usage, new Set(["--refresh-remote"]));
|
|
132
|
+
const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote] [--accept-published-tree <publication-id>].";
|
|
133
|
+
const parsed = parseTail(args, new Set(["--summary", "--summary-file", "--accept-published-tree"]), usage, new Set(["--refresh-remote"]));
|
|
125
134
|
exactPositionals(parsed.positionals, 1, usage);
|
|
126
135
|
const inlineSummary = parsed.options.get("--summary");
|
|
127
136
|
const summaryFile = parsed.options.get("--summary-file");
|
|
@@ -129,7 +138,14 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
|
129
138
|
throw usageError(`Specify exactly one of --summary or --summary-file.`, usage);
|
|
130
139
|
}
|
|
131
140
|
const summary = summaryOverride ?? readCommandText(inlineSummary, summaryFile, "--summary", usage);
|
|
132
|
-
|
|
141
|
+
const acceptedPublishedTreePublicationId = parsed.options.get("--accept-published-tree");
|
|
142
|
+
return {
|
|
143
|
+
taskId: parsed.positionals[0],
|
|
144
|
+
summary,
|
|
145
|
+
...(acceptedPublishedTreePublicationId === undefined
|
|
146
|
+
? {}
|
|
147
|
+
: { acceptedPublishedTreePublicationId })
|
|
148
|
+
};
|
|
133
149
|
}
|
|
134
150
|
/**
|
|
135
151
|
* Check every read-only completion blocker before a caller resolves remote
|
|
@@ -143,7 +159,7 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
|
143
159
|
* terminalization precondition before attempting completion. All blockers
|
|
144
160
|
* are reported in one error instead of one per attempt.
|
|
145
161
|
*/
|
|
146
|
-
export function preflightTaskCompletion(taskId, store, options = {}) {
|
|
162
|
+
export function preflightTaskCompletion(taskId, store, options = {}, request = {}) {
|
|
147
163
|
const task = requireTask(store, taskId);
|
|
148
164
|
const actor = taskActor(options, task.id);
|
|
149
165
|
if (task.status === "completed") {
|
|
@@ -156,7 +172,15 @@ export function preflightTaskCompletion(taskId, store, options = {}) {
|
|
|
156
172
|
// Resolve and authenticate the durable Task-local gate before any remote
|
|
157
173
|
// fetch or Integration write. All checks below mirror the transactional
|
|
158
174
|
// completion path, which remains the final CAS fence after reconciliation.
|
|
159
|
-
|
|
175
|
+
// A human/global Operator cannot present the exact managed Leader contract.
|
|
176
|
+
// For the explicit published-tree path only, let that caller authenticate
|
|
177
|
+
// the stored contract far enough to persist an exact authorization fact.
|
|
178
|
+
// The same command must return before any contract-governed completion
|
|
179
|
+
// mutation; the exact Leader later consumes the authorization with the real
|
|
180
|
+
// contract capability.
|
|
181
|
+
const authorizingPublishedTree = request.acceptedPublishedTreePublicationId !== undefined
|
|
182
|
+
&& actor !== "leader";
|
|
183
|
+
const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options, { allowStoredWithoutSupplied: authorizingPublishedTree });
|
|
160
184
|
const activeTaskReview = store.listReviewRounds(task.id).some((round) => ((round.scope ?? "work-item") === "task"
|
|
161
185
|
&& (round.status === "pending" || round.status === "running")));
|
|
162
186
|
// Issue 06: one shared readiness projection enumerates every blocker.
|
|
@@ -240,7 +264,6 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
240
264
|
case "milestone": return taskMilestoneCommand(rest, store, options);
|
|
241
265
|
case "event": return taskEventCommand(rest, store);
|
|
242
266
|
case "continuation": return taskContinuationCommand(rest, store);
|
|
243
|
-
case "enter": return enterTaskRoleAlias(rest, store, options);
|
|
244
267
|
default:
|
|
245
268
|
throw usageError(command === undefined
|
|
246
269
|
? "Task command is required."
|
|
@@ -429,8 +452,8 @@ function updateTaskCommand(args, store, options) {
|
|
|
429
452
|
/** Compatibility helper for call sites that cannot yet handle foreground enter. */
|
|
430
453
|
export function runTaskOutputCommand(args, store, options = {}) {
|
|
431
454
|
const execution = runTaskCommand(args, store, options);
|
|
432
|
-
if (execution.kind
|
|
433
|
-
throw runtimeError("Task
|
|
455
|
+
if (execution.kind !== "output") {
|
|
456
|
+
throw runtimeError("Task Role foreground runtime control requires the CLI.");
|
|
434
457
|
}
|
|
435
458
|
return execution.output;
|
|
436
459
|
}
|
|
@@ -647,17 +670,42 @@ function completeTaskCommand(args, store, options) {
|
|
|
647
670
|
const summary = request.summary;
|
|
648
671
|
const now = clock(options);
|
|
649
672
|
const result = store.transaction((tx) => {
|
|
650
|
-
const preflight = preflightTaskCompletion(request.taskId, tx, options);
|
|
673
|
+
const preflight = preflightTaskCompletion(request.taskId, tx, options, request);
|
|
651
674
|
const { task, actor } = preflight;
|
|
652
675
|
if (preflight.completed) {
|
|
653
676
|
return {
|
|
654
677
|
task,
|
|
655
678
|
changed: false,
|
|
656
679
|
runtimeCleanupTargets: [],
|
|
657
|
-
finalReview: undefined
|
|
680
|
+
finalReview: undefined,
|
|
681
|
+
publishedTreeAuthorization: undefined
|
|
658
682
|
};
|
|
659
683
|
}
|
|
660
684
|
const taskFinalContract = preflight.taskFinalReviewContract;
|
|
685
|
+
const publishedTreeProof = request.acceptedPublishedTreePublicationId === undefined
|
|
686
|
+
? undefined
|
|
687
|
+
: assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskReviewCandidateForMutation(tx, task, options));
|
|
688
|
+
const requiresContractHandoff = publishedTreeProof !== undefined
|
|
689
|
+
&& taskFinalContract !== undefined;
|
|
690
|
+
if (requiresContractHandoff && actor !== "leader") {
|
|
691
|
+
const existing = matchingPublishedTreeAuthorization(tx, publishedTreeProof);
|
|
692
|
+
const event = existing ?? recordTaskEventRecord(tx, task.id, TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT, publishedTreeAuthorizationPayload(actor, publishedTreeProof), now);
|
|
693
|
+
enqueueWork(tx, leaderMailbox(task.id), wakeReason("published-tree-authorized", event.id), now, [eventRef(task.id, event.id)]);
|
|
694
|
+
return {
|
|
695
|
+
task,
|
|
696
|
+
changed: false,
|
|
697
|
+
runtimeCleanupTargets: [],
|
|
698
|
+
finalReview: undefined,
|
|
699
|
+
publishedTreeAuthorization: {
|
|
700
|
+
event,
|
|
701
|
+
proof: publishedTreeProof,
|
|
702
|
+
created: existing === undefined
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
if (publishedTreeProof !== undefined && actor === "leader") {
|
|
707
|
+
requirePublishedTreeAuthorization(tx, publishedTreeProof);
|
|
708
|
+
}
|
|
661
709
|
const roles = tx.listRoles(task.id);
|
|
662
710
|
const activeRuns = roles
|
|
663
711
|
.map((role) => ({ role, run: tx.getActiveAgentRun(task.id, role.name) }))
|
|
@@ -706,7 +754,8 @@ function completeTaskCommand(args, store, options) {
|
|
|
706
754
|
runtimeCleanupTargets: [],
|
|
707
755
|
finalReview,
|
|
708
756
|
resumedPendingFinalReview: pendingFinalReviewIds.has(finalReview.id),
|
|
709
|
-
terminalizedLeaderRun
|
|
757
|
+
terminalizedLeaderRun,
|
|
758
|
+
publishedTreeAuthorization: undefined
|
|
710
759
|
};
|
|
711
760
|
}
|
|
712
761
|
// Issue 06: re-validate the full completion readiness inside the
|
|
@@ -729,6 +778,17 @@ function completeTaskCommand(args, store, options) {
|
|
|
729
778
|
tx.clearOperatorNotification(task.id);
|
|
730
779
|
tx.saveOperatorNotification(createTaskTerminalNotification(task.id, "completed", actor, summary, now));
|
|
731
780
|
enqueueWork(tx, { kind: "operator" }, "task-terminal", now, [taskRef(task.id)]);
|
|
781
|
+
if (publishedTreeProof !== undefined) {
|
|
782
|
+
recordTaskEvent(tx, task.id, "task.completion-published-tree-accepted", {
|
|
783
|
+
by: actor,
|
|
784
|
+
projectId: publishedTreeProof.projectId,
|
|
785
|
+
publicationId: publishedTreeProof.publicationId,
|
|
786
|
+
reviewRoundId: publishedTreeProof.reviewRoundId,
|
|
787
|
+
localCommit: publishedTreeProof.localCommit,
|
|
788
|
+
remoteCommit: publishedTreeProof.remoteCommit,
|
|
789
|
+
tree: publishedTreeProof.tree
|
|
790
|
+
}, now);
|
|
791
|
+
}
|
|
732
792
|
recordTaskEvent(tx, task.id, "task.completed", { by: actor, summary }, now);
|
|
733
793
|
// A terminal Task must never leave a Task-lane signal that can wake it.
|
|
734
794
|
// The durable records remain intact; only the derived mailbox work is
|
|
@@ -747,9 +807,21 @@ function completeTaskCommand(args, store, options) {
|
|
|
747
807
|
runtimeCleanupTargets,
|
|
748
808
|
finalReview: undefined,
|
|
749
809
|
resumedPendingFinalReview: false,
|
|
750
|
-
terminalizedLeaderRun
|
|
810
|
+
terminalizedLeaderRun,
|
|
811
|
+
publishedTreeAuthorization: undefined
|
|
751
812
|
};
|
|
752
813
|
});
|
|
814
|
+
if (result.publishedTreeAuthorization !== undefined) {
|
|
815
|
+
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
816
|
+
const authorization = result.publishedTreeAuthorization;
|
|
817
|
+
return output(authorization.created
|
|
818
|
+
? `Authorized published-tree completion for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`
|
|
819
|
+
: `Published-tree completion is already authorized for ${result.task.id} as ${authorization.event.id}; exact Task Leader completion is required.\n`, {
|
|
820
|
+
task: result.task,
|
|
821
|
+
authorizationEvent: authorization.event,
|
|
822
|
+
publishedTreeProof: authorization.proof
|
|
823
|
+
});
|
|
824
|
+
}
|
|
753
825
|
if (result.changed) {
|
|
754
826
|
for (const target of result.runtimeCleanupTargets) {
|
|
755
827
|
// Cleanup owns an independent lifecycle lane. Never fall back to the
|
|
@@ -1192,8 +1264,12 @@ function taskRoleCommand(args, store, options) {
|
|
|
1192
1264
|
return output(unbindTaskRole(rest, store, options));
|
|
1193
1265
|
if (command === "reset")
|
|
1194
1266
|
return resetTaskRole(rest, store, options);
|
|
1195
|
-
if (command === "
|
|
1196
|
-
return
|
|
1267
|
+
if (command === "view")
|
|
1268
|
+
return viewTaskRole(rest, store);
|
|
1269
|
+
if (command === "takeover")
|
|
1270
|
+
return transferTaskRoleAuthority(rest, store, options, "takeover");
|
|
1271
|
+
if (command === "release")
|
|
1272
|
+
return transferTaskRoleAuthority(rest, store, options, "release");
|
|
1197
1273
|
throw usageError(command === undefined
|
|
1198
1274
|
? "Task role command is required."
|
|
1199
1275
|
: `Unknown command: task role ${command}`);
|
|
@@ -1506,60 +1582,111 @@ function unbindTaskRole(args, store, options) {
|
|
|
1506
1582
|
});
|
|
1507
1583
|
return `Unbound Agent ${args[2]} from ${result.taskId}/${result.name}\n`;
|
|
1508
1584
|
}
|
|
1509
|
-
function
|
|
1510
|
-
const usage = "Task role
|
|
1511
|
-
|
|
1512
|
-
const
|
|
1513
|
-
exactPositionals(parsed.positionals, 2, usage);
|
|
1514
|
-
if (parsed.options.has("--read-only") && parsed.options.has("--read-write")) {
|
|
1515
|
-
throw usageError("--read-only and --read-write are mutually exclusive.", usage);
|
|
1516
|
-
}
|
|
1517
|
-
const task = requireTask(store, parsed.positionals[0]);
|
|
1585
|
+
function viewTaskRole(args, store) {
|
|
1586
|
+
const usage = "Task role view usage: yui task role view <task> <role>.";
|
|
1587
|
+
exactPositionals(args, 2, usage);
|
|
1588
|
+
const task = requireTask(store, args[0]);
|
|
1518
1589
|
if (task.status !== "active") {
|
|
1519
|
-
throw usageError(inactiveTaskMessage(task, "
|
|
1590
|
+
throw usageError(inactiveTaskMessage(task, "viewing a role session"));
|
|
1520
1591
|
}
|
|
1521
|
-
const role = requireRole(store, task.id,
|
|
1522
|
-
const
|
|
1523
|
-
if (
|
|
1524
|
-
|
|
1592
|
+
const role = requireRole(store, task.id, args[1]);
|
|
1593
|
+
const session = store.getRoleSession(task.id, role.name);
|
|
1594
|
+
if (session === null || session.status === "stopped" || session.status === "broken") {
|
|
1595
|
+
throw usageError(`Task Role has no live Provider view: ${task.id}/${role.name}.`);
|
|
1525
1596
|
}
|
|
1526
1597
|
return {
|
|
1527
|
-
kind: "
|
|
1598
|
+
kind: "view",
|
|
1528
1599
|
taskId: task.id,
|
|
1529
1600
|
roleName: role.name,
|
|
1530
|
-
access,
|
|
1531
|
-
output: `
|
|
1601
|
+
access: "read-only",
|
|
1602
|
+
output: `Viewing ${role.name} for ${task.id} (read-only)\n`
|
|
1532
1603
|
};
|
|
1533
1604
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1605
|
+
function transferTaskRoleAuthority(args, store, options, action) {
|
|
1606
|
+
const usage = `Task role ${action} usage: yui task role ${action} <task> <role>.`;
|
|
1607
|
+
exactPositionals(args, 2, usage);
|
|
1608
|
+
const now = clock(options);
|
|
1609
|
+
try {
|
|
1610
|
+
return store.transaction((tx) => {
|
|
1611
|
+
const task = requireTask(tx, args[0]);
|
|
1612
|
+
if (task.status !== "active") {
|
|
1613
|
+
throw usageError(inactiveTaskMessage(task, `${action} Provider authority`));
|
|
1614
|
+
}
|
|
1615
|
+
const role = requireRole(tx, task.id, args[1]);
|
|
1616
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1617
|
+
const session = sessions?.sessions[role.activeAgentId];
|
|
1618
|
+
const binding = sessions?.providerBinding;
|
|
1619
|
+
if (sessions === null || sessions === undefined || session === undefined
|
|
1620
|
+
|| binding === null || binding === undefined
|
|
1621
|
+
|| session.launchId === undefined
|
|
1622
|
+
|| session.status === "stopped" || session.status === "broken") {
|
|
1623
|
+
throw new Error(`Task Role has no live managed Provider: ${task.id}/${role.name}.`);
|
|
1624
|
+
}
|
|
1625
|
+
const activation = currentProviderActivation(binding);
|
|
1626
|
+
if (activation === null) {
|
|
1627
|
+
throw new Error(`Provider Activation is not live: ${task.id}/${role.name}.`);
|
|
1628
|
+
}
|
|
1629
|
+
if (action === "takeover") {
|
|
1630
|
+
const activeRun = tx.getActiveAgentRun(task.id, role.name);
|
|
1631
|
+
if (sessions.inFlight === null || activeRun?.id !== sessions.inFlight.runId) {
|
|
1632
|
+
throw new Error(`Task Role has no active managed Run for takeover: ${task.id}/${role.name}.`);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
if (action === "takeover"
|
|
1636
|
+
&& binding.authority.owner !== "controller"
|
|
1637
|
+
&& binding.authority.owner !== "human") {
|
|
1638
|
+
throw new Error(`Provider authority is not Controller-owned: ${task.id}/${role.name}.`);
|
|
1639
|
+
}
|
|
1640
|
+
if (action === "release"
|
|
1641
|
+
&& binding.authority.owner !== "human"
|
|
1642
|
+
&& binding.authority.owner !== "controller") {
|
|
1643
|
+
throw new Error(`Provider authority is not human-owned: ${task.id}/${role.name}.`);
|
|
1644
|
+
}
|
|
1645
|
+
const desiredOwner = action === "takeover" ? "human" : "controller";
|
|
1646
|
+
const unchanged = binding.authority.owner === desiredOwner;
|
|
1647
|
+
const updatedBinding = unchanged
|
|
1648
|
+
? binding
|
|
1649
|
+
: transferProviderAuthority(binding, {
|
|
1650
|
+
expectedEpoch: binding.authority.epoch,
|
|
1651
|
+
expectedOwner: binding.authority.owner,
|
|
1652
|
+
owner: desiredOwner,
|
|
1653
|
+
holderId: action === "takeover" ? `human:${randomUUID()}` : activation.activationId,
|
|
1654
|
+
changedAt: now.toISOString()
|
|
1655
|
+
});
|
|
1656
|
+
const authority = updatedBinding.authority;
|
|
1657
|
+
if (authority.owner !== "controller" && authority.owner !== "human") {
|
|
1658
|
+
throw new Error("Provider authority transfer did not produce a writer.");
|
|
1659
|
+
}
|
|
1660
|
+
if (!unchanged) {
|
|
1661
|
+
tx.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updatedBinding, now));
|
|
1662
|
+
recordTaskEvent(tx, task.id, "runtime.provider-authority-transferred", {
|
|
1663
|
+
role: role.name,
|
|
1664
|
+
owner: authority.owner,
|
|
1665
|
+
holderId: authority.holderId,
|
|
1666
|
+
epoch: String(authority.epoch)
|
|
1667
|
+
}, now);
|
|
1668
|
+
}
|
|
1669
|
+
return {
|
|
1670
|
+
kind: "authority",
|
|
1671
|
+
action,
|
|
1672
|
+
taskId: task.id,
|
|
1673
|
+
roleName: role.name,
|
|
1674
|
+
launchId: session.launchId,
|
|
1675
|
+
nativeSessionId: session.nativeSessionId,
|
|
1676
|
+
authority: {
|
|
1677
|
+
epoch: authority.epoch,
|
|
1678
|
+
owner: authority.owner,
|
|
1679
|
+
holderId: authority.holderId
|
|
1680
|
+
},
|
|
1681
|
+
output: action === "takeover"
|
|
1682
|
+
? `Human authority ${unchanged ? "replayed" : "acquired"} for ${task.id}/${role.name} at epoch ${authority.epoch}.\n`
|
|
1683
|
+
: `Controller authority ${unchanged ? "replayed" : "restored"} for ${task.id}/${role.name} at epoch ${authority.epoch}.\n`
|
|
1684
|
+
};
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
catch (error) {
|
|
1688
|
+
throw usageError(messageOf(error), usage);
|
|
1556
1689
|
}
|
|
1557
|
-
return enterTaskRole([
|
|
1558
|
-
parsed.positionals[0],
|
|
1559
|
-
parsed.positionals[1] ?? LEADER_ROLE,
|
|
1560
|
-
...(parsed.options.has("--read-only") ? ["--read-only"] : []),
|
|
1561
|
-
...(parsed.options.has("--read-write") ? ["--read-write"] : [])
|
|
1562
|
-
], store, options);
|
|
1563
1690
|
}
|
|
1564
1691
|
function taskWorkCommand(args, store, options) {
|
|
1565
1692
|
const [command, ...rest] = args;
|
|
@@ -2553,6 +2680,8 @@ function taskReviewCommand(args, store, options) {
|
|
|
2553
2680
|
const [command, ...rest] = args;
|
|
2554
2681
|
if (command === "request")
|
|
2555
2682
|
return requestTaskReviewRound(rest, store, options);
|
|
2683
|
+
if (command === "force-fresh")
|
|
2684
|
+
return forceFreshTaskReviewRound(rest, store, options);
|
|
2556
2685
|
if (command === "retry")
|
|
2557
2686
|
return retryFailedTaskReviewRound(rest, store, options);
|
|
2558
2687
|
if (command === "group")
|
|
@@ -3023,6 +3152,209 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3023
3152
|
: `Task-final delta-recheck requested as ${round.id} (rechecks ${round.deltaRecheck.previousReviewRoundId})\n`
|
|
3024
3153
|
: `Task-final Review is already ${round.status}: ${round.id}\n`, { reviewRound: round });
|
|
3025
3154
|
}
|
|
3155
|
+
const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
|
|
3156
|
+
/**
|
|
3157
|
+
* Creates a distinct full Task-final ReviewRound only when the exact previous
|
|
3158
|
+
* Round failed without producing semantic review evidence. The source Round,
|
|
3159
|
+
* Run, findings, workspace, and terminal report remain immutable history; the
|
|
3160
|
+
* linking Event is both the audit record and the idempotence key.
|
|
3161
|
+
*/
|
|
3162
|
+
function forceFreshTaskReviewRound(args, store, options) {
|
|
3163
|
+
const usage = "Task review force-fresh usage: yui task review force-fresh <task>/<review-round>.";
|
|
3164
|
+
exactPositionals(args, 1, usage);
|
|
3165
|
+
const reference = taskRecordReference(args[0], "reviewRound", "ReviewRound reference", options);
|
|
3166
|
+
const now = clock(options);
|
|
3167
|
+
const result = store.transaction((tx) => {
|
|
3168
|
+
const source = tx.getReviewRound(reference.taskId, reference.localId);
|
|
3169
|
+
if (source === null) {
|
|
3170
|
+
throw dataError(`ReviewRound not found: ${reference.taskId}/${reference.localId}.`);
|
|
3171
|
+
}
|
|
3172
|
+
const task = requireTask(tx, reference.taskId);
|
|
3173
|
+
if (task.status !== "active")
|
|
3174
|
+
throw usageError(`Task is not active: ${task.id}.`);
|
|
3175
|
+
if (taskActor(options, task.id) !== "leader") {
|
|
3176
|
+
throw usageError("Only the Task Leader may force a fresh Task-final ReviewRound.");
|
|
3177
|
+
}
|
|
3178
|
+
if ((source.scope ?? "work-item") !== "task") {
|
|
3179
|
+
throw usageError(`ReviewRound ${source.id} is not a Task-final ReviewRound.`);
|
|
3180
|
+
}
|
|
3181
|
+
const replacementEvents = tx.listEvents(task.id).filter((event) => (event.type === TASK_FINAL_FORCE_FRESH_EVENT
|
|
3182
|
+
&& event.payload.sourceReviewRoundId === source.id));
|
|
3183
|
+
if (replacementEvents.length > 1) {
|
|
3184
|
+
throw dataError(`ReviewRound ${source.id} has duplicate force-fresh audit events.`);
|
|
3185
|
+
}
|
|
3186
|
+
const replacementEvent = replacementEvents[0];
|
|
3187
|
+
if (replacementEvent !== undefined) {
|
|
3188
|
+
const replacementId = replacementEvent.payload.reviewRoundId;
|
|
3189
|
+
const replacement = replacementId === undefined
|
|
3190
|
+
? null
|
|
3191
|
+
: tx.getReviewRound(task.id, replacementId);
|
|
3192
|
+
if (replacement === null
|
|
3193
|
+
|| replacement.id === source.id
|
|
3194
|
+
|| (replacement.scope ?? "work-item") !== "task"
|
|
3195
|
+
|| replacement.workItemId !== source.workItemId
|
|
3196
|
+
|| replacement.candidateId !== source.candidateId
|
|
3197
|
+
|| replacement.reviewerRoleName !== source.reviewerRoleName
|
|
3198
|
+
|| replacement.deltaRecheck !== undefined
|
|
3199
|
+
|| !sameTaskFinalReviewContract(replacement.taskFinalReviewContract, source.taskFinalReviewContract)
|
|
3200
|
+
|| !isSameTaskReviewCandidate(replacement.taskCandidate, source.taskCandidate)
|
|
3201
|
+
|| replacementEvent.payload.workItemId !== replacement.workItemId
|
|
3202
|
+
|| replacementEvent.payload.candidateId !== replacement.candidateId
|
|
3203
|
+
|| replacementEvent.payload.reviewerRoleName !== replacement.reviewerRoleName
|
|
3204
|
+
|| replacementEvent.payload.taskCandidate !== JSON.stringify(replacement.taskCandidate)) {
|
|
3205
|
+
throw dataError(`Force-fresh audit for ${source.id} does not match its replacement Round.`);
|
|
3206
|
+
}
|
|
3207
|
+
return { round: replacement, source, created: false };
|
|
3208
|
+
}
|
|
3209
|
+
const semanticBlocker = forceFreshSemanticBlocker(tx, source);
|
|
3210
|
+
if (semanticBlocker !== null) {
|
|
3211
|
+
throw usageError(`ReviewRound ${source.id} is not eligible for force-fresh: ${semanticBlocker}`);
|
|
3212
|
+
}
|
|
3213
|
+
if (source.taskCandidate === undefined) {
|
|
3214
|
+
throw dataError(`ReviewRound ${source.id} has no frozen Task candidate.`);
|
|
3215
|
+
}
|
|
3216
|
+
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
3217
|
+
if (!sameTaskFinalReviewContract(source.taskFinalReviewContract, taskFinalContract)) {
|
|
3218
|
+
throw usageError(`Task final-review contract does not match ReviewRound ${source.id}.`);
|
|
3219
|
+
}
|
|
3220
|
+
if (source.executionGroup !== undefined
|
|
3221
|
+
&& (source.executionGroup.strategy.mode !== "fixed"
|
|
3222
|
+
|| source.executionGroup.strategy.count !== 1
|
|
3223
|
+
|| source.executionGroup.lanes.length !== 1
|
|
3224
|
+
|| source.executionGroup.lanes[0].roleName !== source.reviewerRoleName)) {
|
|
3225
|
+
throw usageError(`ReviewRound ${source.id} is not a single-Reviewer full Review; force-fresh is refused.`);
|
|
3226
|
+
}
|
|
3227
|
+
const item = tx.getWorkItem(task.id, source.workItemId);
|
|
3228
|
+
const candidate = item?.candidates.find(({ id }) => id === source.candidateId);
|
|
3229
|
+
if (item === null || item === undefined || candidate === undefined) {
|
|
3230
|
+
throw dataError(`Final Review anchor Candidate is no longer available: `
|
|
3231
|
+
+ `${source.workItemId}/${source.candidateId}.`);
|
|
3232
|
+
}
|
|
3233
|
+
const provenance = taskReviewProvenance(tx, task, options);
|
|
3234
|
+
if (!isSameTaskReviewCandidate(source.taskCandidate, provenance.candidate)) {
|
|
3235
|
+
throw usageError(`Final ReviewRound ${source.id} freezes a candidate that is no longer the current Task candidate.`);
|
|
3236
|
+
}
|
|
3237
|
+
const producerCollision = taskReviewProducerCollision(provenance, source.reviewerRoleName);
|
|
3238
|
+
if (producerCollision !== null)
|
|
3239
|
+
throw usageError(producerCollision);
|
|
3240
|
+
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
|
|
3241
|
+
.filter((entry) => (entry.scope ?? "work-item") === "task"));
|
|
3242
|
+
const sourceIndex = taskRounds.findIndex(({ id }) => id === source.id);
|
|
3243
|
+
if (sourceIndex < 0) {
|
|
3244
|
+
throw dataError(`Final ReviewRound is not in Task history: ${source.id}.`);
|
|
3245
|
+
}
|
|
3246
|
+
const laterRound = taskRounds.slice(sourceIndex + 1).at(-1);
|
|
3247
|
+
if (laterRound !== undefined) {
|
|
3248
|
+
throw usageError(`A newer Task-final ReviewRound already exists after ${source.id}: `
|
|
3249
|
+
+ `${laterRound.id}/${laterRound.status}.`);
|
|
3250
|
+
}
|
|
3251
|
+
assertNoConflictingTaskReviewRound(taskRounds, source.id);
|
|
3252
|
+
assertTaskReviewRequestLane(tx, task.id, source.reviewerRoleName);
|
|
3253
|
+
let reviewer = tx.getRole(task.id, source.reviewerRoleName);
|
|
3254
|
+
if (reviewer === null) {
|
|
3255
|
+
if (tx.getGlobalRole(source.reviewerRoleName) === null) {
|
|
3256
|
+
throw usageError(`Global Role not found: ${source.reviewerRoleName}.`);
|
|
3257
|
+
}
|
|
3258
|
+
reviewer = createTaskRole(tx, task, source.reviewerRoleName, undefined, now, source.reviewerRoleName);
|
|
3259
|
+
tx.saveRole(task.id, reviewer);
|
|
3260
|
+
}
|
|
3261
|
+
let created = createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, source.workItemId, source.candidateId, source.reviewerRoleName, "leader", source.taskCandidate, now, taskFinalContract);
|
|
3262
|
+
const group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
|
|
3263
|
+
purpose: "review",
|
|
3264
|
+
target: executionTargetForReviewRound(task, created, item, candidate),
|
|
3265
|
+
strategy: { mode: "fixed", count: 1 },
|
|
3266
|
+
lanes: [{ roleName: reviewer.name, reviewRoundId: created.id }]
|
|
3267
|
+
}, now);
|
|
3268
|
+
created = { ...created, executionGroup: group };
|
|
3269
|
+
tx.saveReviewRound(task.id, created);
|
|
3270
|
+
recordTaskEvent(tx, task.id, TASK_FINAL_FORCE_FRESH_EVENT, {
|
|
3271
|
+
sourceReviewRoundId: source.id,
|
|
3272
|
+
...(source.reviewerRunId === undefined ? {} : { sourceReviewerRunId: source.reviewerRunId }),
|
|
3273
|
+
reviewRoundId: created.id,
|
|
3274
|
+
workItemId: created.workItemId,
|
|
3275
|
+
candidateId: created.candidateId,
|
|
3276
|
+
reviewerRoleName: created.reviewerRoleName,
|
|
3277
|
+
taskCandidate: JSON.stringify(created.taskCandidate),
|
|
3278
|
+
reason: "source-round-failed-without-semantic-review",
|
|
3279
|
+
leaderActionRunId: taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome) ?? "leader"
|
|
3280
|
+
}, now);
|
|
3281
|
+
return { round: created, source, created: true };
|
|
3282
|
+
});
|
|
3283
|
+
return output(result.created
|
|
3284
|
+
? `Fresh Task-final Review requested as ${result.round.id} after ${result.source.id}\n`
|
|
3285
|
+
: `Fresh Task-final Review already requested as ${result.round.id} after ${result.source.id}\n`, { reviewRound: result.round, sourceReviewRound: result.source });
|
|
3286
|
+
}
|
|
3287
|
+
/** Returns the exact semantic evidence that makes a failed Round ineligible. */
|
|
3288
|
+
function forceFreshSemanticBlocker(store, round) {
|
|
3289
|
+
if (round.status !== "failed")
|
|
3290
|
+
return `source status is ${round.status}, not failed.`;
|
|
3291
|
+
if ((round.checks ?? []).length > 0)
|
|
3292
|
+
return "the Round records review checks.";
|
|
3293
|
+
if (round.evidenceCommit !== undefined)
|
|
3294
|
+
return "the Round records a review evidence commit.";
|
|
3295
|
+
if (round.report !== round.summary) {
|
|
3296
|
+
return "the Round stores a report distinct from its failure summary.";
|
|
3297
|
+
}
|
|
3298
|
+
if (runtimeFailureSummaryHasReviewerOutput(round.report ?? "")) {
|
|
3299
|
+
return "the Round stores non-empty Reviewer output in its runtime failure summary.";
|
|
3300
|
+
}
|
|
3301
|
+
if (round.deltaRecheck?.disposition !== undefined
|
|
3302
|
+
|| round.deltaRecheck?.reasoning !== undefined) {
|
|
3303
|
+
return "the Round records a semantic delta-recheck disposition.";
|
|
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));
|
|
3357
|
+
}
|
|
3026
3358
|
/**
|
|
3027
3359
|
* Issue 07: re-validates the CLI-computed delta preflight inside the store
|
|
3028
3360
|
* transaction. The previous Round must be a completed acceptance (a full
|
|
@@ -3252,13 +3584,16 @@ function taskRunCommand(args, store, options) {
|
|
|
3252
3584
|
function runContextCommand(args, store, options) {
|
|
3253
3585
|
const [first, ...rest] = args;
|
|
3254
3586
|
if (first === "expand") {
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3587
|
+
const usage = "Task run context expand usage: yui task run context expand <task>/<run> <ref-id> [--store <store>] [--mode full].";
|
|
3588
|
+
const parsed = parseTail(rest, new Set(["--store", "--mode"]), usage);
|
|
3589
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
3590
|
+
const mode = parsed.options.get("--mode");
|
|
3591
|
+
if (mode !== undefined && mode !== "full") {
|
|
3592
|
+
throw usageError("Run Context expansion mode must be full.", usage);
|
|
3258
3593
|
}
|
|
3259
|
-
const { taskId, runId } = parseRunContextReference(
|
|
3594
|
+
const { taskId, runId } = parseRunContextReference(parsed.positionals[0]);
|
|
3260
3595
|
authorizeRunContext(store, taskId, runId, options.environment);
|
|
3261
|
-
const expanded = store.transaction((tx) => expandRunContextRef(tx, taskId, runId,
|
|
3596
|
+
const expanded = store.transaction((tx) => expandRunContextRef(tx, taskId, runId, parsed.positionals[1], optionalNonEmptyOption(parsed.options, "--store")));
|
|
3262
3597
|
return output(`${JSON.stringify(expanded, null, 2)}\n`, { context: expanded });
|
|
3263
3598
|
}
|
|
3264
3599
|
if (first === "delta") {
|
|
@@ -3996,15 +4331,20 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
3996
4331
|
}
|
|
3997
4332
|
}
|
|
3998
4333
|
function latestTaskReviewContractAnchor(store, task, taskFinalContract) {
|
|
3999
|
-
const
|
|
4000
|
-
.
|
|
4001
|
-
|
|
4334
|
+
const anchor = store.listWorkItems(task.id)
|
|
4335
|
+
.flatMap((item) => {
|
|
4336
|
+
const candidate = governingWorkItemCandidate(item);
|
|
4337
|
+
return candidate !== undefined && sameTaskFinalReviewContract(candidate.taskFinalReviewContract, taskFinalContract)
|
|
4338
|
+
? [{ item, candidate }]
|
|
4339
|
+
: [];
|
|
4340
|
+
})
|
|
4341
|
+
.sort((left, right) => (left.item.updatedAt.localeCompare(right.item.updatedAt)
|
|
4342
|
+
|| left.item.id.localeCompare(right.item.id)))
|
|
4002
4343
|
.at(-1);
|
|
4003
|
-
|
|
4004
|
-
if (item === undefined || candidate === undefined) {
|
|
4344
|
+
if (anchor === undefined) {
|
|
4005
4345
|
throw usageError(`Task ${task.id} has no WorkItem Candidate to anchor its final Review.`);
|
|
4006
4346
|
}
|
|
4007
|
-
return
|
|
4347
|
+
return anchor;
|
|
4008
4348
|
}
|
|
4009
4349
|
/**
|
|
4010
4350
|
* Leader-only retry of an exact failed Task-final review Run. The old failed
|
|
@@ -5324,6 +5664,44 @@ function recordTaskEventRecord(store, taskId, type, payload, now) {
|
|
|
5324
5664
|
store.saveEvent(taskId, event);
|
|
5325
5665
|
return event;
|
|
5326
5666
|
}
|
|
5667
|
+
function publishedTreeAuthorizationPayload(actor, proof) {
|
|
5668
|
+
return {
|
|
5669
|
+
by: actor,
|
|
5670
|
+
projectId: proof.projectId,
|
|
5671
|
+
publicationId: proof.publicationId,
|
|
5672
|
+
reviewRoundId: proof.reviewRoundId,
|
|
5673
|
+
localCommit: proof.localCommit,
|
|
5674
|
+
remoteCommit: proof.remoteCommit,
|
|
5675
|
+
tree: proof.tree
|
|
5676
|
+
};
|
|
5677
|
+
}
|
|
5678
|
+
function matchingPublishedTreeAuthorization(store, proof) {
|
|
5679
|
+
const events = store.listEvents(proof.taskId);
|
|
5680
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
5681
|
+
const event = events[index];
|
|
5682
|
+
if (event.type === "task.completed" || event.type === "task.reopened")
|
|
5683
|
+
return undefined;
|
|
5684
|
+
if (event.type === TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT
|
|
5685
|
+
&& (event.payload.by === "user" || event.payload.by === "operator")
|
|
5686
|
+
&& event.payload.projectId === proof.projectId
|
|
5687
|
+
&& event.payload.publicationId === proof.publicationId
|
|
5688
|
+
&& event.payload.reviewRoundId === proof.reviewRoundId
|
|
5689
|
+
&& event.payload.localCommit === proof.localCommit
|
|
5690
|
+
&& event.payload.remoteCommit === proof.remoteCommit
|
|
5691
|
+
&& event.payload.tree === proof.tree) {
|
|
5692
|
+
return event;
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
return undefined;
|
|
5696
|
+
}
|
|
5697
|
+
function requirePublishedTreeAuthorization(store, proof) {
|
|
5698
|
+
const authorization = matchingPublishedTreeAuthorization(store, proof);
|
|
5699
|
+
if (authorization === undefined) {
|
|
5700
|
+
throw usageError(`Published-tree completion requires explicit user or global Operator authorization for `
|
|
5701
|
+
+ `${proof.taskId}/${proof.publicationId} at Task-final Review ${proof.reviewRoundId}.`);
|
|
5702
|
+
}
|
|
5703
|
+
return authorization;
|
|
5704
|
+
}
|
|
5327
5705
|
/** Keeps a free-text run-fact note bounded so an event payload stays compact. */
|
|
5328
5706
|
function truncateEventNote(note) {
|
|
5329
5707
|
const normalized = note.trim();
|
|
@@ -6272,6 +6650,9 @@ function workItemRef(taskId, id) {
|
|
|
6272
6650
|
function messageRef(taskId, id) {
|
|
6273
6651
|
return { type: "message", taskId, id };
|
|
6274
6652
|
}
|
|
6653
|
+
function eventRef(taskId, id) {
|
|
6654
|
+
return { type: "event", taskId, id };
|
|
6655
|
+
}
|
|
6275
6656
|
function notifyMailbox(runtime, target, compatibilityTaskId) {
|
|
6276
6657
|
if (runtime?.notifyMailboxChanged !== undefined) {
|
|
6277
6658
|
runtime.notifyMailboxChanged(target);
|