@zq-silk/yui 0.6.13 → 0.6.14
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/README.md +34 -5
- package/dist/cli/commandCatalog.js +173 -57
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli.js +99 -11
- package/dist/commands/configCommands.js +521 -171
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +56 -3
- package/dist/commands/projectCommands.js +504 -2
- package/dist/commands/releaseCommands.js +0 -1
- package/dist/commands/taskActor.js +17 -0
- package/dist/commands/taskBaseCommands.js +29 -0
- package/dist/commands/taskCommands.js +577 -126
- package/dist/commands/taskContextCommand.js +36 -2
- package/dist/commands/taskNextActionCommand.js +48 -3
- package/dist/commands/taskPublicationCommands.js +319 -0
- package/dist/commands/taskRoleRuntimeStatus.js +83 -59
- package/dist/commands/telemetryCommands.js +14 -13
- package/dist/config/yuiConfig.js +161 -8
- package/dist/context/sessionContextBudget.js +68 -0
- package/dist/context/wakeNotification.js +65 -0
- package/dist/controller/clientRuntime.js +2 -1
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
- package/dist/controller/jobSupervisor.js +5 -4
- package/dist/controller/resourceCleanupLinux.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +3 -3
- package/dist/controller/runtime.js +88 -10
- package/dist/controller/updateReconciliation.js +4 -3
- package/dist/doctor/doctor.js +26 -7
- package/dist/executor/agentConfigurationCatalog.js +18 -0
- package/dist/executor/fileRoleLaunchPlanner.js +3 -3
- package/dist/lifecycle/contextBudgetRollover.js +81 -0
- package/dist/lifecycle/exactRunTerminalization.js +11 -1
- package/dist/lifecycle/providerErrorClass.js +33 -12
- package/dist/observability/executionAudit.js +214 -6
- package/dist/output/table.js +18 -0
- package/dist/repository/gitWorkspace.js +92 -0
- package/dist/repository/project.js +218 -4
- package/dist/repository/taskBaseFreshness.js +318 -0
- package/dist/repository/taskWorkspacePreparer.js +16 -2
- package/dist/review/deltaRecheck.js +232 -0
- package/dist/review/reviewConfig.js +31 -0
- package/dist/review/reviewFindingLedger.js +5 -1
- package/dist/review/reviewRound.js +156 -1
- package/dist/run/providerRetry.js +21 -3
- package/dist/run/providerRetryConfig.js +13 -60
- package/dist/run/recoveryProjection.js +199 -0
- package/dist/runtime/builtinAgentDrivers.js +3 -0
- package/dist/runtime/builtinTranscriptUsage.js +76 -32
- package/dist/runtime/continuationManager.js +17 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/launchDiagnostics.js +154 -0
- package/dist/runtime/lifecycleReservation.js +13 -0
- package/dist/runtime/providerContinuation.js +38 -0
- package/dist/runtime/providerContinuationReconciliationService.js +1 -0
- package/dist/runtime/providerErrorCodes.js +278 -0
- package/dist/runtime/runtimeHealthPolicy.js +20 -0
- package/dist/runtime/runtimeObservation.js +1 -0
- package/dist/runtime/runtimeProjection.js +115 -23
- package/dist/runtime/tmuxAdapters.js +242 -48
- package/dist/scheduler/activeRoleRunDelivery.js +139 -3
- package/dist/scheduler/activeTaskProgress.js +4 -3
- package/dist/scheduler/leaderWakeupProcessor.js +105 -15
- package/dist/scheduler/roleRunLiveness.js +2 -1
- package/dist/scheduler/roleRunStall.js +3 -2
- package/dist/scheduler/taskWake.js +72 -0
- package/dist/scheduler/wakeReason.js +64 -0
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/migration/productionRegistry.js +325 -1
- package/dist/storage/sqliteSchema.js +61 -2
- package/dist/storage/sqliteStore.js +129 -2
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskStore.js +262 -5
- package/dist/storage/upgrade/recordVersions.js +6 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
- package/dist/task/completionReadiness.js +282 -0
- package/dist/task/publicationReference.js +123 -0
- package/dist/task/taskRecordReference.js +3 -1
- package/dist/telemetry/telemetryConfig.js +23 -18
- package/dist/telemetry/telemetryWiring.js +8 -8
- package/dist/tmux/tmuxManager.js +50 -9
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/view.js +18 -0
- package/dist/web/webSnapshot.js +100 -10
- package/i18n/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +49 -10
- package/skills/yui-operator/SKILL.md +13 -3
- package/skills/yui-worker/SKILL.md +8 -0
|
@@ -15,22 +15,27 @@ import { recoverExactAgentRun, terminalizeExactTaskRun, validateExactRunReviewRo
|
|
|
15
15
|
import { resetTaskRoleSessionGeneration } from "../lifecycle/taskRoleSessionReset.js";
|
|
16
16
|
import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
|
|
17
17
|
import { createAgentRun } from "../run/agentRun.js";
|
|
18
|
+
import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
|
|
18
19
|
import { matchYieldReceipt } from "../run/yieldReceipt.js";
|
|
19
20
|
import { providerRetryConfig } from "../run/providerRetryConfig.js";
|
|
20
|
-
import { createReviewRound, createTaskReviewRound, attachReviewExecutionGroup, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
|
|
21
|
-
import {
|
|
21
|
+
import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
|
|
22
|
+
import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../review/deltaRecheck.js";
|
|
23
|
+
import { buildTaskFinalReviewFindingContext, dispositionReviewFinding, planRepairGroups, reconcileReviewFindings, reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
|
|
22
24
|
import { LEADER_FINDING_DISPOSITIONS } from "../review/reviewFinding.js";
|
|
23
25
|
import { markYuiRunInput, retagYuiRunInput } from "../run/runIdentity.js";
|
|
24
26
|
import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
25
27
|
import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
|
|
26
28
|
import { createDecision, supersedeDecision } from "../decision/decision.js";
|
|
27
29
|
import { createMilestone } from "../milestone/milestone.js";
|
|
30
|
+
import { runPublicationCommand } from "./taskPublicationCommands.js";
|
|
28
31
|
import { enqueueWork, settleExactWorkExecution } from "../coordination/workMailboxQueue.js";
|
|
29
32
|
import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
|
|
30
33
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
31
|
-
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
34
|
+
import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
35
|
+
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
32
36
|
import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
33
37
|
import { formatAgentRunReceiptId, resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
38
|
+
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
34
39
|
import { resolveProject } from "../repository/project.js";
|
|
35
40
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
36
41
|
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
@@ -43,13 +48,14 @@ import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
|
|
|
43
48
|
import { runTaskContextCommand } from "./taskContextCommand.js";
|
|
44
49
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
45
50
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
46
|
-
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
51
|
+
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
47
52
|
import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
48
53
|
import { runGrantCommand } from "./grantCommands.js";
|
|
49
54
|
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
50
55
|
import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActor.js";
|
|
51
56
|
import { createTaskTerminalNotification } from "../scheduler/operatorNotification.js";
|
|
52
57
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
58
|
+
import { renderWakeReason, wakeReason } from "../scheduler/wakeReason.js";
|
|
53
59
|
import { collectTaskActionability, computeActionabilityDigest, deriveLeaderRunDisposition } from "../scheduler/actionability.js";
|
|
54
60
|
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
55
61
|
import { buildTaskOverview, parseTaskListOptions, renderTaskOverview } from "./taskOverviewCommand.js";
|
|
@@ -114,8 +120,8 @@ function taskFinalReviewContractForMutation(store, taskId, options) {
|
|
|
114
120
|
return stored;
|
|
115
121
|
}
|
|
116
122
|
export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
117
|
-
const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->).";
|
|
118
|
-
const parsed = parseTail(args, new Set(["--summary", "--summary-file"]), usage);
|
|
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"]));
|
|
119
125
|
exactPositionals(parsed.positionals, 1, usage);
|
|
120
126
|
const inlineSummary = parsed.options.get("--summary");
|
|
121
127
|
const summaryFile = parsed.options.get("--summary-file");
|
|
@@ -131,6 +137,11 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
|
131
137
|
* path invokes this same preflight and then repeats its checks while holding
|
|
132
138
|
* the store write fence, so remote reconciliation can never get ahead of the
|
|
133
139
|
* local lifecycle/readiness gate.
|
|
140
|
+
*
|
|
141
|
+
* Issue 06: the blocker enumeration is the pure `projectCompletionReadiness`
|
|
142
|
+
* projection, shared with `task next-action` so the Leader sees every
|
|
143
|
+
* terminalization precondition before attempting completion. All blockers
|
|
144
|
+
* are reported in one error instead of one per attempt.
|
|
134
145
|
*/
|
|
135
146
|
export function preflightTaskCompletion(taskId, store, options = {}) {
|
|
136
147
|
const task = requireTask(store, taskId);
|
|
@@ -148,64 +159,27 @@ export function preflightTaskCompletion(taskId, store, options = {}) {
|
|
|
148
159
|
const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options);
|
|
149
160
|
const activeTaskReview = store.listReviewRounds(task.id).some((round) => ((round.scope ?? "work-item") === "task"
|
|
150
161
|
&& (round.status === "pending" || round.status === "running")));
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (store.listChangeSets(task.id).length === 0) {
|
|
168
|
-
throw usageError(`Task ${task.id} requires at least one ChangeSet before completion.`);
|
|
169
|
-
}
|
|
170
|
-
if (!store.listIntegrationAttempts(task.id).some(({ status }) => status === "committed")) {
|
|
171
|
-
throw usageError(`Task ${task.id} requires a committed Integration Attempt before completion.`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const unresolvedIntegration = store.listIntegrationAttempts(task.id).find((integration) => (integration.status === "running"
|
|
175
|
-
|| integration.status === "blocked"
|
|
176
|
-
|| integration.status === "validating"));
|
|
177
|
-
if (unresolvedIntegration !== undefined) {
|
|
178
|
-
throw usageError(`Task ${task.id} has an unresolved Integration Attempt: ${unresolvedIntegration.id}.`);
|
|
179
|
-
}
|
|
180
|
-
const unsettledQueueEntry = store.listIntegrationQueueEntries(task.id).find((entry) => (entry.status !== "committed" && entry.status !== "superseded"));
|
|
181
|
-
if (unsettledQueueEntry !== undefined) {
|
|
182
|
-
throw usageError(`Task ${task.id} has an unsettled integration queue entry: `
|
|
183
|
-
+ `${unsettledQueueEntry.id}/${unsettledQueueEntry.status}.`);
|
|
184
|
-
}
|
|
185
|
-
const continuationBlockers = blockingProviderContinuations(store.listEvents(task.id));
|
|
186
|
-
if (continuationBlockers.length > 0) {
|
|
187
|
-
throw usageError(`Task ${task.id} has ${continuationBlockers.length} Provider continuation(s) `
|
|
188
|
-
+ "that may still write the Workspace or have an identity conflict.");
|
|
189
|
-
}
|
|
190
|
-
const isolatedWorkspace = store.listManagedWorkspaces(task.id)
|
|
191
|
-
.find(({ owner }) => owner.type === "work-item");
|
|
192
|
-
if (isolatedWorkspace?.owner.type === "work-item") {
|
|
193
|
-
throw usageError(`Task ${task.id} has an isolated WorkItem workspace: `
|
|
194
|
-
+ `${isolatedWorkspace.owner.workItemId}. Capture, integrate or abandon it, then clean it up.`);
|
|
162
|
+
// Issue 06: one shared readiness projection enumerates every blocker.
|
|
163
|
+
const readinessFacts = store.readCompletionReadinessFacts(task.id);
|
|
164
|
+
if (readinessFacts === null)
|
|
165
|
+
throw taskNotFound(task.id);
|
|
166
|
+
// The finding ledger gate is intentionally deferred: the transactional
|
|
167
|
+
// completion path runs it after `prepareFinalTaskReview`, which may create
|
|
168
|
+
// the Task-final Review that resolves `fixed-pending-review` findings.
|
|
169
|
+
const readiness = projectCompletionReadiness(readinessFacts, { findingsGate: false });
|
|
170
|
+
// An active Task-final Review is not a preflight failure: the transactional
|
|
171
|
+
// path resumes a pending Round (or reports the running one) via
|
|
172
|
+
// `prepareFinalTaskReview`, and the CLI skips remote reconciliation while
|
|
173
|
+
// `activeTaskReview` is true. The blocker stays in the shared projection
|
|
174
|
+
// so other surfaces (next-action, future readers) see the full rule set.
|
|
175
|
+
const blockers = readiness.blockers.filter((blocker) => blocker.code !== "active-task-review");
|
|
176
|
+
if (blockers.length > 0) {
|
|
177
|
+
throw usageError(formatCompletionBlockers(task.id, blockers));
|
|
195
178
|
}
|
|
196
179
|
const roles = store.listRoles(task.id);
|
|
197
180
|
const activeRuns = roles
|
|
198
181
|
.map((role) => ({ role, run: store.getActiveAgentRun(task.id, role.name) }))
|
|
199
182
|
.filter((entry) => entry.run !== null);
|
|
200
|
-
const workerRun = activeRuns.find(({ role }) => role.name !== LEADER_ROLE);
|
|
201
|
-
if (workerRun !== undefined) {
|
|
202
|
-
throw usageError(`Task ${task.id} has an active run for Role ${workerRun.role.name}.`);
|
|
203
|
-
}
|
|
204
|
-
const unsettledWork = store.listWorkItems(task.id)
|
|
205
|
-
.find((item) => item.status === "pending" || item.status === "running");
|
|
206
|
-
if (unsettledWork !== undefined) {
|
|
207
|
-
throw usageError(`Task ${task.id} has unsettled work: ${unsettledWork.id}.`);
|
|
208
|
-
}
|
|
209
183
|
const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
|
|
210
184
|
if (leaderEntry !== undefined) {
|
|
211
185
|
if (actor !== "leader") {
|
|
@@ -226,6 +200,15 @@ export function preflightTaskCompletion(taskId, store, options = {}) {
|
|
|
226
200
|
...(taskFinalReviewContract === undefined ? {} : { taskFinalReviewContract })
|
|
227
201
|
};
|
|
228
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Issue 06: format every completion blocker into one fail-closed error so the
|
|
205
|
+
* Leader sees the full remaining work instead of one blocker per attempt.
|
|
206
|
+
*/
|
|
207
|
+
function formatCompletionBlockers(taskId, blockers) {
|
|
208
|
+
const lines = blockers.map((blocker) => ` ${blocker.code} (${blocker.ref.kind} ${blocker.ref.id}): ${blocker.reason}`
|
|
209
|
+
+ ` — fix: ${blocker.fix}`);
|
|
210
|
+
return `Task ${taskId} cannot complete: ${blockers.length} blocker(s) remain.\n${lines.join("\n")}`;
|
|
211
|
+
}
|
|
229
212
|
export function runTaskCommand(args, store, options = {}) {
|
|
230
213
|
const [command, ...rest] = args;
|
|
231
214
|
switch (command) {
|
|
@@ -242,11 +225,12 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
242
225
|
case "retire": return retireTaskCommand(rest, store, options);
|
|
243
226
|
case "reconcile": return output(reconcileTaskCommand(rest, store, options));
|
|
244
227
|
case "message": return output(taskMessageCommand(rest, store, options));
|
|
245
|
-
case "wake": return
|
|
228
|
+
case "wake": return taskWakeDispatch(rest, store, options);
|
|
246
229
|
case "project": return taskProjectCommand(rest, store, options);
|
|
247
230
|
case "input": return runTaskInputCommand(rest, store, options);
|
|
248
231
|
case "grant": return runGrantCommand(rest, store, options);
|
|
249
232
|
case "workflow": return runWorkflowCommand(rest, store, options);
|
|
233
|
+
case "publication": return runPublicationCommand(rest, store, options);
|
|
250
234
|
case "role": return taskRoleCommand(rest, store, options);
|
|
251
235
|
case "work": return taskWorkCommand(rest, store, options);
|
|
252
236
|
case "review": return taskReviewCommand(rest, store, options);
|
|
@@ -255,6 +239,7 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
255
239
|
case "decision": return taskDecisionCommand(rest, store, options);
|
|
256
240
|
case "milestone": return taskMilestoneCommand(rest, store, options);
|
|
257
241
|
case "event": return taskEventCommand(rest, store);
|
|
242
|
+
case "continuation": return taskContinuationCommand(rest, store);
|
|
258
243
|
case "enter": return enterTaskRoleAlias(rest, store, options);
|
|
259
244
|
default:
|
|
260
245
|
throw usageError(command === undefined
|
|
@@ -571,6 +556,8 @@ function showTaskCommand(args, store) {
|
|
|
571
556
|
const work = store.listWorkItems(task.id);
|
|
572
557
|
const changeSets = store.listChangeSets(task.id);
|
|
573
558
|
const integrations = store.listIntegrationAttempts(task.id);
|
|
559
|
+
const publications = store.listPublicationReferences(task.id);
|
|
560
|
+
const verifiedMergedPublications = publications.filter((reference) => (reference.state === "merged" && reference.verification === "verified")).length;
|
|
574
561
|
const counts = {
|
|
575
562
|
messages: messages.length,
|
|
576
563
|
decisions: decisions.length,
|
|
@@ -580,6 +567,7 @@ function showTaskCommand(args, store) {
|
|
|
580
567
|
agentRuns: store.listAgentRuns(task.id).length,
|
|
581
568
|
changeSets: changeSets.length,
|
|
582
569
|
integrations: integrations.length,
|
|
570
|
+
publications: publications.length,
|
|
583
571
|
openInputs
|
|
584
572
|
};
|
|
585
573
|
const timeZone = store.getConfig().timeZone;
|
|
@@ -615,6 +603,7 @@ function showTaskCommand(args, store) {
|
|
|
615
603
|
`Agent Runs: ${counts.agentRuns}`,
|
|
616
604
|
`ChangeSets: ${counts.changeSets}`,
|
|
617
605
|
`Integration Attempts: ${counts.integrations}`,
|
|
606
|
+
`Publication references: ${counts.publications} (${verifiedMergedPublications} verified merged)`,
|
|
618
607
|
`Open inputs: ${counts.openInputs}`,
|
|
619
608
|
`Created: ${presentTime(task.createdAt, timeZone)}`,
|
|
620
609
|
`Updated: ${presentTime(task.updatedAt, timeZone)}`
|
|
@@ -673,20 +662,10 @@ function completeTaskCommand(args, store, options) {
|
|
|
673
662
|
const activeRuns = roles
|
|
674
663
|
.map((role) => ({ role, run: tx.getActiveAgentRun(task.id, role.name) }))
|
|
675
664
|
.filter((entry) => entry.run !== null);
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const unsettledWork = tx.listWorkItems(task.id)
|
|
681
|
-
.find((item) => item.status === "pending" || item.status === "running");
|
|
682
|
-
if (unsettledWork !== undefined) {
|
|
683
|
-
throw usageError(`Task ${task.id} has unsettled work: ${unsettledWork.id}.`);
|
|
684
|
-
}
|
|
685
|
-
const continuationBlockers = blockingProviderContinuations(tx.listEvents(task.id));
|
|
686
|
-
if (continuationBlockers.length > 0) {
|
|
687
|
-
throw usageError(`Task ${task.id} has ${continuationBlockers.length} Provider continuation(s) `
|
|
688
|
-
+ "that may still write the Workspace or have an identity conflict.");
|
|
689
|
-
}
|
|
665
|
+
// The preflight (above, same transaction) already projected the full
|
|
666
|
+
// readiness via projectCompletionReadiness. The leader-Run check is
|
|
667
|
+
// actor-dependent and stays here; every other blocker is re-validated by
|
|
668
|
+
// the post-Review readiness fence below.
|
|
690
669
|
let terminalizedLeaderRun = false;
|
|
691
670
|
const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
|
|
692
671
|
if (leaderEntry !== undefined) {
|
|
@@ -730,21 +709,18 @@ function completeTaskCommand(args, store, options) {
|
|
|
730
709
|
terminalizedLeaderRun
|
|
731
710
|
};
|
|
732
711
|
}
|
|
733
|
-
// Issue 06:
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
throw usageError(`Task ${task.id} has ${blocking.length} undispositioned open P1/P2 finding(s): `
|
|
746
|
-
+ `${blocking.map(({ id }) => id).join(", ")}. `
|
|
747
|
-
+ "Disposition each finding (yui task review finding dispose) before completing the Task.");
|
|
712
|
+
// Issue 06: re-validate the full completion readiness inside the
|
|
713
|
+
// transaction (the CAS fence) after final-review preparation. This is the
|
|
714
|
+
// same pure projection `task next-action` displays, now with the finding
|
|
715
|
+
// ledger gate enabled: `fixed-pending-review` findings that the prepared
|
|
716
|
+
// Review would resolve are no longer blocked, but once no Review is needed
|
|
717
|
+
// every remaining blocker fails closed with the fresh list.
|
|
718
|
+
const readinessFacts = tx.readCompletionReadinessFacts(task.id);
|
|
719
|
+
if (readinessFacts === null)
|
|
720
|
+
throw taskNotFound(task.id);
|
|
721
|
+
const readiness = projectCompletionReadiness(readinessFacts);
|
|
722
|
+
if (!readiness.ready) {
|
|
723
|
+
throw usageError(formatCompletionBlockers(task.id, readiness.blockers));
|
|
748
724
|
}
|
|
749
725
|
const completed = completeTask(task, now, { by: actor, summary });
|
|
750
726
|
tx.saveTask(completed);
|
|
@@ -823,8 +799,9 @@ function reopenTaskCommand(args, store, options) {
|
|
|
823
799
|
const active = reopenTask(task, now);
|
|
824
800
|
tx.saveTask(active);
|
|
825
801
|
tx.clearOperatorNotification(task.id);
|
|
826
|
-
|
|
827
|
-
enqueueWork(tx,
|
|
802
|
+
const reopenedReason = wakeReason("task-reopened");
|
|
803
|
+
enqueueWork(tx, leaderMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
804
|
+
enqueueWork(tx, taskMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
828
805
|
recordTaskEvent(tx, task.id, "task.reopened", { status: active.status }, now);
|
|
829
806
|
return { task: active, changed: true };
|
|
830
807
|
});
|
|
@@ -1133,9 +1110,25 @@ function taskMessageCommand(args, store, options) {
|
|
|
1133
1110
|
return `Sent message ${result.message.id} to ${result.task.id}\n`;
|
|
1134
1111
|
}
|
|
1135
1112
|
if (command === "list") {
|
|
1136
|
-
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1113
|
+
const messageListUsage = "Task message list usage: yui task message list <id> [--after <timestamp>] [--limit <n>].";
|
|
1114
|
+
const parsed = parseTail(rest, new Set(["--after", "--limit"]), messageListUsage);
|
|
1115
|
+
exactPositionals(parsed.positionals, 1, messageListUsage);
|
|
1116
|
+
const task = requireTask(store, parsed.positionals[0]);
|
|
1117
|
+
let messages = store.listMessages(task.id);
|
|
1118
|
+
const after = optionalNonEmptyOption(parsed.options, "--after");
|
|
1119
|
+
if (after !== undefined) {
|
|
1120
|
+
const afterMs = Date.parse(after);
|
|
1121
|
+
if (!Number.isFinite(afterMs))
|
|
1122
|
+
throw usageError("--after must be a valid timestamp.", messageListUsage);
|
|
1123
|
+
messages = messages.filter((m) => Date.parse(m.createdAt) > afterMs);
|
|
1124
|
+
}
|
|
1125
|
+
const limit = optionalNonEmptyOption(parsed.options, "--limit");
|
|
1126
|
+
if (limit !== undefined) {
|
|
1127
|
+
const n = Number(limit);
|
|
1128
|
+
if (!Number.isSafeInteger(n) || n <= 0)
|
|
1129
|
+
throw usageError("--limit must be a positive integer.", messageListUsage);
|
|
1130
|
+
messages = messages.slice(-n);
|
|
1131
|
+
}
|
|
1139
1132
|
if (messages.length === 0)
|
|
1140
1133
|
return "No messages found.\n";
|
|
1141
1134
|
const timeZone = store.getConfig().timeZone;
|
|
@@ -1160,7 +1153,7 @@ function taskMessageCommand(args, store, options) {
|
|
|
1160
1153
|
* enqueues exactly one Leader wakeup with an auditable reason. The reason is
|
|
1161
1154
|
* truncated to keep the event payload compact.
|
|
1162
1155
|
*/
|
|
1163
|
-
function
|
|
1156
|
+
function taskWakeForceCommand(args, store, options) {
|
|
1164
1157
|
const usage = "Task wake usage: yui task wake <id> --force --reason <text>.";
|
|
1165
1158
|
const parsed = parseTail(args, new Set(["--reason"]), usage, new Set(["--force"]));
|
|
1166
1159
|
exactPositionals(parsed.positionals, 1, usage);
|
|
@@ -1171,13 +1164,13 @@ function taskWakeCommand(args, store, options) {
|
|
|
1171
1164
|
const now = clock(options);
|
|
1172
1165
|
const task = requireTask(store, parsed.positionals[0]);
|
|
1173
1166
|
assertTaskOpen(task);
|
|
1174
|
-
const
|
|
1167
|
+
const wakeReasonTag = wakeReason("force-wake", truncateEventNote(reason));
|
|
1175
1168
|
store.transaction((tx) => {
|
|
1176
|
-
queueLeaderWakeup(tx, task.id,
|
|
1177
|
-
recordTaskEvent(tx, task.id, "task.wake-forced", { reason:
|
|
1169
|
+
queueLeaderWakeup(tx, task.id, wakeReasonTag, now);
|
|
1170
|
+
recordTaskEvent(tx, task.id, "task.wake-forced", { reason: wakeReasonTag }, now);
|
|
1178
1171
|
});
|
|
1179
1172
|
notifyMailbox(options.runtime, leaderMailbox(task.id), task.id);
|
|
1180
|
-
return `Woke ${task.id} (${
|
|
1173
|
+
return `Woke ${task.id} (${wakeReasonTag})\n`;
|
|
1181
1174
|
}
|
|
1182
1175
|
function taskRoleCommand(args, store, options) {
|
|
1183
1176
|
const [command, ...rest] = args;
|
|
@@ -1313,6 +1306,7 @@ function listTaskRoles(args, store, options) {
|
|
|
1313
1306
|
{ header: "Health", minWidth: 6, maxWidth: 15 },
|
|
1314
1307
|
{ header: "Open input", minWidth: 5, maxWidth: 10 },
|
|
1315
1308
|
{ header: "Active work", minWidth: 10, maxWidth: 34 },
|
|
1309
|
+
{ header: "Last run", minWidth: 10, maxWidth: 28 },
|
|
1316
1310
|
{ header: "Native session", minWidth: 10, maxWidth: 28 },
|
|
1317
1311
|
{ header: "tmux", minWidth: 6, maxWidth: 22 }
|
|
1318
1312
|
], statuses.map((status) => [
|
|
@@ -1321,6 +1315,7 @@ function listTaskRoles(args, store, options) {
|
|
|
1321
1315
|
status.health,
|
|
1322
1316
|
taskRoleOpenInputLabel(status),
|
|
1323
1317
|
taskRoleActiveWorkLabel(status),
|
|
1318
|
+
taskRoleLastRunLabel(status),
|
|
1324
1319
|
taskRoleNativeSessionLabel(status),
|
|
1325
1320
|
taskRoleTmuxLabel(status)
|
|
1326
1321
|
]), defaultTableWidth())}\n`, { roles: statuses });
|
|
@@ -2806,12 +2801,14 @@ function extractReviewFindingsCommand(args, store, options) {
|
|
|
2806
2801
|
+ `${result.created.length} created, ${result.updated.length} updated, ${result.conflicts.length} conflict(s).\n`);
|
|
2807
2802
|
}
|
|
2808
2803
|
function requestTaskReviewRound(args, store, options) {
|
|
2809
|
-
const usage = "Task review request usage: yui task review request <task> --role <global-role>
|
|
2810
|
-
|
|
2804
|
+
const usage = "Task review request usage: yui task review request <task> --role <global-role> "
|
|
2805
|
+
+ "[--strategy fixed:<count>|adaptive:<max>] [--lane-role <role> ...] [--delta-recheck].";
|
|
2806
|
+
const parsed = parseMultiValueTail(args, new Set(["--role", "--strategy"]), new Set(["--lane-role"]), usage, new Set(["--delta-recheck"]));
|
|
2811
2807
|
exactPositionals(parsed.positionals, 1, usage);
|
|
2812
2808
|
const reviewerRoleName = requiredOption(parsed.options, "--role");
|
|
2813
2809
|
const requestedStrategy = parseExecutionStrategy(parsed.options.get("--strategy"), usage);
|
|
2814
2810
|
const requestedLaneRoles = parsed.multiOptions.get("--lane-role") ?? [];
|
|
2811
|
+
const deltaRecheckRequested = parsed.options.has("--delta-recheck");
|
|
2815
2812
|
const now = clock(options);
|
|
2816
2813
|
const round = store.transaction((tx) => {
|
|
2817
2814
|
const task = requireTask(tx, parsed.positionals[0]);
|
|
@@ -2824,6 +2821,17 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
2824
2821
|
throw usageError(`Task ${task.id} has no bound Projects for a Task-final Review.`);
|
|
2825
2822
|
}
|
|
2826
2823
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
2824
|
+
if (deltaRecheckRequested) {
|
|
2825
|
+
const reviewConfig = tx.getReviewConfig();
|
|
2826
|
+
if (reviewConfig === null || reviewConfig.deltaRecheck !== "enabled") {
|
|
2827
|
+
throw usageError("Delta-recheck is not enabled for this Project's review policy. "
|
|
2828
|
+
+ "Set `yui config set review --role <role> --trigger final --delta-recheck enabled` "
|
|
2829
|
+
+ "or request a full Review.");
|
|
2830
|
+
}
|
|
2831
|
+
if (taskFinalContract !== undefined) {
|
|
2832
|
+
throw usageError("Delta-recheck is not supported with a Task-final review contract.");
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2827
2835
|
if (tx.getGlobalRole(reviewerRoleName) === null) {
|
|
2828
2836
|
throw usageError(`Global Role not found: ${reviewerRoleName}.`);
|
|
2829
2837
|
}
|
|
@@ -2838,7 +2846,8 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
2838
2846
|
const exact = taskRounds.filter((entry) => (entry.reviewerRoleName === reviewerRoleName
|
|
2839
2847
|
&& (taskFinalContract === undefined || sameTaskFinalReviewContract(entry.taskFinalReviewContract, taskFinalContract))
|
|
2840
2848
|
&& isSameTaskReviewCandidate(entry.taskCandidate, provenance.candidate))).at(-1);
|
|
2841
|
-
if (exact !== undefined
|
|
2849
|
+
if (exact !== undefined
|
|
2850
|
+
&& (deltaRecheckRequested || !deltaRecheckBlocksAcceptance(exact))) {
|
|
2842
2851
|
if (exact.status === "failed") {
|
|
2843
2852
|
throw usageError(`Explicit Task-final ReviewRound ${exact.id} is failed for this exact candidate; resolve it before requesting again.`);
|
|
2844
2853
|
}
|
|
@@ -2941,7 +2950,16 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
2941
2950
|
}
|
|
2942
2951
|
assertTaskReviewRequestLane(tx, task.id, laneRole.name);
|
|
2943
2952
|
}
|
|
2944
|
-
let
|
|
2953
|
+
let deltaRecord;
|
|
2954
|
+
if (deltaRecheckRequested) {
|
|
2955
|
+
if (requestedLaneRoles.length > 0 || requestedStrategy !== undefined) {
|
|
2956
|
+
throw usageError("Delta-recheck supports only the default single Reviewer Lane.");
|
|
2957
|
+
}
|
|
2958
|
+
deltaRecord = validateDeltaRecheckRequest(tx, task.id, reviewerRoleName, provenance.candidate, options.deltaRecheckPreflight);
|
|
2959
|
+
}
|
|
2960
|
+
let created = deltaRecord === undefined
|
|
2961
|
+
? createTaskReviewRound(tx.nextReviewRoundId(task.id), task.id, anchor.item.id, anchor.candidate.id, reviewerRoleName, "leader", provenance.candidate, now, taskFinalContract)
|
|
2962
|
+
: createTaskDeltaReviewRound(tx.nextReviewRoundId(task.id), task.id, anchor.item.id, anchor.candidate.id, reviewerRoleName, "leader", provenance.candidate, deltaRecord, now, taskFinalContract);
|
|
2945
2963
|
let group = createExecutionGroup(`execution-group-${created.id}`, task.id, {
|
|
2946
2964
|
purpose: "review",
|
|
2947
2965
|
target: executionTargetForReviewRound(task, created, anchor.item, anchor.candidate),
|
|
@@ -2953,20 +2971,80 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
2953
2971
|
executionGroup: group
|
|
2954
2972
|
};
|
|
2955
2973
|
tx.saveReviewRound(task.id, created);
|
|
2974
|
+
// Issue 07: when a full Review is created after a non-accepting delta
|
|
2975
|
+
// disposition, record the escalation lineage on the delta Round.
|
|
2976
|
+
if (exact !== undefined
|
|
2977
|
+
&& deltaRecheckBlocksAcceptance(exact)
|
|
2978
|
+
&& created.deltaRecheck === undefined
|
|
2979
|
+
&& exact.deltaRecheck !== undefined
|
|
2980
|
+
&& exact.deltaRecheck.escalatedToReviewRoundId === undefined) {
|
|
2981
|
+
tx.saveReviewRound(task.id, {
|
|
2982
|
+
...exact,
|
|
2983
|
+
deltaRecheck: {
|
|
2984
|
+
...exact.deltaRecheck,
|
|
2985
|
+
escalatedToReviewRoundId: created.id
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2956
2989
|
recordTaskEvent(tx, task.id, "review.task-final-requested", {
|
|
2957
2990
|
reviewRoundId: created.id,
|
|
2958
2991
|
workItemId: created.workItemId,
|
|
2959
2992
|
candidateId: created.candidateId,
|
|
2960
2993
|
reviewerRoleName: created.reviewerRoleName,
|
|
2961
2994
|
requestedBy: created.requestedBy,
|
|
2962
|
-
taskCandidate: JSON.stringify(created.taskCandidate)
|
|
2995
|
+
taskCandidate: JSON.stringify(created.taskCandidate),
|
|
2996
|
+
...(created.deltaRecheck === undefined
|
|
2997
|
+
? {}
|
|
2998
|
+
: {
|
|
2999
|
+
deltaRecheck: "true",
|
|
3000
|
+
previousReviewRoundId: created.deltaRecheck.previousReviewRoundId,
|
|
3001
|
+
diffDigest: created.deltaRecheck.diffDigest
|
|
3002
|
+
})
|
|
2963
3003
|
}, now);
|
|
2964
3004
|
return created;
|
|
2965
3005
|
});
|
|
2966
3006
|
return output(round.status === "pending"
|
|
2967
|
-
?
|
|
3007
|
+
? round.deltaRecheck === undefined
|
|
3008
|
+
? `Task-final Review requested as ${round.id}\n`
|
|
3009
|
+
: `Task-final delta-recheck requested as ${round.id} (rechecks ${round.deltaRecheck.previousReviewRoundId})\n`
|
|
2968
3010
|
: `Task-final Review is already ${round.status}: ${round.id}\n`, { reviewRound: round });
|
|
2969
3011
|
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Issue 07: re-validates the CLI-computed delta preflight inside the store
|
|
3014
|
+
* transaction. The previous Round must be a completed acceptance (a full
|
|
3015
|
+
* Review or an equivalent-and-accepted delta) so a delta never extends a
|
|
3016
|
+
* non-accepting disposition.
|
|
3017
|
+
*/
|
|
3018
|
+
function validateDeltaRecheckRequest(store, taskId, reviewerRoleName, candidate, preflight) {
|
|
3019
|
+
if (preflight === undefined) {
|
|
3020
|
+
throw usageError("Delta-recheck assessment is missing; the CLI preflight did not run. "
|
|
3021
|
+
+ "Request a full Review or retry with a current CLI.");
|
|
3022
|
+
}
|
|
3023
|
+
const previous = store.getReviewRound(taskId, preflight.record.previousReviewRoundId);
|
|
3024
|
+
if (previous === null
|
|
3025
|
+
|| (previous.scope ?? "work-item") !== "task"
|
|
3026
|
+
|| previous.status !== "completed") {
|
|
3027
|
+
throw usageError(`Delta-recheck previous ReviewRound is not a completed Task-final Review: `
|
|
3028
|
+
+ `${preflight.record.previousReviewRoundId}.`);
|
|
3029
|
+
}
|
|
3030
|
+
if (previous.reviewerRoleName !== reviewerRoleName) {
|
|
3031
|
+
throw usageError(`Delta-recheck Reviewer Role must match the previous acceptance: `
|
|
3032
|
+
+ `${previous.reviewerRoleName}.`);
|
|
3033
|
+
}
|
|
3034
|
+
if (previous.reviewBaseCommit !== preflight.record.previousBaseCommit) {
|
|
3035
|
+
throw usageError("Delta-recheck previous base commit does not match the recorded acceptance.");
|
|
3036
|
+
}
|
|
3037
|
+
// A delta may only extend an acceptance, never a finding or an escalation.
|
|
3038
|
+
if (previous.deltaRecheck !== undefined
|
|
3039
|
+
&& previous.deltaRecheck.disposition !== "equivalent-and-accepted") {
|
|
3040
|
+
throw usageError(`Delta-recheck cannot extend ${previous.id}: its disposition is `
|
|
3041
|
+
+ `${previous.deltaRecheck.disposition}. Resolve it with a full Review first.`);
|
|
3042
|
+
}
|
|
3043
|
+
if (candidate.projects[0].commit === preflight.record.previousBaseCommit) {
|
|
3044
|
+
throw usageError("Delta-recheck candidate head is unchanged; the previous acceptance already covers it.");
|
|
3045
|
+
}
|
|
3046
|
+
return preflight.record;
|
|
3047
|
+
}
|
|
2970
3048
|
function assertTaskReviewRequestLane(store, taskId, reviewerRoleName, reusableRound) {
|
|
2971
3049
|
const reviewerMailbox = store.getWorkMailbox(roleMailbox(taskId, reviewerRoleName));
|
|
2972
3050
|
const runtimeMailbox = store.getWorkMailbox(runtimeLifecycleTarget({
|
|
@@ -3137,6 +3215,8 @@ function taskRunCommand(args, store, options) {
|
|
|
3137
3215
|
const [command, ...rest] = args;
|
|
3138
3216
|
if (command === "list")
|
|
3139
3217
|
return output(listRuns(rest, store, options));
|
|
3218
|
+
if (command === "show")
|
|
3219
|
+
return showRun(rest, store, options);
|
|
3140
3220
|
if (command === "retry")
|
|
3141
3221
|
return retryRun(rest, store, options);
|
|
3142
3222
|
if (command === "settle")
|
|
@@ -3717,7 +3797,33 @@ function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
|
3717
3797
|
// review evidence. Do not create duplicate rounds on repeated completion
|
|
3718
3798
|
// attempts. A failed round remains a blocker until the Leader changes the
|
|
3719
3799
|
// candidate or otherwise resolves the failed evidence explicitly.
|
|
3720
|
-
|
|
3800
|
+
// Issue 07: a completed delta-recheck that did not accept the head is not
|
|
3801
|
+
// final evidence. A `requires-full-review` disposition escalates to a new
|
|
3802
|
+
// full Review; a `finding` disposition stays a blocker for the Leader.
|
|
3803
|
+
if (latest.status === "completed"
|
|
3804
|
+
&& latest.deltaRecheck !== undefined
|
|
3805
|
+
&& latest.deltaRecheck.disposition === "requires-full-review") {
|
|
3806
|
+
// Fall through to queue a full Review for the same candidate.
|
|
3807
|
+
const anchor = taskFinalContract === undefined
|
|
3808
|
+
? latestTaskReviewAnchor(store, task)
|
|
3809
|
+
: latestTaskReviewContractAnchor(store, task, taskFinalContract);
|
|
3810
|
+
const escalated = queueTaskReviewRound(store, task, anchor.item, anchor.candidate.id, config, taskCandidate, options, now, establishedRound?.requestedBy ?? "policy", taskFinalContract);
|
|
3811
|
+
// Record the escalation lineage on the delta Round so the full Review
|
|
3812
|
+
// is traceable from the non-accepting delta disposition.
|
|
3813
|
+
if (latest.deltaRecheck.escalatedToReviewRoundId === undefined) {
|
|
3814
|
+
store.saveReviewRound(task.id, {
|
|
3815
|
+
...latest,
|
|
3816
|
+
deltaRecheck: {
|
|
3817
|
+
...latest.deltaRecheck,
|
|
3818
|
+
escalatedToReviewRoundId: escalated.id
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3822
|
+
return escalated;
|
|
3823
|
+
}
|
|
3824
|
+
else {
|
|
3825
|
+
return latest.status === "completed" ? null : latest;
|
|
3826
|
+
}
|
|
3721
3827
|
}
|
|
3722
3828
|
const anchor = taskFinalContract === undefined
|
|
3723
3829
|
? latestTaskReviewAnchor(store, task)
|
|
@@ -3997,11 +4103,12 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
3997
4103
|
* another input or changes a native generation from this command.
|
|
3998
4104
|
*/
|
|
3999
4105
|
function recoverRun(args, store, options) {
|
|
4000
|
-
const usage = "Task run recover usage: yui task run recover <task>/<run> --action <diagnose|retry|replace-session|terminate> --expected-progress-at <timestamp> --provider-acceptance <accepted|rejected|ambiguous> --reason <text> [--agent-id <id>] [--adapter-id <id>] [--native-session-id <id>] [--launch-id <id>].";
|
|
4106
|
+
const usage = "Task run recover usage: yui task run recover <task>/<run> --action <diagnose|retry|replace-session|terminate> (--expected-progress-at <timestamp>|--from-next-action <fingerprint>) --provider-acceptance <accepted|rejected|ambiguous> --reason <text> [--agent-id <id>] [--adapter-id <id>] [--native-session-id <id>] [--launch-id <id>].";
|
|
4001
4107
|
const parsed = parseTail(args, new Set([
|
|
4002
4108
|
"--action",
|
|
4003
4109
|
"--expected-progress-at",
|
|
4004
4110
|
"--progress-at",
|
|
4111
|
+
"--from-next-action",
|
|
4005
4112
|
"--provider-acceptance",
|
|
4006
4113
|
"--reason",
|
|
4007
4114
|
"--role",
|
|
@@ -4012,6 +4119,12 @@ function recoverRun(args, store, options) {
|
|
|
4012
4119
|
]), usage);
|
|
4013
4120
|
exactPositionals(parsed.positionals, 1, usage);
|
|
4014
4121
|
const now = clock(options);
|
|
4122
|
+
const fingerprint = parsed.options.get("--from-next-action");
|
|
4123
|
+
const explicitFence = parsed.options.get("--expected-progress-at")
|
|
4124
|
+
?? parsed.options.get("--progress-at");
|
|
4125
|
+
if (fingerprint !== undefined && explicitFence !== undefined) {
|
|
4126
|
+
throw usageError("--from-next-action and --expected-progress-at/--progress-at are mutually exclusive.", usage);
|
|
4127
|
+
}
|
|
4015
4128
|
const input = store.transaction((tx) => {
|
|
4016
4129
|
const active = requireRun(tx, parsed.positionals[0], options);
|
|
4017
4130
|
const task = requireTask(tx, active.taskId);
|
|
@@ -4027,17 +4140,41 @@ function recoverRun(args, store, options) {
|
|
|
4027
4140
|
const role = requireRole(tx, task.id, roleName);
|
|
4028
4141
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
4029
4142
|
const session = sessions?.sessions[sessions.activeAgentId];
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4143
|
+
// Issue 08: a fingerprint copied from `task run show` resolves the
|
|
4144
|
+
// canonical fence server-side. The projection is recomputed here, so a
|
|
4145
|
+
// fingerprint from stale observations matches nothing and fails closed.
|
|
4146
|
+
let plan = null;
|
|
4147
|
+
if (fingerprint !== undefined) {
|
|
4148
|
+
const facts = readRunRecoveryFacts(tx, task.id, active.id);
|
|
4149
|
+
if (facts === null)
|
|
4150
|
+
throw usageError(`Agent Run not found: ${task.id}/${active.id}.`, usage);
|
|
4151
|
+
const projection = projectRunRecovery(facts);
|
|
4152
|
+
plan = projection.actions.find((entry) => entry.fingerprint === fingerprint) ?? null;
|
|
4153
|
+
if (plan === null) {
|
|
4154
|
+
throw usageError(runRecoveryStaleDiagnosis(task.id, active.id, projection.canonicalProgressAt ?? null, "recovery action fingerprint is stale or unknown"), usage);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
const action = parseRecoveryAction(parsed.options.get("--action") ?? plan?.action, usage);
|
|
4158
|
+
if (plan !== null && plan.action !== action) {
|
|
4159
|
+
throw usageError(`--action ${action} does not match recovery fingerprint action ${plan.action}.`, usage);
|
|
4160
|
+
}
|
|
4161
|
+
const expectedProgressAt = explicitFence ?? plan?.expectedProgressAt;
|
|
4162
|
+
if (expectedProgressAt === undefined) {
|
|
4034
4163
|
throw usageError("--expected-progress-at is required.", usage);
|
|
4164
|
+
}
|
|
4035
4165
|
const providerAcceptance = parseProviderAcceptance(parsed.options.get("--provider-acceptance"), usage);
|
|
4036
|
-
const agentId = parsed.options.get("--agent-id")
|
|
4037
|
-
|
|
4166
|
+
const agentId = parsed.options.get("--agent-id")
|
|
4167
|
+
?? plan?.agentId
|
|
4168
|
+
?? active.effective.agentId;
|
|
4169
|
+
const adapterId = parsed.options.get("--adapter-id")
|
|
4170
|
+
?? plan?.adapterId
|
|
4171
|
+
?? active.effective.adapterId;
|
|
4038
4172
|
const nativeSessionId = parsed.options.get("--native-session-id")
|
|
4173
|
+
?? plan?.nativeSessionId
|
|
4039
4174
|
?? session?.nativeSessionId;
|
|
4040
|
-
const launchId = parsed.options.get("--launch-id")
|
|
4175
|
+
const launchId = parsed.options.get("--launch-id")
|
|
4176
|
+
?? plan?.launchId
|
|
4177
|
+
?? session?.launchId;
|
|
4041
4178
|
return {
|
|
4042
4179
|
taskId: task.id,
|
|
4043
4180
|
roleName: role.name,
|
|
@@ -4055,7 +4192,7 @@ function recoverRun(args, store, options) {
|
|
|
4055
4192
|
});
|
|
4056
4193
|
const result = recoverExactAgentRun(store, input);
|
|
4057
4194
|
if (result.disposition !== "applied") {
|
|
4058
|
-
throw usageError(`
|
|
4195
|
+
throw usageError(runRecoveryStaleDiagnosis(input.taskId, input.runId, result.progressAt ?? null, `exact Run recovery ${result.disposition}: ${result.reason ?? "state changed"}`), usage);
|
|
4059
4196
|
}
|
|
4060
4197
|
// The durable recovery request is committed before asking the Controller to
|
|
4061
4198
|
// wake the owning Leader; a failed transaction must not leak a signal.
|
|
@@ -4067,6 +4204,86 @@ function recoverRun(args, store, options) {
|
|
|
4067
4204
|
: "";
|
|
4068
4205
|
return `Recorded exact ${result.action} recovery for ${result.run?.id ?? "unknown Run"}.${followup}\n`;
|
|
4069
4206
|
}
|
|
4207
|
+
/**
|
|
4208
|
+
* Issue 08: every recovery rejection carries the current canonical fence and
|
|
4209
|
+
* points at the single read-only command that projects it. The caller's
|
|
4210
|
+
* side-effecting action is never retried automatically.
|
|
4211
|
+
*/
|
|
4212
|
+
function runRecoveryStaleDiagnosis(taskId, runId, canonicalProgressAt, detail) {
|
|
4213
|
+
const fence = canonicalProgressAt === null
|
|
4214
|
+
? "no durable progress timestamp is available for this Run"
|
|
4215
|
+
: `the canonical durable fence is now ${canonicalProgressAt}`;
|
|
4216
|
+
return `${detail}; ${fence}. Re-read the recovery plan: yui task run show ${taskId}/${runId}.`;
|
|
4217
|
+
}
|
|
4218
|
+
/**
|
|
4219
|
+
* Issue 08: read-only Run detail with the canonical recovery fence, Provider
|
|
4220
|
+
* evidence, and every exact recovery action. Never mutates state and never
|
|
4221
|
+
* selects an action or Provider acceptance for the Leader.
|
|
4222
|
+
*/
|
|
4223
|
+
function showRun(args, store, options) {
|
|
4224
|
+
const usage = "Task run show usage: yui task run show <task>/<run> [--json].";
|
|
4225
|
+
const asJson = args.includes("--json");
|
|
4226
|
+
const positionals = args.filter((arg) => arg !== "--json");
|
|
4227
|
+
exactPositionals(positionals, 1, usage);
|
|
4228
|
+
const data = store.transaction((tx) => {
|
|
4229
|
+
const run = requireRun(tx, positionals[0], options);
|
|
4230
|
+
const facts = readRunRecoveryFacts(tx, run.taskId, run.id);
|
|
4231
|
+
if (facts === null)
|
|
4232
|
+
throw usageError(`Agent Run not found: ${run.taskId}/${run.id}.`, usage);
|
|
4233
|
+
return { run, recovery: projectRunRecovery(facts) };
|
|
4234
|
+
});
|
|
4235
|
+
if (asJson) {
|
|
4236
|
+
return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
|
|
4237
|
+
}
|
|
4238
|
+
return { kind: "output", output: renderRunShow(data.run, data.recovery), data };
|
|
4239
|
+
}
|
|
4240
|
+
function renderRunShow(run, recovery) {
|
|
4241
|
+
const lines = [
|
|
4242
|
+
`Run: ${run.id}`,
|
|
4243
|
+
`Task: ${run.taskId}`,
|
|
4244
|
+
`Role: ${run.roleName}`,
|
|
4245
|
+
`Purpose: ${run.purpose}`,
|
|
4246
|
+
`Mode: ${run.mode}`,
|
|
4247
|
+
`Status: ${run.status}`,
|
|
4248
|
+
`Effective: ${run.effective.agentId}/${run.effective.adapterId} r${run.effective.sourceDesiredRevision}`,
|
|
4249
|
+
`Created: ${run.createdAt}`,
|
|
4250
|
+
...(run.pushedAt === undefined ? [] : [`Pushed: ${run.pushedAt}`]),
|
|
4251
|
+
...(run.deliveredAt === undefined
|
|
4252
|
+
? []
|
|
4253
|
+
: [`Provider accepted (durable): ${run.deliveredAt}`]),
|
|
4254
|
+
...(run.summary === undefined || run.summary.trim().length === 0
|
|
4255
|
+
? []
|
|
4256
|
+
: [`Summary: ${run.summary}`])
|
|
4257
|
+
];
|
|
4258
|
+
if (recovery.canonicalProgressAt !== null) {
|
|
4259
|
+
lines.push(`Canonical recovery fence (Yui durable CAS): ${recovery.canonicalProgressAt}`, ...(recovery.canonicalProgressEvidence === undefined
|
|
4260
|
+
? []
|
|
4261
|
+
: [`Fence evidence: ${recovery.canonicalProgressEvidence}`]));
|
|
4262
|
+
}
|
|
4263
|
+
if (recovery.provider.observedAt !== null) {
|
|
4264
|
+
lines.push(`Provider observation (evidence only, not a fence): `
|
|
4265
|
+
+ `${recovery.provider.observationKind} at ${recovery.provider.observedAt}`);
|
|
4266
|
+
}
|
|
4267
|
+
if (recovery.session !== null) {
|
|
4268
|
+
const session = recovery.session;
|
|
4269
|
+
lines.push(`Session: ${session.status}`
|
|
4270
|
+
+ `${session.nativeSessionId === undefined ? "" : ` ${session.nativeSessionId}`}`
|
|
4271
|
+
+ `${session.launchId === undefined ? "" : ` launch ${session.launchId}`}`);
|
|
4272
|
+
}
|
|
4273
|
+
if (recovery.recoverable) {
|
|
4274
|
+
lines.push(`Provider acceptance options: ${recovery.providerAcceptance.options.join(", ")}`, "Recovery actions (copy one; the fence is already canonical):");
|
|
4275
|
+
for (const plan of recovery.actions) {
|
|
4276
|
+
lines.push(` [${plan.action}] ${plan.reason}`, ` ${plan.command}`);
|
|
4277
|
+
}
|
|
4278
|
+
if (recovery.judgmentRequired !== undefined) {
|
|
4279
|
+
lines.push(`Judgment: ${recovery.judgmentRequired}`);
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
else {
|
|
4283
|
+
lines.push(`Not recoverable: ${recovery.reason ?? "unknown"}`);
|
|
4284
|
+
}
|
|
4285
|
+
return `${lines.join("\n")}\n`;
|
|
4286
|
+
}
|
|
4070
4287
|
/**
|
|
4071
4288
|
* Issue 04: builds the terminal yield outcome from the command inputs. The
|
|
4072
4289
|
* same construction feeds both the first commit and the idempotent replay, so
|
|
@@ -4099,7 +4316,13 @@ function buildYieldOutcome(run, inputSummary, options) {
|
|
|
4099
4316
|
...(options.executionLaneGitSnapshot === undefined
|
|
4100
4317
|
|| options.executionLaneGitSnapshot === null
|
|
4101
4318
|
? {}
|
|
4102
|
-
: { gitSnapshot: options.executionLaneGitSnapshot })
|
|
4319
|
+
: { gitSnapshot: options.executionLaneGitSnapshot }),
|
|
4320
|
+
...(yieldedReport.deltaDisposition === undefined
|
|
4321
|
+
? {}
|
|
4322
|
+
: { deltaDisposition: yieldedReport.deltaDisposition }),
|
|
4323
|
+
...(yieldedReport.deltaReasoning === undefined
|
|
4324
|
+
? {}
|
|
4325
|
+
: { deltaReasoning: yieldedReport.deltaReasoning })
|
|
4103
4326
|
}
|
|
4104
4327
|
};
|
|
4105
4328
|
}
|
|
@@ -4108,9 +4331,8 @@ function buildYieldOutcome(run, inputSummary, options) {
|
|
|
4108
4331
|
* for the same outcome, fails closed for a different outcome, or returns
|
|
4109
4332
|
* `null` to keep the legacy "already terminal" behavior.
|
|
4110
4333
|
*/
|
|
4111
|
-
function replayYieldReceipt(run, inputSummary, options) {
|
|
4112
|
-
|
|
4113
|
-
if (!config.yieldReceiptReplay)
|
|
4334
|
+
function replayYieldReceipt(run, inputSummary, options, retryConfig) {
|
|
4335
|
+
if (!retryConfig.yieldReceiptReplay)
|
|
4114
4336
|
return null;
|
|
4115
4337
|
if (run.yieldReceipt === undefined)
|
|
4116
4338
|
return null;
|
|
@@ -4167,7 +4389,7 @@ function yieldRun(args, store, options) {
|
|
|
4167
4389
|
// transaction; the receipt is immutable once committed.
|
|
4168
4390
|
const existing = requireRun(store, parsed.positionals[0], options);
|
|
4169
4391
|
if (existing.status !== "active") {
|
|
4170
|
-
const replayed = replayYieldReceipt(existing, inputSummary, options);
|
|
4392
|
+
const replayed = replayYieldReceipt(existing, inputSummary, options, providerRetryConfig(store.getConfig()));
|
|
4171
4393
|
if (replayed !== null)
|
|
4172
4394
|
return replayed;
|
|
4173
4395
|
throw usageError(`Run ${existing.id} is already terminal: ${existing.status}.`);
|
|
@@ -4292,7 +4514,15 @@ function yieldRun(args, store, options) {
|
|
|
4292
4514
|
reviewBaseCommit: round.reviewBaseCommit,
|
|
4293
4515
|
evidenceCommit: round.evidenceCommit ?? "none",
|
|
4294
4516
|
checks: round.checks?.map(({ name, outcome }) => `${name}:${outcome}`)
|
|
4295
|
-
.join(",") || "none"
|
|
4517
|
+
.join(",") || "none",
|
|
4518
|
+
...(round.deltaRecheck === undefined
|
|
4519
|
+
? {}
|
|
4520
|
+
: {
|
|
4521
|
+
reviewMode: "delta-recheck",
|
|
4522
|
+
deltaDisposition: round.deltaRecheck.disposition ?? "requires-full-review",
|
|
4523
|
+
previousReviewRoundId: round.deltaRecheck.previousReviewRoundId,
|
|
4524
|
+
diffDigest: round.deltaRecheck.diffDigest
|
|
4525
|
+
})
|
|
4296
4526
|
}, now);
|
|
4297
4527
|
}
|
|
4298
4528
|
}
|
|
@@ -4707,6 +4937,30 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4707
4937
|
const findingContext = taskScope
|
|
4708
4938
|
? buildTaskFinalReviewFindingContext(tx, taskId, round.taskCandidate).context
|
|
4709
4939
|
: "";
|
|
4940
|
+
let deltaContext = "";
|
|
4941
|
+
if (taskScope && round.deltaRecheck !== undefined) {
|
|
4942
|
+
const previousRound = tx.getReviewRound(taskId, round.deltaRecheck.previousReviewRoundId);
|
|
4943
|
+
if (previousRound === null || previousRound.status !== "completed") {
|
|
4944
|
+
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck previous ReviewRound is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
|
|
4945
|
+
}
|
|
4946
|
+
const diffByProject = options.deltaRecheckDiff;
|
|
4947
|
+
if (diffByProject === undefined) {
|
|
4948
|
+
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck diff is missing for ${round.id}; the CLI preflight did not run.`);
|
|
4949
|
+
}
|
|
4950
|
+
try {
|
|
4951
|
+
verifyDeltaRecheckDiff(round.deltaRecheck, diffByProject);
|
|
4952
|
+
}
|
|
4953
|
+
catch (error) {
|
|
4954
|
+
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck diff verification failed for ${round.id}: `
|
|
4955
|
+
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
4956
|
+
}
|
|
4957
|
+
deltaContext = buildDeltaRecheckDispatchContext({
|
|
4958
|
+
round,
|
|
4959
|
+
previousRound,
|
|
4960
|
+
diffByProject,
|
|
4961
|
+
ledgerContext: findingContext
|
|
4962
|
+
});
|
|
4963
|
+
}
|
|
4710
4964
|
const scopeLabel = taskScope ? "Task-final" : "WorkItem";
|
|
4711
4965
|
const projectPolicyPointers = task.projectBindings
|
|
4712
4966
|
.map(({ projectId }) => (`yui project show ${projectId}; yui project knowledge list ${projectId}`))
|
|
@@ -4723,10 +4977,10 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4723
4977
|
`Review workspace source: exact workspace attached to this Reviewer Lane`,
|
|
4724
4978
|
`Candidate summary: ${candidate.summary}`,
|
|
4725
4979
|
`Acceptance criteria: ${item.acceptance.length === 0 ? "none" : item.acceptance.join("; ")}`,
|
|
4726
|
-
...(taskScope ? [findingContext] : []),
|
|
4980
|
+
...(taskScope ? [deltaContext !== "" ? deltaContext : findingContext] : []),
|
|
4727
4981
|
"Start from the user's core outcome and the WorkItem intent. The candidate summary is a pointer, not proof: inspect the complete relevant change, callers, and proportionate checks.",
|
|
4728
4982
|
"Keep Yui Core lifecycle safety, generic Reviewer behavior, Project Policy/Knowledge, and the Task Contract separate. Follow Project Policy pointers from the dispatch context for project-specific checks.",
|
|
4729
|
-
...(round.scope === "task"
|
|
4983
|
+
...(round.scope === "task" && round.deltaRecheck === undefined
|
|
4730
4984
|
? ["This is the one final Task Review: inspect every bound Project at the frozen integrated heads, and report only reachable, material, actionable P1/P2 findings or bounded verification gaps."]
|
|
4731
4985
|
: []),
|
|
4732
4986
|
"You may freely edit source/tests, run local build or test commands, and optionally commit diagnostic evidence only inside this ReviewRound-owned workspace.",
|
|
@@ -5482,9 +5736,25 @@ function taskMilestoneCommand(args, store, options) {
|
|
|
5482
5736
|
function taskEventCommand(args, store) {
|
|
5483
5737
|
const [command, ...rest] = args;
|
|
5484
5738
|
if (command === "list") {
|
|
5485
|
-
|
|
5486
|
-
const
|
|
5487
|
-
|
|
5739
|
+
const eventListUsage = "Task event list usage: yui task event list <task> [--after <timestamp>] [--limit <n>].";
|
|
5740
|
+
const parsed = parseTail(rest, new Set(["--after", "--limit"]), eventListUsage);
|
|
5741
|
+
exactPositionals(parsed.positionals, 1, eventListUsage);
|
|
5742
|
+
const task = requireTask(store, parsed.positionals[0]);
|
|
5743
|
+
let events = store.listEvents(task.id);
|
|
5744
|
+
const after = optionalNonEmptyOption(parsed.options, "--after");
|
|
5745
|
+
if (after !== undefined) {
|
|
5746
|
+
const afterMs = Date.parse(after);
|
|
5747
|
+
if (!Number.isFinite(afterMs))
|
|
5748
|
+
throw usageError("--after must be a valid timestamp.", eventListUsage);
|
|
5749
|
+
events = events.filter((e) => Date.parse(e.createdAt) > afterMs);
|
|
5750
|
+
}
|
|
5751
|
+
const limit = optionalNonEmptyOption(parsed.options, "--limit");
|
|
5752
|
+
if (limit !== undefined) {
|
|
5753
|
+
const n = Number(limit);
|
|
5754
|
+
if (!Number.isSafeInteger(n) || n <= 0)
|
|
5755
|
+
throw usageError("--limit must be a positive integer.", eventListUsage);
|
|
5756
|
+
events = events.slice(-n);
|
|
5757
|
+
}
|
|
5488
5758
|
if (events.length === 0) {
|
|
5489
5759
|
return output(`No events found for ${task.id}.\n`, { taskId: task.id, events: [] });
|
|
5490
5760
|
}
|
|
@@ -5518,6 +5788,187 @@ function taskEventCommand(args, store) {
|
|
|
5518
5788
|
? "Task event command is required."
|
|
5519
5789
|
: `Unknown command: task event ${command}`);
|
|
5520
5790
|
}
|
|
5791
|
+
/**
|
|
5792
|
+
* Issue 13: native child durability visibility. A native Provider subagent is
|
|
5793
|
+
* best-effort until Yui persists its result content; once a continuation
|
|
5794
|
+
* report carries a result digest receipt the child is durable-result and its
|
|
5795
|
+
* full content stays readable through the referenced Task event.
|
|
5796
|
+
*/
|
|
5797
|
+
function taskContinuationCommand(args, store) {
|
|
5798
|
+
const [command, ...rest] = args;
|
|
5799
|
+
if (command !== "list") {
|
|
5800
|
+
throw usageError(command === undefined
|
|
5801
|
+
? "Task continuation command is required."
|
|
5802
|
+
: `Unknown command: task continuation ${command}`);
|
|
5803
|
+
}
|
|
5804
|
+
const usage = "Task continuation list usage: yui task continuation list <task> [--json].";
|
|
5805
|
+
const asJson = rest.includes("--json");
|
|
5806
|
+
const positionals = rest.filter((arg) => arg !== "--json");
|
|
5807
|
+
exactPositionals(positionals, 1, usage);
|
|
5808
|
+
const task = requireTask(store, positionals[0]);
|
|
5809
|
+
const events = store.listEvents(task.id);
|
|
5810
|
+
const continuations = projectProviderContinuations(events);
|
|
5811
|
+
const reportEvents = continuationReportEvents(events);
|
|
5812
|
+
const rows = continuations.map((continuation) => {
|
|
5813
|
+
const identity = continuation.identity;
|
|
5814
|
+
const report = [...continuation.reports].reverse()[0];
|
|
5815
|
+
const reportEvent = report === undefined
|
|
5816
|
+
? undefined
|
|
5817
|
+
: reportEvents.find((entry) => (entry.continuationId === identity.continuationId
|
|
5818
|
+
&& entry.continuationGeneration === identity.generation
|
|
5819
|
+
&& entry.reportId === report.reportId));
|
|
5820
|
+
return Object.freeze({
|
|
5821
|
+
continuationId: identity.continuationId,
|
|
5822
|
+
generation: identity.generation,
|
|
5823
|
+
driver: identity.providerNamespace,
|
|
5824
|
+
runId: continuation.runId,
|
|
5825
|
+
execution: continuation.execution,
|
|
5826
|
+
outcome: continuation.outcome,
|
|
5827
|
+
attachment: continuation.attachment,
|
|
5828
|
+
durability: continuation.durability,
|
|
5829
|
+
...(report?.resultDigest === undefined
|
|
5830
|
+
? {}
|
|
5831
|
+
: { resultDigest: report.resultDigest }),
|
|
5832
|
+
...(report?.resultSize === undefined
|
|
5833
|
+
? {}
|
|
5834
|
+
: { resultSize: report.resultSize }),
|
|
5835
|
+
...(reportEvent === undefined ? {} : { resultEvent: reportEvent.event.id }),
|
|
5836
|
+
...(continuation.settledAt === undefined ? {} : { settledAt: continuation.settledAt })
|
|
5837
|
+
});
|
|
5838
|
+
});
|
|
5839
|
+
if (asJson) {
|
|
5840
|
+
return output(`${JSON.stringify({ taskId: task.id, continuations: rows }, null, 2)}\n`, { taskId: task.id, continuations: rows });
|
|
5841
|
+
}
|
|
5842
|
+
if (rows.length === 0) {
|
|
5843
|
+
return output(`No native child continuations found for ${task.id}.\n`, { taskId: task.id, continuations: rows });
|
|
5844
|
+
}
|
|
5845
|
+
const timeZone = store.getConfig().timeZone;
|
|
5846
|
+
return output(`${renderTable(`Native child continuations: ${task.id}`, [
|
|
5847
|
+
{ header: "Child", minWidth: 8, maxWidth: 24 },
|
|
5848
|
+
{ header: "Driver", minWidth: 8, maxWidth: 24 },
|
|
5849
|
+
{ header: "Execution", minWidth: 8, maxWidth: 12 },
|
|
5850
|
+
{ header: "Outcome", minWidth: 8, maxWidth: 12 },
|
|
5851
|
+
{ header: "Durability", minWidth: 10, maxWidth: 16 },
|
|
5852
|
+
{ header: "Result", minWidth: 8, maxWidth: 24 },
|
|
5853
|
+
{ header: "Settled", minWidth: 10, maxWidth: 28 }
|
|
5854
|
+
], rows.map((row) => [
|
|
5855
|
+
row.continuationId,
|
|
5856
|
+
row.driver,
|
|
5857
|
+
row.execution,
|
|
5858
|
+
row.outcome,
|
|
5859
|
+
row.durability,
|
|
5860
|
+
row.resultEvent ?? (row.resultDigest === undefined ? "-" : `digest:${row.resultDigest.slice(0, 12)}`),
|
|
5861
|
+
...(row.settledAt === undefined ? ["-"] : [presentTime(row.settledAt, timeZone)])
|
|
5862
|
+
]), defaultTableWidth())}\n`, { taskId: task.id, continuations: rows });
|
|
5863
|
+
}
|
|
5864
|
+
function continuationReportEvents(events) {
|
|
5865
|
+
const result = [];
|
|
5866
|
+
for (const event of events) {
|
|
5867
|
+
const observation = runtimeObservationFromTaskEvent(event);
|
|
5868
|
+
if (observation !== null && observation.kind === "continuation.reported") {
|
|
5869
|
+
const continuationId = observation.fence.continuationId;
|
|
5870
|
+
const continuationGeneration = observation.fence.continuationGeneration;
|
|
5871
|
+
const reportId = observation.payload?.reportId;
|
|
5872
|
+
if (continuationId !== undefined
|
|
5873
|
+
&& continuationGeneration !== undefined
|
|
5874
|
+
&& reportId !== undefined) {
|
|
5875
|
+
result.push({ event, continuationId, continuationGeneration, reportId });
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5879
|
+
return result;
|
|
5880
|
+
}
|
|
5881
|
+
/**
|
|
5882
|
+
* Issue 04 (long-term): the durable wake ledger. `wake list` shows the
|
|
5883
|
+
* dispatch history; `wake show` returns the structured delta content for one
|
|
5884
|
+
* wake — the on-demand read the Agent uses instead of a context dump in the
|
|
5885
|
+
* wake envelope.
|
|
5886
|
+
*/
|
|
5887
|
+
function taskWakeDispatch(args, store, options) {
|
|
5888
|
+
const [subcommand] = args;
|
|
5889
|
+
if (subcommand === "list" || subcommand === "show") {
|
|
5890
|
+
return taskWakeInspectionCommand(args, store);
|
|
5891
|
+
}
|
|
5892
|
+
return output(taskWakeForceCommand(args, store, options));
|
|
5893
|
+
}
|
|
5894
|
+
/**
|
|
5895
|
+
* Issue 04 (long-term): the durable wake ledger. `wake list` shows the
|
|
5896
|
+
* dispatch history; `wake show` returns the structured delta content for one
|
|
5897
|
+
* wake — the on-demand read the Agent uses instead of a context dump in the
|
|
5898
|
+
* wake envelope.
|
|
5899
|
+
*/
|
|
5900
|
+
function taskWakeInspectionCommand(args, store) {
|
|
5901
|
+
const [command, ...rest] = args;
|
|
5902
|
+
if (command === "list") {
|
|
5903
|
+
const usage = "Task wake list usage: yui task wake list <task>.";
|
|
5904
|
+
exactPositionals(rest, 1, usage);
|
|
5905
|
+
const task = requireTask(store, rest[0]);
|
|
5906
|
+
const wakes = store.listTaskWakes(task.id);
|
|
5907
|
+
if (wakes.length === 0) {
|
|
5908
|
+
return output(`No wakes recorded for ${task.id}.\n`, { taskId: task.id, wakes: [] });
|
|
5909
|
+
}
|
|
5910
|
+
const timeZone = store.getConfig().timeZone;
|
|
5911
|
+
return output(`${renderTable(`Wakes: ${task.id}`, [
|
|
5912
|
+
{ header: "Wake", minWidth: 8, maxWidth: 18 },
|
|
5913
|
+
{ header: "Status", minWidth: 8, maxWidth: 12 },
|
|
5914
|
+
{ header: "Reasons", minWidth: 10, maxWidth: 40 },
|
|
5915
|
+
{ header: "Run", minWidth: 10, maxWidth: 20 },
|
|
5916
|
+
{ header: "Dispatched", minWidth: 10, maxWidth: 28 }
|
|
5917
|
+
], wakes.map((wake) => [
|
|
5918
|
+
wake.id,
|
|
5919
|
+
wake.status,
|
|
5920
|
+
wake.reasons.map(renderWakeReason).join(", "),
|
|
5921
|
+
wake.runId ?? "-",
|
|
5922
|
+
presentTime(wake.createdAt, timeZone)
|
|
5923
|
+
]), defaultTableWidth())}\n`, { taskId: task.id, wakes });
|
|
5924
|
+
}
|
|
5925
|
+
if (command === "show") {
|
|
5926
|
+
const usage = "Task wake show usage: yui task wake show <task> <wake>.";
|
|
5927
|
+
exactPositionals(rest, 2, usage);
|
|
5928
|
+
const task = requireTask(store, rest[0]);
|
|
5929
|
+
const wake = store.getTaskWake(task.id, rest[1]);
|
|
5930
|
+
if (wake === null)
|
|
5931
|
+
throw dataError(`Wake not found: ${rest[1]}.`);
|
|
5932
|
+
const timeZone = store.getConfig().timeZone;
|
|
5933
|
+
const fromMs = Date.parse(wake.fromCursor);
|
|
5934
|
+
const toMs = Date.parse(wake.toCursor);
|
|
5935
|
+
const inWindow = (createdAt) => {
|
|
5936
|
+
const ms = Date.parse(createdAt);
|
|
5937
|
+
return ms > fromMs && ms <= toMs;
|
|
5938
|
+
};
|
|
5939
|
+
const events = store.listEvents(task.id).filter((e) => inWindow(e.createdAt));
|
|
5940
|
+
const messages = store.listMessages(task.id).filter((m) => inWindow(m.createdAt));
|
|
5941
|
+
const runs = store.listAgentRuns(task.id).filter((r) => inWindow(r.createdAt));
|
|
5942
|
+
const lines = [
|
|
5943
|
+
`Wake: ${wake.id}`,
|
|
5944
|
+
`Task: ${task.id}`,
|
|
5945
|
+
`Status: ${wake.status}`,
|
|
5946
|
+
`Reasons: ${wake.reasons.map(renderWakeReason).join(", ")}`,
|
|
5947
|
+
`Delta window: ${wake.fromCursor} → ${wake.toCursor}`,
|
|
5948
|
+
...(wake.runId === undefined ? [] : [`Run: ${wake.runId}`]),
|
|
5949
|
+
`Dispatched: ${presentTime(wake.createdAt, timeZone)}`,
|
|
5950
|
+
...(wake.consumedAt === undefined
|
|
5951
|
+
? []
|
|
5952
|
+
: [`Consumed: ${presentTime(wake.consumedAt, timeZone)}`]),
|
|
5953
|
+
`Events (${events.length}):`,
|
|
5954
|
+
...events.map((e) => ` ${e.id} ${e.type} ${presentTime(e.createdAt, timeZone)}`),
|
|
5955
|
+
`Messages (${messages.length}):`,
|
|
5956
|
+
...messages.map((m) => ` ${m.id} [${taskMessageAuthorLabel(m.author)}] ${presentTime(m.createdAt, timeZone)}`),
|
|
5957
|
+
`Runs (${runs.length}):`,
|
|
5958
|
+
...runs.map((r) => ` ${r.id} [${r.status}/${r.purpose}] ${r.roleName} ${presentTime(r.createdAt, timeZone)}`)
|
|
5959
|
+
];
|
|
5960
|
+
return output(lines.join("\n").concat("\n"), {
|
|
5961
|
+
taskId: task.id,
|
|
5962
|
+
wake,
|
|
5963
|
+
events,
|
|
5964
|
+
messages,
|
|
5965
|
+
runs
|
|
5966
|
+
});
|
|
5967
|
+
}
|
|
5968
|
+
throw usageError(command === undefined
|
|
5969
|
+
? "Task wake command is required."
|
|
5970
|
+
: `Unknown command: task wake ${command}`);
|
|
5971
|
+
}
|
|
5521
5972
|
function parseMultiValueTail(args, valueOptions, repeatOptions, usage, flagOptions = new Set()) {
|
|
5522
5973
|
const positionals = [];
|
|
5523
5974
|
const options = new Map();
|