@zq-silk/yui 0.12.3 → 0.13.0
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/dist/cli/commandCatalog.js +2 -2
- package/dist/cli/updatePorts.js +18 -2
- package/dist/cli.js +3 -6
- package/dist/commands/durableJobCommands.js +6 -18
- package/dist/commands/executionAuditCommands.js +1 -1
- package/dist/commands/taskActor.js +35 -61
- package/dist/commands/taskCommands.js +260 -200
- package/dist/commands/taskIntegrationCommands.js +33 -24
- package/dist/commands/taskIntegrationQueueCommands.js +16 -18
- package/dist/commands/taskPublicationCommands.js +0 -4
- package/dist/commands/taskWorkspaceCommands.js +1 -1
- package/dist/context/runContextPack.js +15 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +17 -1
- package/dist/controller/jobClient.js +5 -8
- package/dist/controller/jobControl.js +59 -105
- package/dist/executor/workspacePreflightClassification.js +6 -5
- package/dist/integration/gitIntegrationService.js +3 -3
- package/dist/integration/integrationAttempt.js +13 -9
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/lifecycle/exactRunTerminalization.js +146 -6
- package/dist/milestone/milestone.js +9 -3
- package/dist/review/deltaRecheck.js +1 -1
- package/dist/review/reviewFinding.js +3 -3
- package/dist/review/reviewRound.js +11 -8
- package/dist/run/rejectedYieldAttempt.js +221 -0
- package/dist/runtime/tmuxAdapters.js +12 -3
- package/dist/scheduler/activeRoleRunDelivery.js +4 -0
- package/dist/storage/migration/productionRegistry.js +9 -0
- package/dist/storage/taskStore.js +9 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +4 -0
- package/skills/yui-operator/SKILL.md +19 -13
|
@@ -9,7 +9,7 @@ import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewR
|
|
|
9
9
|
import { reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
|
|
10
10
|
import { agentRunDeliveryReceiptId, failAgentRun, withYieldReceipt, yieldAgentRun } from "../run/agentRun.js";
|
|
11
11
|
import { createYieldReceipt } from "../run/yieldReceipt.js";
|
|
12
|
-
import { recordExecutionLaneResult } from "../execution/executionGroup.js";
|
|
12
|
+
import { recordExecutionLaneResult, resolveExecutionGroup } from "../execution/executionGroup.js";
|
|
13
13
|
import { isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
14
14
|
import { runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
|
|
15
15
|
import { runHasActiveRuntimeOperations } from "../runtime/runtimeObservation.js";
|
|
@@ -145,7 +145,7 @@ export function terminalizeExactRunReviewRound(store, input, now) {
|
|
|
145
145
|
return validation;
|
|
146
146
|
}
|
|
147
147
|
const reviewRound = validation.round;
|
|
148
|
-
|
|
148
|
+
let groupedRound = input.run.executionGroupId !== undefined
|
|
149
149
|
&& input.run.executionLaneId !== undefined
|
|
150
150
|
&& reviewRound.executionGroup !== undefined
|
|
151
151
|
? updateReviewExecutionGroup(reviewRound, recordExecutionLaneResult(reviewRound.executionGroup, input.run.executionLaneId, {
|
|
@@ -174,10 +174,25 @@ export function terminalizeExactRunReviewRound(store, input, now) {
|
|
|
174
174
|
: { gitSnapshot: input.reviewResult.gitSnapshot })
|
|
175
175
|
}, input.outcome.status === "yielded" ? "completed" : "failed", now))
|
|
176
176
|
: reviewRound;
|
|
177
|
+
if (input.settleFailedExecutionGroup === true
|
|
178
|
+
&& input.outcome.status === "failed"
|
|
179
|
+
&& groupedRound.executionGroup !== undefined
|
|
180
|
+
&& groupedRound.executionGroup.resolution === undefined
|
|
181
|
+
&& groupedRound.executionGroup.lanes.every((lane) => (lane.status === "yielded"
|
|
182
|
+
|| lane.status === "completed"
|
|
183
|
+
|| lane.status === "failed"
|
|
184
|
+
|| lane.status === "skipped"))) {
|
|
185
|
+
groupedRound = updateReviewExecutionGroup(groupedRound, resolveExecutionGroup(groupedRound.executionGroup, {
|
|
186
|
+
decision: "blocked",
|
|
187
|
+
summary: input.outcome.summary
|
|
188
|
+
}, now));
|
|
189
|
+
}
|
|
177
190
|
const groupedMultiLane = groupedRound.executionGroup !== undefined
|
|
178
191
|
&& (groupedRound.executionGroup.lanes.length > 1
|
|
179
192
|
|| groupedRound.executionGroup.strategy.mode === "adaptive");
|
|
180
|
-
if (groupedMultiLane
|
|
193
|
+
if (groupedMultiLane
|
|
194
|
+
&& groupedRound.executionGroup !== undefined
|
|
195
|
+
&& groupedRound.executionGroup.resolution === undefined) {
|
|
181
196
|
// A panel Lane only contributes evidence. The Leader must see every
|
|
182
197
|
// terminal Lane and explicitly resolve the Group before this ReviewRound
|
|
183
198
|
// can become terminal.
|
|
@@ -193,6 +208,108 @@ export function terminalizeExactRunReviewRound(store, input, now) {
|
|
|
193
208
|
}
|
|
194
209
|
return { disposition: "applied", round: terminal };
|
|
195
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Retire one stranded active Run only after its exact Provider Turn is
|
|
213
|
+
* terminal and every durable execution fence is quiet. The caller owns the
|
|
214
|
+
* surrounding aggregate transaction so the Run, ReviewRound/Lane, mailbox,
|
|
215
|
+
* Session, and append-only retirement record commit together.
|
|
216
|
+
*/
|
|
217
|
+
export function retireExactActiveAgentRun(store, input, now) {
|
|
218
|
+
const current = store.getAgentRun(input.taskId, input.runId);
|
|
219
|
+
const stateChanged = (reason) => ({
|
|
220
|
+
disposition: "state-changed",
|
|
221
|
+
run: current,
|
|
222
|
+
...(current === null ? {} : { progressAt: latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId)?.progressAt }),
|
|
223
|
+
reason
|
|
224
|
+
});
|
|
225
|
+
const task = store.getTask(input.taskId);
|
|
226
|
+
if (task === null)
|
|
227
|
+
return stateChanged("task-missing");
|
|
228
|
+
if (task.status !== "active")
|
|
229
|
+
return stateChanged("task-terminal");
|
|
230
|
+
if (current === null)
|
|
231
|
+
return stateChanged("run-missing");
|
|
232
|
+
if (current.status !== "active")
|
|
233
|
+
return stateChanged("run-terminal");
|
|
234
|
+
if (current.taskId !== input.taskId || current.roleName !== input.roleName) {
|
|
235
|
+
return stateChanged("run-owner-mismatch");
|
|
236
|
+
}
|
|
237
|
+
if (current.effective.agentId !== input.agentId
|
|
238
|
+
|| current.effective.adapterId !== input.adapterId) {
|
|
239
|
+
return stateChanged("run-launch-identity-mismatch");
|
|
240
|
+
}
|
|
241
|
+
const active = current.executionGroupId !== undefined && current.executionLaneId !== undefined
|
|
242
|
+
? store.getActiveExecutionLaneRun(input.taskId, current.executionGroupId, current.executionLaneId)
|
|
243
|
+
: store.getActiveAgentRun(input.taskId, input.roleName);
|
|
244
|
+
if (active?.id !== current.id)
|
|
245
|
+
return stateChanged("active-run-mismatch");
|
|
246
|
+
const progress = latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId);
|
|
247
|
+
if (progress === null)
|
|
248
|
+
return stateChanged("progress-unavailable");
|
|
249
|
+
if (progress.progressAt !== input.expectedProgressAt) {
|
|
250
|
+
return { ...stateChanged("progress-fence-mismatch"), progressAt: progress.progressAt };
|
|
251
|
+
}
|
|
252
|
+
if (!matchesRecoverySessionFence(store, {
|
|
253
|
+
...input,
|
|
254
|
+
action: "terminate",
|
|
255
|
+
providerAcceptance: current.deliveredAt === undefined ? "rejected" : "accepted",
|
|
256
|
+
now
|
|
257
|
+
}))
|
|
258
|
+
return stateChanged("session-or-launch-fence-mismatch");
|
|
259
|
+
const blocker = exactRecoveryExecutionBlocker(store, current);
|
|
260
|
+
if (blocker !== null) {
|
|
261
|
+
return {
|
|
262
|
+
disposition: "blocked",
|
|
263
|
+
run: current,
|
|
264
|
+
progressAt: progress.progressAt,
|
|
265
|
+
reason: blocker
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
269
|
+
const session = sessions?.sessions[input.agentId];
|
|
270
|
+
const providerBinding = sessions?.providerBinding;
|
|
271
|
+
const providerTurn = providerBinding?.turn;
|
|
272
|
+
const providerSettled = providerBinding?.runId === current.id
|
|
273
|
+
&& (providerTurn?.status === "completed"
|
|
274
|
+
|| providerTurn?.status === "failed"
|
|
275
|
+
|| providerTurn?.status === "cancelled"
|
|
276
|
+
|| providerTurn?.status === "rejected");
|
|
277
|
+
if (session?.status !== "stopped" && session?.status !== "broken" && !providerSettled) {
|
|
278
|
+
return {
|
|
279
|
+
disposition: "blocked",
|
|
280
|
+
run: current,
|
|
281
|
+
progressAt: progress.progressAt,
|
|
282
|
+
reason: "runtime-not-terminal"
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const terminal = terminalizeExactTaskRun(store, {
|
|
286
|
+
taskId: input.taskId,
|
|
287
|
+
roleName: input.roleName,
|
|
288
|
+
agentId: input.agentId,
|
|
289
|
+
runId: input.runId,
|
|
290
|
+
receiptId: agentRunDeliveryReceiptId(current),
|
|
291
|
+
...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
|
|
292
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
293
|
+
settleFailedExecutionGroup: true,
|
|
294
|
+
outcome: { status: "failed", summary: input.reason }
|
|
295
|
+
}, now);
|
|
296
|
+
if (terminal.disposition !== "applied" || terminal.run === null) {
|
|
297
|
+
return stateChanged(terminal.reason ?? "terminalization-fence-mismatch");
|
|
298
|
+
}
|
|
299
|
+
if (terminal.run.purpose === "execution" && terminal.run.workItemId !== undefined) {
|
|
300
|
+
const item = store.getWorkItem(input.taskId, terminal.run.workItemId);
|
|
301
|
+
if (item !== null
|
|
302
|
+
&& !["completed", "failed", "retired"].includes(item.status)
|
|
303
|
+
&& !workItemOwnsUnresolvedExecutionLane(item, terminal.run.executionGroupId, terminal.run.executionLaneId)) {
|
|
304
|
+
store.saveWorkItem(input.taskId, updateWorkItemStatus(item, "failed", now, input.reason));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
disposition: "applied",
|
|
309
|
+
run: terminal.run,
|
|
310
|
+
progressAt: progress.progressAt
|
|
311
|
+
};
|
|
312
|
+
}
|
|
196
313
|
/**
|
|
197
314
|
* Applies one exact application-level terminal fact inside the caller's
|
|
198
315
|
* FileTaskStore transaction. All caller-owned outcome records can therefore
|
|
@@ -279,7 +396,10 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
279
396
|
taskId: input.taskId,
|
|
280
397
|
run,
|
|
281
398
|
outcome: input.outcome,
|
|
282
|
-
reviewResult: input.reviewResult
|
|
399
|
+
reviewResult: input.reviewResult,
|
|
400
|
+
...(input.settleFailedExecutionGroup === undefined
|
|
401
|
+
? {}
|
|
402
|
+
: { settleFailedExecutionGroup: input.settleFailedExecutionGroup })
|
|
283
403
|
}, now);
|
|
284
404
|
if (reviewRoundTerminalization.disposition !== "applied") {
|
|
285
405
|
return obsolete(run, reviewRoundTerminalization.reason ?? "review-round-mismatch");
|
|
@@ -313,7 +433,7 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
313
433
|
? undefined
|
|
314
434
|
: workItemExecutionGroupById(item, run.executionGroupId);
|
|
315
435
|
if (item !== null && group !== undefined) {
|
|
316
|
-
|
|
436
|
+
let grouped = recordExecutionLaneResult(group, run.executionLaneId, {
|
|
317
437
|
summary: input.outcome.summary,
|
|
318
438
|
...(input.reviewResult?.report === undefined ? {} : { report: input.reviewResult.report }),
|
|
319
439
|
...(input.reviewResult?.checks === undefined ? {} : { checks: input.reviewResult.checks }),
|
|
@@ -322,6 +442,18 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
322
442
|
...(input.reviewResult?.evidenceCommit === undefined ? {} : { evidenceCommit: input.reviewResult.evidenceCommit }),
|
|
323
443
|
...(input.reviewResult?.gitSnapshot === undefined ? {} : { gitSnapshot: input.reviewResult.gitSnapshot })
|
|
324
444
|
}, input.outcome.status === "yielded" ? "completed" : "failed", now);
|
|
445
|
+
if (input.settleFailedExecutionGroup === true
|
|
446
|
+
&& input.outcome.status === "failed"
|
|
447
|
+
&& grouped.resolution === undefined
|
|
448
|
+
&& grouped.lanes.every((lane) => (lane.status === "yielded"
|
|
449
|
+
|| lane.status === "completed"
|
|
450
|
+
|| lane.status === "failed"
|
|
451
|
+
|| lane.status === "skipped"))) {
|
|
452
|
+
grouped = resolveExecutionGroup(grouped, {
|
|
453
|
+
decision: "blocked",
|
|
454
|
+
summary: input.outcome.summary
|
|
455
|
+
}, now);
|
|
456
|
+
}
|
|
325
457
|
store.saveWorkItem(input.taskId, updateWorkItemExecutionGroup(item, grouped, now));
|
|
326
458
|
}
|
|
327
459
|
}
|
|
@@ -643,6 +775,12 @@ function matchesSessionFence(sessions, input) {
|
|
|
643
775
|
&& inFlight.receiptId === input.receiptId);
|
|
644
776
|
}
|
|
645
777
|
function matchesLaunchFence(store, sessions, input) {
|
|
778
|
+
// A resumed Run shares the native Provider Session whose process environment
|
|
779
|
+
// was created for an earlier Run. Once that exact native Session has passed
|
|
780
|
+
// matchesSessionFence, its immutable per-launch environment is not a second
|
|
781
|
+
// Run identity. Opaque hosts still require the launch fence below.
|
|
782
|
+
if (input.nativeSessionId !== undefined)
|
|
783
|
+
return true;
|
|
646
784
|
if (input.launchId === undefined)
|
|
647
785
|
return true;
|
|
648
786
|
const session = sessions?.sessions[input.agentId];
|
|
@@ -664,7 +802,9 @@ function settleLaunchReservation(store, sessions, input) {
|
|
|
664
802
|
const mailbox = store.getWorkMailbox(target);
|
|
665
803
|
const reservation = mailbox?.processing;
|
|
666
804
|
const session = sessions?.sessions[input.agentId];
|
|
667
|
-
const launchId = input.
|
|
805
|
+
const launchId = input.nativeSessionId === undefined
|
|
806
|
+
? input.launchId ?? session?.launchId
|
|
807
|
+
: session?.launchId;
|
|
668
808
|
if (launchId === undefined || !isRuntimeLaunchReservation(reservation, launchId))
|
|
669
809
|
return;
|
|
670
810
|
const settled = completeProcessing(mailbox, reservation.batchId);
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
2
|
-
export function createMilestone(id, taskId, title, summary, now) {
|
|
2
|
+
export function createMilestone(id, taskId, title, summary, createdBy, now) {
|
|
3
3
|
return {
|
|
4
|
-
schemaVersion:
|
|
4
|
+
schemaVersion: 2,
|
|
5
5
|
id: validateTaskRecordReference({ taskId, localId: id }, "milestone").localId,
|
|
6
6
|
taskId: requireSafeIdentity(taskId, "Task id"),
|
|
7
7
|
title: requireText(title, "Milestone title"),
|
|
8
8
|
summary: requireText(summary, "Milestone summary"),
|
|
9
|
-
createdBy:
|
|
9
|
+
createdBy: requireTaskControlActor(createdBy),
|
|
10
10
|
createdAt: now.toISOString()
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
+
function requireTaskControlActor(value) {
|
|
14
|
+
if (value !== "user" && value !== "operator" && value !== "leader") {
|
|
15
|
+
throw new Error(`Milestone createdBy is invalid: ${String(value)}.`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
13
19
|
function requireSafeIdentity(value, label) {
|
|
14
20
|
const normalized = requireText(value, label);
|
|
15
21
|
if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
|
|
@@ -127,7 +127,7 @@ export function buildDeltaRecheckDispatchContext(input) {
|
|
|
127
127
|
"- finding: the diff introduces a material problem; report it as a finding.",
|
|
128
128
|
"- requires-full-review: you cannot prove equivalence (uncertainty, cross-scope,",
|
|
129
129
|
" semantic change, evidence doubt). This is the safe default.",
|
|
130
|
-
"Yui verified only the technical delta boundary;
|
|
130
|
+
"Yui verified only the technical delta boundary; a Task-control Agent selected this mode.",
|
|
131
131
|
`Previous accepted ReviewRound: ${previousRound.id}@${record.previousBaseCommit}`,
|
|
132
132
|
`Previous acceptance summary: ${compact(previousRound.summary ?? previousRound.report ?? "")}`,
|
|
133
133
|
...(round.taskCandidate?.projects.map((project) => {
|
|
@@ -6,8 +6,8 @@ export const BLOCKING_FINDING_DISPOSITIONS = [
|
|
|
6
6
|
"open",
|
|
7
7
|
"fixed-pending-review"
|
|
8
8
|
];
|
|
9
|
-
/** Dispositions
|
|
10
|
-
export const
|
|
9
|
+
/** Dispositions a Task-control Agent may set explicitly. */
|
|
10
|
+
export const TASK_CONTROL_FINDING_DISPOSITIONS = [
|
|
11
11
|
"fixed-pending-review",
|
|
12
12
|
"verified-fixed",
|
|
13
13
|
"accepted-risk",
|
|
@@ -86,7 +86,7 @@ export function createReviewFinding(id, taskId, input, now) {
|
|
|
86
86
|
*/
|
|
87
87
|
export function disposeReviewFinding(finding, input) {
|
|
88
88
|
validateReviewFinding(finding);
|
|
89
|
-
if (!
|
|
89
|
+
if (!TASK_CONTROL_FINDING_DISPOSITIONS.includes(input.disposition)) {
|
|
90
90
|
throw new Error(`ReviewFinding disposition is not a Leader decision: ${input.disposition}.`);
|
|
91
91
|
}
|
|
92
92
|
if (finding.disposition !== "open" && finding.disposition !== "fixed-pending-review") {
|
|
@@ -5,7 +5,7 @@ import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
|
5
5
|
import { assertExecutionGroupTransition, resetReviewExecutionLane, validateExecutionGroup } from "../execution/executionGroup.js";
|
|
6
6
|
export function createReviewRound(id, taskId, workItemId, candidateId, reviewerRoleName, requestedBy, reviewBaseCommit, now, executionGroup) {
|
|
7
7
|
return validateReviewRound({
|
|
8
|
-
schemaVersion:
|
|
8
|
+
schemaVersion: 6,
|
|
9
9
|
id: requireIdentity(id, "ReviewRound id"),
|
|
10
10
|
taskId: requireIdentity(taskId, "Task id"),
|
|
11
11
|
workItemId: requireIdentity(workItemId, "Work Item id"),
|
|
@@ -21,7 +21,7 @@ export function createReviewRound(id, taskId, workItemId, candidateId, reviewerR
|
|
|
21
21
|
export function createTaskReviewRound(id, taskId, reviewerRoleName, requestedBy, taskCandidate, now, taskFinalReviewContract, executionGroup) {
|
|
22
22
|
const candidate = validateTaskReviewCandidate(taskCandidate);
|
|
23
23
|
return validateReviewRound({
|
|
24
|
-
schemaVersion:
|
|
24
|
+
schemaVersion: 6,
|
|
25
25
|
id: requireIdentity(id, "ReviewRound id"),
|
|
26
26
|
taskId: requireIdentity(taskId, "Task id"),
|
|
27
27
|
reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
|
|
@@ -47,7 +47,7 @@ export function createTaskReviewRound(id, taskId, reviewerRoleName, requestedBy,
|
|
|
47
47
|
export function createTaskDeltaReviewRound(id, taskId, reviewerRoleName, requestedBy, taskCandidate, deltaRecheck, now, taskFinalReviewContract, executionGroup) {
|
|
48
48
|
const candidate = validateTaskReviewCandidate(taskCandidate);
|
|
49
49
|
return validateReviewRound({
|
|
50
|
-
schemaVersion:
|
|
50
|
+
schemaVersion: 6,
|
|
51
51
|
id: requireIdentity(id, "ReviewRound id"),
|
|
52
52
|
taskId: requireIdentity(taskId, "Task id"),
|
|
53
53
|
reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
|
|
@@ -138,7 +138,7 @@ export function finishReviewRound(round, status, summary, now, result = {}) {
|
|
|
138
138
|
* returns to pending so infrastructure retries do not manufacture a new
|
|
139
139
|
* semantic ReviewRound or duplicate findings.
|
|
140
140
|
*/
|
|
141
|
-
export function retryTaskReviewRound(round, executionLaneId) {
|
|
141
|
+
export function retryTaskReviewRound(round, requestedBy, executionLaneId) {
|
|
142
142
|
validateReviewRound(round);
|
|
143
143
|
if ((round.scope ?? "work-item") !== "task") {
|
|
144
144
|
throw new Error(`Only a Task-final ReviewRound can be retried in place: ${round.id}.`);
|
|
@@ -180,7 +180,7 @@ export function retryTaskReviewRound(round, executionLaneId) {
|
|
|
180
180
|
// Keep the historical attempt Group and Lane addressable from AgentRun
|
|
181
181
|
// history while resetting the Lane for another dispatch attempt.
|
|
182
182
|
...(retryExecutionGroup === undefined ? {} : { executionGroup: retryExecutionGroup }),
|
|
183
|
-
requestedBy:
|
|
183
|
+
requestedBy: validateReviewRequestSource(requestedBy),
|
|
184
184
|
status: "pending",
|
|
185
185
|
...(round.workspace === undefined ? {} : { workspace: round.workspace }),
|
|
186
186
|
createdAt: round.createdAt
|
|
@@ -359,8 +359,8 @@ export function updateReviewExecutionGroup(round, executionGroup) {
|
|
|
359
359
|
return validateReviewRound({ ...round, executionGroup });
|
|
360
360
|
}
|
|
361
361
|
export function validateReviewRound(round) {
|
|
362
|
-
if (round.schemaVersion !==
|
|
363
|
-
throw new Error("ReviewRound must use schemaVersion
|
|
362
|
+
if (round.schemaVersion !== 6)
|
|
363
|
+
throw new Error("ReviewRound must use schemaVersion 6.");
|
|
364
364
|
validateTaskRecordReference({ taskId: round.taskId, localId: round.id }, "reviewRound");
|
|
365
365
|
requireIdentity(round.reviewerRoleName, "Reviewer Role");
|
|
366
366
|
requireCommit(round.reviewBaseCommit, "Review base commit");
|
|
@@ -637,7 +637,10 @@ function requireCommit(value, label) {
|
|
|
637
637
|
return commit;
|
|
638
638
|
}
|
|
639
639
|
function validateReviewRequestSource(source) {
|
|
640
|
-
if (source !== "policy"
|
|
640
|
+
if (source !== "policy"
|
|
641
|
+
&& source !== "user"
|
|
642
|
+
&& source !== "operator"
|
|
643
|
+
&& source !== "leader") {
|
|
641
644
|
throw new Error(`Review request source is invalid: ${String(source)}.`);
|
|
642
645
|
}
|
|
643
646
|
return source;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { contextContentDigest, validateContextSnapshotRef } from "../context/contextSnapshot.js";
|
|
2
|
+
import { computeYieldOutcomeDigest } from "./yieldReceipt.js";
|
|
3
|
+
export const RUN_YIELD_REJECTED_EVENT = "run.yield-rejected";
|
|
4
|
+
const MAX_SUMMARY_CHARACTERS = 2_000;
|
|
5
|
+
const MAX_REPORT_CHARACTERS = 8_000;
|
|
6
|
+
const MAX_CHECKS = 16;
|
|
7
|
+
const MAX_FINDINGS = 16;
|
|
8
|
+
const MAX_EVIDENCE = 16;
|
|
9
|
+
const MAX_LABEL_CHARACTERS = 128;
|
|
10
|
+
const MAX_DETAILS_CHARACTERS = 512;
|
|
11
|
+
const MAX_EVIDENCE_CHARACTERS = 512;
|
|
12
|
+
/**
|
|
13
|
+
* Creates the bounded diagnostic projection persisted when a Reviewer report
|
|
14
|
+
* cannot cross the exact terminalization fence. The full validated submission
|
|
15
|
+
* participates in the immutable digest, while only known, size-bounded fields
|
|
16
|
+
* are retained as non-authoritative evidence.
|
|
17
|
+
*/
|
|
18
|
+
export function createRejectedYieldAttempt(input) {
|
|
19
|
+
const reviewResult = input.reviewResult;
|
|
20
|
+
const fullReport = reviewResult?.report ?? input.summary;
|
|
21
|
+
const contentDigest = computeYieldOutcomeDigest({
|
|
22
|
+
status: "yielded",
|
|
23
|
+
summary: input.summary,
|
|
24
|
+
...(reviewResult === undefined ? {} : { reviewResult })
|
|
25
|
+
});
|
|
26
|
+
const summary = boundedText(input.summary, MAX_SUMMARY_CHARACTERS);
|
|
27
|
+
const report = boundedText(fullReport, MAX_REPORT_CHARACTERS);
|
|
28
|
+
const checks = (reviewResult?.checks ?? []).slice(0, MAX_CHECKS).map((check) => ({
|
|
29
|
+
name: boundedText(check.name, MAX_LABEL_CHARACTERS),
|
|
30
|
+
outcome: check.outcome,
|
|
31
|
+
...(check.details === undefined
|
|
32
|
+
? {}
|
|
33
|
+
: { details: boundedText(check.details, MAX_DETAILS_CHARACTERS) })
|
|
34
|
+
}));
|
|
35
|
+
const findings = (reviewResult?.findings ?? []).slice(0, MAX_FINDINGS).map((finding) => ({
|
|
36
|
+
id: boundedText(finding.id, MAX_LABEL_CHARACTERS),
|
|
37
|
+
severity: finding.severity,
|
|
38
|
+
status: finding.status,
|
|
39
|
+
summary: boundedText(finding.summary, MAX_DETAILS_CHARACTERS)
|
|
40
|
+
}));
|
|
41
|
+
const evidence = (reviewResult?.evidence ?? []).slice(0, MAX_EVIDENCE)
|
|
42
|
+
.map((entry) => boundedText(entry, MAX_EVIDENCE_CHARACTERS));
|
|
43
|
+
const deltaReasoning = reviewResult?.deltaReasoning === undefined
|
|
44
|
+
? undefined
|
|
45
|
+
: boundedText(reviewResult.deltaReasoning, MAX_DETAILS_CHARACTERS);
|
|
46
|
+
const projectionTruncated = summary !== input.summary
|
|
47
|
+
|| report !== fullReport
|
|
48
|
+
|| checks.length !== (reviewResult?.checks?.length ?? 0)
|
|
49
|
+
|| checks.some((check, index) => (check.name !== reviewResult?.checks?.[index]?.name
|
|
50
|
+
|| check.details !== reviewResult?.checks?.[index]?.details))
|
|
51
|
+
|| findings.length !== (reviewResult?.findings?.length ?? 0)
|
|
52
|
+
|| findings.some((finding, index) => (finding.id !== reviewResult?.findings?.[index]?.id
|
|
53
|
+
|| finding.summary !== reviewResult?.findings?.[index]?.summary))
|
|
54
|
+
|| evidence.length !== (reviewResult?.evidence?.length ?? 0)
|
|
55
|
+
|| evidence.some((entry, index) => entry !== reviewResult?.evidence?.[index])
|
|
56
|
+
|| deltaReasoning !== reviewResult?.deltaReasoning
|
|
57
|
+
|| reviewResult?.gitSnapshot !== undefined;
|
|
58
|
+
const observed = Object.freeze({
|
|
59
|
+
nativeSessionId: input.nativeSessionId ?? null,
|
|
60
|
+
launchId: input.launchId ?? null,
|
|
61
|
+
durableNativeSessionId: input.durableNativeSessionId ?? null,
|
|
62
|
+
durableLaunchId: input.durableLaunchId ?? null,
|
|
63
|
+
inFlightRunId: input.inFlightRunId ?? null,
|
|
64
|
+
inFlightReceiptId: input.inFlightReceiptId ?? null,
|
|
65
|
+
activeRunId: input.activeRun?.id ?? null,
|
|
66
|
+
activeReceiptId: input.activeRun?.receiptId ?? null,
|
|
67
|
+
contextSnapshot: input.contextSnapshot ?? null,
|
|
68
|
+
activeContextSnapshot: input.activeRun?.contextSnapshot ?? null
|
|
69
|
+
});
|
|
70
|
+
const attemptDigest = contextContentDigest({
|
|
71
|
+
taskId: input.taskId,
|
|
72
|
+
runId: input.runId,
|
|
73
|
+
reviewRoundId: input.reviewRoundId,
|
|
74
|
+
receiptId: input.receiptId,
|
|
75
|
+
rejectionReason: input.rejectionReason,
|
|
76
|
+
contentDigest,
|
|
77
|
+
observed
|
|
78
|
+
});
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
schemaVersion: 1,
|
|
81
|
+
authority: "unaccepted",
|
|
82
|
+
semanticStatus: "diagnostic-only",
|
|
83
|
+
taskId: input.taskId,
|
|
84
|
+
runId: input.runId,
|
|
85
|
+
roleName: input.roleName,
|
|
86
|
+
purpose: "review",
|
|
87
|
+
reviewRoundId: input.reviewRoundId,
|
|
88
|
+
receiptId: input.receiptId,
|
|
89
|
+
rejectionReason: input.rejectionReason,
|
|
90
|
+
summary,
|
|
91
|
+
report,
|
|
92
|
+
checks: Object.freeze(checks),
|
|
93
|
+
findings: Object.freeze(findings),
|
|
94
|
+
evidence: Object.freeze(evidence),
|
|
95
|
+
...(reviewResult?.evidenceCommit === undefined
|
|
96
|
+
? {}
|
|
97
|
+
: { evidenceCommit: reviewResult.evidenceCommit }),
|
|
98
|
+
...(reviewResult?.deltaDisposition === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: { deltaDisposition: reviewResult.deltaDisposition }),
|
|
101
|
+
...(deltaReasoning === undefined ? {} : { deltaReasoning }),
|
|
102
|
+
projectionTruncated,
|
|
103
|
+
observed,
|
|
104
|
+
attemptedAt: input.attemptedAt.toISOString(),
|
|
105
|
+
contentDigest,
|
|
106
|
+
attemptDigest
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export function rejectedYieldAttemptEventPayload(attempt) {
|
|
110
|
+
return {
|
|
111
|
+
runId: attempt.runId,
|
|
112
|
+
roleName: attempt.roleName,
|
|
113
|
+
reviewRoundId: attempt.reviewRoundId,
|
|
114
|
+
receiptId: attempt.receiptId,
|
|
115
|
+
rejectionReason: attempt.rejectionReason,
|
|
116
|
+
authority: attempt.authority,
|
|
117
|
+
semanticStatus: attempt.semanticStatus,
|
|
118
|
+
contentDigest: attempt.contentDigest,
|
|
119
|
+
attemptDigest: attempt.attemptDigest,
|
|
120
|
+
attempt: JSON.stringify(attempt)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Reads only canonical records emitted by rejectedYieldAttemptEventPayload. */
|
|
124
|
+
export function rejectedYieldAttemptFromTaskEvent(event) {
|
|
125
|
+
if (event.type !== RUN_YIELD_REJECTED_EVENT)
|
|
126
|
+
return null;
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(event.payload.attempt ?? "");
|
|
129
|
+
if (!isRejectedYieldAttempt(parsed))
|
|
130
|
+
return null;
|
|
131
|
+
const expectedAttemptDigest = contextContentDigest({
|
|
132
|
+
taskId: parsed.taskId,
|
|
133
|
+
runId: parsed.runId,
|
|
134
|
+
reviewRoundId: parsed.reviewRoundId,
|
|
135
|
+
receiptId: parsed.receiptId,
|
|
136
|
+
rejectionReason: parsed.rejectionReason,
|
|
137
|
+
contentDigest: parsed.contentDigest,
|
|
138
|
+
observed: parsed.observed
|
|
139
|
+
});
|
|
140
|
+
if (parsed.schemaVersion !== 1
|
|
141
|
+
|| parsed.authority !== "unaccepted"
|
|
142
|
+
|| parsed.semanticStatus !== "diagnostic-only"
|
|
143
|
+
|| parsed.purpose !== "review"
|
|
144
|
+
|| parsed.taskId !== event.taskId
|
|
145
|
+
|| parsed.runId !== event.payload.runId
|
|
146
|
+
|| parsed.roleName !== event.payload.roleName
|
|
147
|
+
|| parsed.reviewRoundId !== event.payload.reviewRoundId
|
|
148
|
+
|| parsed.receiptId !== event.payload.receiptId
|
|
149
|
+
|| parsed.rejectionReason !== event.payload.rejectionReason
|
|
150
|
+
|| parsed.contentDigest !== event.payload.contentDigest
|
|
151
|
+
|| parsed.attemptDigest !== event.payload.attemptDigest
|
|
152
|
+
|| parsed.attemptDigest !== expectedAttemptDigest
|
|
153
|
+
|| parsed.attemptedAt !== event.createdAt
|
|
154
|
+
|| !/^[a-f0-9]{64}$/u.test(parsed.contentDigest)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return parsed;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function isRejectedYieldAttempt(value) {
|
|
164
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
165
|
+
return false;
|
|
166
|
+
const attempt = value;
|
|
167
|
+
const observed = attempt.observed;
|
|
168
|
+
if (typeof observed !== "object" || observed === null || Array.isArray(observed))
|
|
169
|
+
return false;
|
|
170
|
+
const nullableObserved = [
|
|
171
|
+
observed.nativeSessionId,
|
|
172
|
+
observed.launchId,
|
|
173
|
+
observed.durableNativeSessionId,
|
|
174
|
+
observed.durableLaunchId,
|
|
175
|
+
observed.inFlightRunId,
|
|
176
|
+
observed.inFlightReceiptId,
|
|
177
|
+
observed.activeRunId,
|
|
178
|
+
observed.activeReceiptId
|
|
179
|
+
];
|
|
180
|
+
if (nullableObserved.some((entry) => entry !== null && typeof entry !== "string")) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
if (observed.contextSnapshot !== null)
|
|
184
|
+
validateContextSnapshotRef(observed.contextSnapshot);
|
|
185
|
+
if (observed.activeContextSnapshot !== null) {
|
|
186
|
+
validateContextSnapshotRef(observed.activeContextSnapshot);
|
|
187
|
+
}
|
|
188
|
+
return typeof attempt.taskId === "string"
|
|
189
|
+
&& typeof attempt.runId === "string"
|
|
190
|
+
&& typeof attempt.roleName === "string"
|
|
191
|
+
&& typeof attempt.reviewRoundId === "string"
|
|
192
|
+
&& typeof attempt.receiptId === "string"
|
|
193
|
+
&& typeof attempt.rejectionReason === "string"
|
|
194
|
+
&& typeof attempt.summary === "string"
|
|
195
|
+
&& typeof attempt.report === "string"
|
|
196
|
+
&& typeof attempt.projectionTruncated === "boolean"
|
|
197
|
+
&& typeof attempt.attemptedAt === "string"
|
|
198
|
+
&& typeof attempt.contentDigest === "string"
|
|
199
|
+
&& typeof attempt.attemptDigest === "string"
|
|
200
|
+
&& Array.isArray(attempt.checks)
|
|
201
|
+
&& attempt.checks.every((check) => (typeof check === "object" && check !== null
|
|
202
|
+
&& typeof check.name === "string"
|
|
203
|
+
&& ["passed", "failed", "skipped"].includes(check.outcome)
|
|
204
|
+
&& (check.details === undefined || typeof check.details === "string")))
|
|
205
|
+
&& Array.isArray(attempt.findings)
|
|
206
|
+
&& attempt.findings.every((finding) => (typeof finding === "object" && finding !== null
|
|
207
|
+
&& typeof finding.id === "string"
|
|
208
|
+
&& typeof finding.summary === "string"))
|
|
209
|
+
&& Array.isArray(attempt.evidence)
|
|
210
|
+
&& attempt.evidence.every((entry) => typeof entry === "string");
|
|
211
|
+
}
|
|
212
|
+
function boundedText(value, maximum) {
|
|
213
|
+
const safe = value
|
|
214
|
+
.replaceAll(/\u001B\][^\u0007]*(?:\u0007|\u001B\\|\u009C)/gu, " ")
|
|
215
|
+
.replaceAll(/\u001B\[[0-?]*[ -/]*[@-~]/gu, " ")
|
|
216
|
+
.replaceAll(/[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/gu, " ");
|
|
217
|
+
const characters = Array.from(safe);
|
|
218
|
+
if (characters.length <= maximum)
|
|
219
|
+
return safe;
|
|
220
|
+
return `${characters.slice(0, maximum - 1).join("")}…`;
|
|
221
|
+
}
|
|
@@ -183,9 +183,18 @@ export class TmuxSessionHost {
|
|
|
183
183
|
: { environment: request.environment }),
|
|
184
184
|
...(request.mode === "resume" ? { nativeSessionId: request.nativeSessionId } : {})
|
|
185
185
|
};
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
let planned;
|
|
187
|
+
try {
|
|
188
|
+
planned = request.owner.scope === "task"
|
|
189
|
+
? this.planner.plan({ taskId: request.owner.taskId, ...input })
|
|
190
|
+
: this.planner.planGlobalRole(input);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
throw toRuntimeLaunchFailure(error, "validation", {
|
|
194
|
+
cwd: request.workspace,
|
|
195
|
+
agentId: request.agentId
|
|
196
|
+
});
|
|
197
|
+
}
|
|
189
198
|
if (planned.role.name !== request.owner.roleName) {
|
|
190
199
|
throw new Error("Planned Role does not match the runtime owner.");
|
|
191
200
|
}
|
|
@@ -304,6 +304,9 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
304
304
|
run,
|
|
305
305
|
session: existingSession,
|
|
306
306
|
summary: error.message,
|
|
307
|
+
leaderRecovery: role.name === "leader" && error.diagnostic.kind === "config"
|
|
308
|
+
? "blocked"
|
|
309
|
+
: "automatic",
|
|
307
310
|
now
|
|
308
311
|
});
|
|
309
312
|
results.push({
|
|
@@ -375,6 +378,7 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
375
378
|
run,
|
|
376
379
|
session: existingSession,
|
|
377
380
|
summary: `Role Run could not start: ${message}`,
|
|
381
|
+
leaderRecovery: "automatic",
|
|
378
382
|
now
|
|
379
383
|
});
|
|
380
384
|
results.push({
|
|
@@ -57,6 +57,8 @@ const REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION = 3;
|
|
|
57
57
|
const REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION = 4;
|
|
58
58
|
const REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION = 4;
|
|
59
59
|
const REVIEW_ROUND_TASK_ANCHOR_TO_VERSION = 5;
|
|
60
|
+
const REVIEW_ROUND_ACTOR_FROM_VERSION = 5;
|
|
61
|
+
const REVIEW_ROUND_ACTOR_TO_VERSION = 6;
|
|
60
62
|
const ACTIVE_RUN_POINTER_FROM_VERSION = 1;
|
|
61
63
|
const ACTIVE_RUN_POINTER_TO_VERSION = 2;
|
|
62
64
|
const ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION = 2;
|
|
@@ -69,6 +71,10 @@ const INTEGRATION_ATTEMPT_FROM_VERSION = 2;
|
|
|
69
71
|
const INTEGRATION_ATTEMPT_TO_VERSION = 3;
|
|
70
72
|
const INTEGRATION_ATTEMPT_GATE_IDENTITY_FROM_VERSION = 3;
|
|
71
73
|
const INTEGRATION_ATTEMPT_GATE_IDENTITY_TO_VERSION = 4;
|
|
74
|
+
const INTEGRATION_ATTEMPT_ACTOR_FROM_VERSION = 4;
|
|
75
|
+
const INTEGRATION_ATTEMPT_ACTOR_TO_VERSION = 5;
|
|
76
|
+
const MILESTONE_ACTOR_FROM_VERSION = 1;
|
|
77
|
+
const MILESTONE_ACTOR_TO_VERSION = 2;
|
|
72
78
|
const INTEGRATION_QUEUE_FROM_VERSION = 0;
|
|
73
79
|
const INTEGRATION_QUEUE_TO_VERSION = 1;
|
|
74
80
|
const CONTEXT_SNAPSHOT_FROM_VERSION = 0;
|
|
@@ -174,12 +180,15 @@ export function createProductionStorageRegistry() {
|
|
|
174
180
|
.registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_FROM_VERSION, REVIEW_ROUND_TO_VERSION, "reviewRounds"))
|
|
175
181
|
.registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION, REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION, "reviewRounds"))
|
|
176
182
|
.registerOfflineMigration(reviewRoundTaskAnchorStep())
|
|
183
|
+
.registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_ACTOR_FROM_VERSION, REVIEW_ROUND_ACTOR_TO_VERSION, "reviewRounds"))
|
|
177
184
|
.registerOfflineMigration(recordFamilyStep("activeRunPointer", ACTIVE_RUN_POINTER_FROM_VERSION, ACTIVE_RUN_POINTER_TO_VERSION, "activeRuns"))
|
|
178
185
|
.registerOfflineMigration(managedWorkspaceFamilyStep())
|
|
179
186
|
.registerOfflineMigration(activeRunPointerNamespaceStep())
|
|
180
187
|
.registerOfflineMigration(changeSetManifestStep())
|
|
181
188
|
.registerOfflineMigration(integrationAttemptSupersededStep())
|
|
182
189
|
.registerOfflineMigration(recordFamilyStep("integrationAttempt", INTEGRATION_ATTEMPT_GATE_IDENTITY_FROM_VERSION, INTEGRATION_ATTEMPT_GATE_IDENTITY_TO_VERSION, "integrationAttempts"))
|
|
190
|
+
.registerOfflineMigration(recordFamilyStep("integrationAttempt", INTEGRATION_ATTEMPT_ACTOR_FROM_VERSION, INTEGRATION_ATTEMPT_ACTOR_TO_VERSION, "integrationAttempts"))
|
|
191
|
+
.registerOfflineMigration(recordFamilyStep("milestone", MILESTONE_ACTOR_FROM_VERSION, MILESTONE_ACTOR_TO_VERSION, "milestones"))
|
|
183
192
|
.registerOfflineMigration(integrationQueueIntroductionStep())
|
|
184
193
|
.registerOfflineMigration(contextSnapshotIntroductionStep())
|
|
185
194
|
.registerCompatible(storedTaskDurableJobsStep())
|