@zq-silk/yui 0.12.4 → 0.13.1
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/updatePorts.js +18 -2
- package/dist/cli.js +3 -6
- package/dist/commands/durableJobCommands.js +5 -17
- package/dist/commands/executionAuditCommands.js +1 -1
- package/dist/commands/taskActor.js +17 -61
- package/dist/commands/taskCommands.js +44 -166
- package/dist/commands/taskIntegrationCommands.js +7 -9
- package/dist/commands/taskIntegrationQueueCommands.js +9 -11
- package/dist/commands/taskPublicationCommands.js +0 -4
- package/dist/commands/taskWorkspaceCommands.js +1 -1
- package/dist/controller/jobClient.js +5 -8
- package/dist/controller/jobControl.js +59 -105
- package/dist/integration/gitIntegrationService.js +3 -3
- package/dist/integration/integrationAttempt.js +13 -9
- package/dist/integration/integrationQueueService.js +7 -7
- 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/storage/migration/productionRegistry.js +9 -0
- package/dist/storage/taskStore.js +9 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +2 -2
- package/dist/storage/upgradeCoordination.js +6 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +4 -0
- package/skills/yui-operator/SKILL.md +19 -13
|
@@ -11,8 +11,7 @@ import { execSync } from "node:child_process";
|
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { resolve, sep } from "node:path";
|
|
13
13
|
import { acknowledgeUnknownDurableJob, createDurableJob, durableJobIdempotencyKey, isDurableJobTerminal, requestDurableJobCancel, retryDurableJobIdempotencyKey } from "../job/durableJob.js";
|
|
14
|
-
import {
|
|
15
|
-
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
14
|
+
import { activeLiveRoleAgentSession } from "../executor/agentExecutor.js";
|
|
16
15
|
export function createDurableJobControl(store) {
|
|
17
16
|
return {
|
|
18
17
|
startJob(params, now) {
|
|
@@ -63,7 +62,7 @@ export function createDurableJobControl(store) {
|
|
|
63
62
|
return null;
|
|
64
63
|
// rr8: Bind the cancel request to the caller's managed identity. The
|
|
65
64
|
// same rules as job.start apply, checked against the job's owner.
|
|
66
|
-
assertCallerAuthorized(tx, caller,
|
|
65
|
+
assertCallerAuthorized(tx, caller, taskId);
|
|
67
66
|
const next = requestDurableJobCancel(current, now);
|
|
68
67
|
if (next !== current)
|
|
69
68
|
tx.saveDurableJob(taskId, next);
|
|
@@ -75,11 +74,7 @@ export function createDurableJobControl(store) {
|
|
|
75
74
|
const current = tx.getDurableJob(taskId, jobId);
|
|
76
75
|
if (current === null)
|
|
77
76
|
return null;
|
|
78
|
-
|
|
79
|
-
// caller-key, active-Run, receipt, and Session checks, but explicitly
|
|
80
|
-
// require the Leader role; start/cancel's owner binding intentionally
|
|
81
|
-
// permits a Worker to operate its own Work Item.
|
|
82
|
-
assertCallerAuthorized(tx, caller, current.owner, taskId, { leaderOnly: true });
|
|
77
|
+
assertCallerAuthorized(tx, caller, taskId);
|
|
83
78
|
const next = acknowledgeUnknownDurableJob(current, now);
|
|
84
79
|
if (next !== current)
|
|
85
80
|
tx.saveDurableJob(taskId, next);
|
|
@@ -184,9 +179,9 @@ function validateStartParams(store, params) {
|
|
|
184
179
|
}
|
|
185
180
|
}
|
|
186
181
|
// rr8: Bind the declared owner to the caller's managed identity. A
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
assertCallerAuthorized(store, params.caller, params.
|
|
182
|
+
// Role is not an authorization boundary. Scope and exact managed Session
|
|
183
|
+
// identity are verified independently below.
|
|
184
|
+
assertCallerAuthorized(store, params.caller, params.taskId);
|
|
190
185
|
}
|
|
191
186
|
/**
|
|
192
187
|
* rr8/rr12: Bind the declared job owner to the caller's managed identity. The
|
|
@@ -194,49 +189,70 @@ function validateStartParams(store, params) {
|
|
|
194
189
|
* against durable Run/Session state — a self-reported role or scope is never
|
|
195
190
|
* authority on its own. The rules:
|
|
196
191
|
*
|
|
197
|
-
* - `user` (non-managed):
|
|
198
|
-
*
|
|
199
|
-
* Leader Run. A managed Session that sheds its identity and declares
|
|
200
|
-
* `scope: "user"` without that proof is rejected.
|
|
192
|
+
* - `user` (non-managed): rejected because it has no managed Session identity.
|
|
193
|
+
* - `global`: full Task authority after current Role Session verification.
|
|
201
194
|
* - `task` + mismatched taskId: rejected.
|
|
202
195
|
* - `task` + missing `runId`: rejected (a managed caller must bind to a Run).
|
|
203
196
|
* - `task` + Run not found / not active / `roleName !== role`: rejected — the
|
|
204
197
|
* claimed Role must be the real Role of an active Run.
|
|
205
|
-
* - `task
|
|
206
|
-
* -
|
|
207
|
-
* active-Run + in-flight-receipt + Session check as `job.acknowledge`, so a
|
|
208
|
-
* borrowed or stale Leader Run cannot authorize the request.
|
|
209
|
-
* - `task` + any other Role (Worker etc.): the owner must be `work-item` and
|
|
210
|
-
* the caller's Run must be for that same Work Item.
|
|
198
|
+
* - `task`: full authority inside the matching Task after active Run and
|
|
199
|
+
* per-Session caller-key verification; Role does not narrow it.
|
|
211
200
|
*/
|
|
212
201
|
/**
|
|
213
202
|
* rr13: `job.start`/`job.cancel` caller authorization at the Controller
|
|
214
203
|
* boundary. Every identity claim the Controller can verify from durable state
|
|
215
|
-
* (role
|
|
204
|
+
* (role and runId) is replayable by any client in the
|
|
216
205
|
* same Home that reads state.json. The channel itself is therefore not
|
|
217
206
|
* authenticated by those claims alone. A non-replayable per-Session caller key
|
|
218
207
|
* closes the gap:
|
|
219
208
|
*
|
|
220
209
|
* - `user` scope: **rejected outright** for start/cancel. A bare shell or a
|
|
221
|
-
* managed Session that sheds its identity has no
|
|
222
|
-
*
|
|
210
|
+
* managed Session that sheds its identity has no managed Session binding.
|
|
211
|
+
* - `global` scope: verified against the current global Role Session.
|
|
223
212
|
* - `task` scope: the caller must present `callerKey` — the
|
|
224
213
|
* `YUI_JOB_CALLER_KEY` injected at its native Session launch. The Controller
|
|
225
214
|
* hashes it (SHA-256) and compares against the durable `jobCallerKeyHashes`
|
|
226
215
|
* map for the caller's Role + Agent. No legacy fallback: an absent hash or a
|
|
227
216
|
* mismatched key is UNAUTHORIZED.
|
|
228
217
|
*
|
|
229
|
-
* The existing Run
|
|
230
|
-
*
|
|
231
|
-
* channel boundary before any durable-state lookup.
|
|
218
|
+
* The existing Run binding checks run after the key check, so a forged
|
|
219
|
+
* identity that also lacks the key is rejected at the channel boundary.
|
|
232
220
|
*/
|
|
233
|
-
function assertCallerAuthorized(store, caller,
|
|
221
|
+
function assertCallerAuthorized(store, caller, taskId) {
|
|
234
222
|
if (caller.scope === "user") {
|
|
235
223
|
// rr13: A user-scope caller has no per-Session channel binding. Every
|
|
236
|
-
// durable-state claim it could carry
|
|
224
|
+
// durable-state claim it could carry is replayable
|
|
237
225
|
// by any client in the same Home. Reject outright (fail-closed).
|
|
238
226
|
throw jobControlError("UNAUTHORIZED", "job.start/job.cancel requires a managed Session caller key; user scope is rejected.");
|
|
239
227
|
}
|
|
228
|
+
if (caller.scope === "global") {
|
|
229
|
+
const roleName = caller.role;
|
|
230
|
+
const agentId = caller.agentId;
|
|
231
|
+
const role = roleName === undefined ? null : store.getGlobalRole(roleName);
|
|
232
|
+
const binding = role === null || agentId === undefined
|
|
233
|
+
? undefined
|
|
234
|
+
: role.agentBindings[role.activeAgentId];
|
|
235
|
+
const sessions = roleName === undefined ? null : store.getGlobalRoleSessionSet(roleName);
|
|
236
|
+
const session = activeLiveRoleAgentSession(sessions);
|
|
237
|
+
if (role === null
|
|
238
|
+
|| binding === undefined
|
|
239
|
+
|| agentId === undefined
|
|
240
|
+
|| caller.adapterId === undefined
|
|
241
|
+
|| caller.launchId === undefined
|
|
242
|
+
|| sessions === null
|
|
243
|
+
|| sessions.activeAgentId !== role.activeAgentId
|
|
244
|
+
|| session === null
|
|
245
|
+
|| binding.agentId !== agentId
|
|
246
|
+
|| binding.adapterId !== caller.adapterId
|
|
247
|
+
|| session.agentId !== agentId
|
|
248
|
+
|| session.adapterId !== caller.adapterId
|
|
249
|
+
|| session.launchId !== caller.launchId
|
|
250
|
+
|| (caller.nativeSessionId !== undefined
|
|
251
|
+
&& session.nativeSessionId !== caller.nativeSessionId)) {
|
|
252
|
+
throw jobControlError("UNAUTHORIZED", "DurableJob control requires the current managed global Agent Session.");
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
240
256
|
if (caller.taskId !== taskId) {
|
|
241
257
|
throw jobControlError("UNAUTHORIZED", "A managed Task Session may not start or cancel Jobs for a different Task.");
|
|
242
258
|
}
|
|
@@ -262,29 +278,6 @@ function assertCallerAuthorized(store, caller, owner, taskId, options = {}) {
|
|
|
262
278
|
if (presentedHash !== expectedHash) {
|
|
263
279
|
throw jobControlError("UNAUTHORIZED", "The managed Session caller key does not match the durable hash.");
|
|
264
280
|
}
|
|
265
|
-
if (options.leaderOnly && caller.role !== "leader") {
|
|
266
|
-
throw jobControlError("UNAUTHORIZED", "job.acknowledge requires the current Task Leader Session.");
|
|
267
|
-
}
|
|
268
|
-
if (caller.role === "reviewer") {
|
|
269
|
-
throw jobControlError("UNAUTHORIZED", "A Reviewer may not start or cancel DurableJobs.");
|
|
270
|
-
}
|
|
271
|
-
if (caller.role === "leader") {
|
|
272
|
-
// rr12: Verify the caller is THE current in-flight Task Leader, not a
|
|
273
|
-
// borrowed or stale Leader Run. This mirrors job.acknowledge.
|
|
274
|
-
if (caller.receiptId === undefined) {
|
|
275
|
-
throw jobControlError("UNAUTHORIZED", "A Leader must carry the current in-flight Turn receipt.");
|
|
276
|
-
}
|
|
277
|
-
assertLeaderActionRun(store, taskId, {
|
|
278
|
-
runId: run.id,
|
|
279
|
-
receiptId: caller.receiptId
|
|
280
|
-
});
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
// Worker (or any non-leader, non-reviewer managed role): the owner must be
|
|
284
|
-
// the caller's own Work Item, verified through the caller's Run.
|
|
285
|
-
if (owner.kind !== "work-item" || run.workItemId !== owner.workItemId) {
|
|
286
|
-
throw jobControlError("UNAUTHORIZED", "A managed Worker may only start or cancel Jobs owned by its own Work Item.");
|
|
287
|
-
}
|
|
288
281
|
}
|
|
289
282
|
/**
|
|
290
283
|
* Resolve the managed workspace for the job's owner. Returns null if no
|
|
@@ -331,30 +324,6 @@ function readGitHead(path) {
|
|
|
331
324
|
function isTerminalWorkItemStatus(status) {
|
|
332
325
|
return status === "completed" || status === "failed" || status === "retired";
|
|
333
326
|
}
|
|
334
|
-
/**
|
|
335
|
-
* rr5/f5: Verify that the caller is the current in-flight Task Leader. The
|
|
336
|
-
* runId must identify the active Leader Run and the receiptId must match the
|
|
337
|
-
* Run's current in-flight Turn receipt. This binds the assertion to the
|
|
338
|
-
* Leader identity/session the Controller already tracks — a bare flag or a
|
|
339
|
-
* stale Run/receipt pair is rejected.
|
|
340
|
-
*/
|
|
341
|
-
function assertLeaderActionRun(store, taskId, assertion) {
|
|
342
|
-
const run = store.getActiveAgentRun(taskId, "leader");
|
|
343
|
-
if (run === null || run.status !== "active" || run.id !== assertion.runId) {
|
|
344
|
-
throw jobControlError("UNAUTHORIZED", "job.acknowledge requires the current active Task Leader Run.");
|
|
345
|
-
}
|
|
346
|
-
const expectedReceipt = formatAgentRunReceiptId(taskId, run.id);
|
|
347
|
-
if (assertion.receiptId !== expectedReceipt) {
|
|
348
|
-
throw jobControlError("UNAUTHORIZED", "job.acknowledge Leader receipt does not match the current Run.");
|
|
349
|
-
}
|
|
350
|
-
const sessions = store.getTaskRoleSessionSet(taskId, "leader");
|
|
351
|
-
if (sessions === null
|
|
352
|
-
|| sessions.inFlight === null
|
|
353
|
-
|| sessions.inFlight.runId !== run.id
|
|
354
|
-
|| sessions.inFlight.receiptId !== agentRunDeliveryReceiptId(run)) {
|
|
355
|
-
throw jobControlError("UNAUTHORIZED", "job.acknowledge Leader Run is not in flight.");
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
327
|
/**
|
|
359
328
|
* Strict `job.start` params parsing. Throws CoreApplicationError-shaped errors
|
|
360
329
|
* so the socket layer reports INVALID_PARAMS without leaking internals.
|
|
@@ -430,8 +399,7 @@ export function parseDurableJobCancelParams(value) {
|
|
|
430
399
|
}
|
|
431
400
|
/**
|
|
432
401
|
* rr26: `job.acknowledge` carries the same managed task caller as
|
|
433
|
-
* job.start/job.cancel.
|
|
434
|
-
* Session caller key is rejected by assertCallerAuthorized.
|
|
402
|
+
* job.start/job.cancel.
|
|
435
403
|
*/
|
|
436
404
|
export function parseDurableJobAcknowledgeParams(value) {
|
|
437
405
|
if (typeof value !== "object" || value === null || Array.isArray(value)
|
|
@@ -508,9 +476,8 @@ function parseSteps(value) {
|
|
|
508
476
|
* boundary (fail-closed). A non-managed caller must explicitly resolve to
|
|
509
477
|
* `{scope: "user"}`; the Controller never defaults a missing identity to
|
|
510
478
|
* user scope. A present caller must carry a valid `scope` and may carry
|
|
511
|
-
* optional
|
|
512
|
-
*
|
|
513
|
-
* `assertCallerAuthorized`; parsing only validates the JSON shape.
|
|
479
|
+
* optional managed Session identity fields. The identity is verified against
|
|
480
|
+
* durable state by `assertCallerAuthorized`; parsing only validates the JSON shape.
|
|
514
481
|
*/
|
|
515
482
|
function parseCaller(value) {
|
|
516
483
|
if (value === undefined) {
|
|
@@ -521,14 +488,15 @@ function parseCaller(value) {
|
|
|
521
488
|
}
|
|
522
489
|
const record = value;
|
|
523
490
|
const allowed = new Set([
|
|
524
|
-
"scope", "taskId", "role", "
|
|
491
|
+
"scope", "taskId", "role", "agentId", "adapterId", "launchId", "nativeSessionId",
|
|
492
|
+
"runId", "callerKey"
|
|
525
493
|
]);
|
|
526
494
|
for (const key of Object.keys(record)) {
|
|
527
495
|
if (!allowed.has(key)) {
|
|
528
496
|
throw jobControlError("INVALID_PARAMS", "job caller is invalid.");
|
|
529
497
|
}
|
|
530
498
|
}
|
|
531
|
-
if (record.scope !== "user" && record.scope !== "task") {
|
|
499
|
+
if (record.scope !== "user" && record.scope !== "global" && record.scope !== "task") {
|
|
532
500
|
throw jobControlError("INVALID_PARAMS", "job caller scope is invalid.");
|
|
533
501
|
}
|
|
534
502
|
const optionalId = (key) => {
|
|
@@ -539,9 +507,11 @@ function parseCaller(value) {
|
|
|
539
507
|
};
|
|
540
508
|
const taskId = optionalId("taskId");
|
|
541
509
|
const role = optionalId("role");
|
|
510
|
+
const agentId = optionalId("agentId");
|
|
511
|
+
const adapterId = optionalId("adapterId");
|
|
512
|
+
const launchId = optionalId("launchId");
|
|
513
|
+
const nativeSessionId = optionalId("nativeSessionId");
|
|
542
514
|
const runId = optionalId("runId");
|
|
543
|
-
const receiptId = optionalId("receiptId");
|
|
544
|
-
const leaderAssertion = parseLeaderAssertion(record.leaderAssertion);
|
|
545
515
|
const callerKey = record.callerKey === undefined
|
|
546
516
|
? undefined
|
|
547
517
|
: requiredId(record.callerKey, "job caller callerKey");
|
|
@@ -549,30 +519,14 @@ function parseCaller(value) {
|
|
|
549
519
|
scope: record.scope,
|
|
550
520
|
...(taskId === undefined ? {} : { taskId }),
|
|
551
521
|
...(role === undefined ? {} : { role }),
|
|
522
|
+
...(agentId === undefined ? {} : { agentId }),
|
|
523
|
+
...(adapterId === undefined ? {} : { adapterId }),
|
|
524
|
+
...(launchId === undefined ? {} : { launchId }),
|
|
525
|
+
...(nativeSessionId === undefined ? {} : { nativeSessionId }),
|
|
552
526
|
...(runId === undefined ? {} : { runId }),
|
|
553
|
-
...(receiptId === undefined ? {} : { receiptId }),
|
|
554
|
-
...(leaderAssertion === undefined ? {} : { leaderAssertion }),
|
|
555
527
|
...(callerKey === undefined ? {} : { callerKey })
|
|
556
528
|
};
|
|
557
529
|
}
|
|
558
|
-
/**
|
|
559
|
-
* rr12: Parse a `leaderAssertion` sub-object (`{runId, receiptId}`) carried by
|
|
560
|
-
* a user-scope caller. Returns undefined when absent; throws on a malformed
|
|
561
|
-
* shape.
|
|
562
|
-
*/
|
|
563
|
-
function parseLeaderAssertion(value) {
|
|
564
|
-
if (value === undefined)
|
|
565
|
-
return undefined;
|
|
566
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)
|
|
567
|
-
|| Object.keys(value).length !== 2) {
|
|
568
|
-
throw jobControlError("INVALID_PARAMS", "job caller leaderAssertion must have runId and receiptId.");
|
|
569
|
-
}
|
|
570
|
-
const record = value;
|
|
571
|
-
return {
|
|
572
|
-
runId: requiredId(record.runId, "job caller leaderAssertion runId"),
|
|
573
|
-
receiptId: requiredId(record.receiptId, "job caller leaderAssertion receiptId")
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
530
|
function parseStringMap(value, label) {
|
|
577
531
|
if (value === undefined)
|
|
578
532
|
return {};
|
|
@@ -16,7 +16,7 @@ import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenance
|
|
|
16
16
|
import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
|
|
17
17
|
import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
|
|
18
18
|
import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
19
|
-
import { recordIntegrationCheckJob,
|
|
19
|
+
import { recordIntegrationCheckJob, requireResolutionDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
|
|
20
20
|
import { createManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
21
21
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
22
22
|
import { readRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
@@ -228,7 +228,7 @@ export class GitIntegrationService {
|
|
|
228
228
|
}
|
|
229
229
|
catch (error) {
|
|
230
230
|
if (error instanceof RemoteBaselineConflictError) {
|
|
231
|
-
const pending =
|
|
231
|
+
const pending = requireResolutionDecision(current, {
|
|
232
232
|
affectedPaths: error.affectedPaths,
|
|
233
233
|
summary: error.message
|
|
234
234
|
}, this.now());
|
|
@@ -592,7 +592,7 @@ export class GitIntegrationService {
|
|
|
592
592
|
await git(["-C", candidatePath, "cherry-pick", "--skip"]);
|
|
593
593
|
continue;
|
|
594
594
|
}
|
|
595
|
-
const pending =
|
|
595
|
+
const pending = requireResolutionDecision(attempt, {
|
|
596
596
|
affectedPaths,
|
|
597
597
|
summary: `ChangeSet ${changeSetId} conflicts with ${attempt.targetRef}.`
|
|
598
598
|
}, this.now());
|
|
@@ -4,7 +4,7 @@ import { normalizeCheckResult } from "./checkResult.js";
|
|
|
4
4
|
export function createIntegrationAttempt(input, now) {
|
|
5
5
|
const timestamp = now.toISOString();
|
|
6
6
|
return validateIntegrationAttempt({
|
|
7
|
-
schemaVersion:
|
|
7
|
+
schemaVersion: 5,
|
|
8
8
|
id: input.id,
|
|
9
9
|
taskId: input.taskId,
|
|
10
10
|
projectId: input.projectId,
|
|
@@ -37,7 +37,7 @@ export function recordIntegrationCheckJob(attempt, jobId, now) {
|
|
|
37
37
|
updatedAt: now.toISOString()
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
-
export function
|
|
40
|
+
export function requireResolutionDecision(attempt, report, now) {
|
|
41
41
|
validateIntegrationAttempt(attempt);
|
|
42
42
|
const continuingAfterResolution = attempt.status === "blocked"
|
|
43
43
|
&& attempt.resolution?.action === "manual-resolution";
|
|
@@ -52,7 +52,7 @@ export function requireLeaderDecision(attempt, report, now) {
|
|
|
52
52
|
updatedAt: now.toISOString()
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
export function recordResolutionDecision(attempt, decision, now) {
|
|
55
|
+
export function recordResolutionDecision(attempt, decision, decidedBy, now) {
|
|
56
56
|
validateIntegrationAttempt(attempt);
|
|
57
57
|
if (attempt.status !== "blocked" || attempt.conflict === undefined) {
|
|
58
58
|
throw new Error("Integration has no pending semantic decision.");
|
|
@@ -67,13 +67,19 @@ export function recordResolutionDecision(attempt, decision, now) {
|
|
|
67
67
|
resolution: {
|
|
68
68
|
action: decision.action,
|
|
69
69
|
rationale: requireText(decision.rationale, "Resolution rationale"),
|
|
70
|
-
decidedBy:
|
|
70
|
+
decidedBy: requireTaskControlActor(decidedBy),
|
|
71
71
|
decidedAt: timestamp
|
|
72
72
|
},
|
|
73
73
|
updatedAt: timestamp,
|
|
74
74
|
...(decision.action === "reject" ? { endedAt: timestamp } : {})
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
+
function requireTaskControlActor(value) {
|
|
78
|
+
if (value !== "user" && value !== "operator" && value !== "leader") {
|
|
79
|
+
throw new Error(`Integration decision actor is invalid: ${String(value)}.`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
77
83
|
const TERMINAL_STATUSES = ["committed", "superseded", "failed"];
|
|
78
84
|
export function updateIntegrationAttempt(attempt, patch, now) {
|
|
79
85
|
validateIntegrationAttempt(attempt);
|
|
@@ -117,8 +123,8 @@ export function supersedeIntegration(attempt, reason, now) {
|
|
|
117
123
|
});
|
|
118
124
|
}
|
|
119
125
|
export function validateIntegrationAttempt(attempt) {
|
|
120
|
-
if (attempt.schemaVersion !==
|
|
121
|
-
throw new Error("IntegrationAttempt must use schemaVersion
|
|
126
|
+
if (attempt.schemaVersion !== 5) {
|
|
127
|
+
throw new Error("IntegrationAttempt must use schemaVersion 5.");
|
|
122
128
|
}
|
|
123
129
|
validateTaskRecordReference({
|
|
124
130
|
taskId: attempt.taskId,
|
|
@@ -162,9 +168,7 @@ export function validateIntegrationAttempt(attempt) {
|
|
|
162
168
|
throw new Error(`Resolution action is invalid: ${String(attempt.resolution.action)}.`);
|
|
163
169
|
}
|
|
164
170
|
requireText(attempt.resolution.rationale, "Resolution rationale");
|
|
165
|
-
|
|
166
|
-
throw new Error("Only the Leader may record a ResolutionDecision.");
|
|
167
|
-
}
|
|
171
|
+
requireTaskControlActor(attempt.resolution.decidedBy);
|
|
168
172
|
requireTimestamp(attempt.resolution.decidedAt, "Resolution decidedAt");
|
|
169
173
|
}
|
|
170
174
|
attempt.checks?.forEach(normalizeCheckResult);
|
|
@@ -701,11 +701,11 @@ export async function reconcileIntegrationQueueEntry(store, taskId, entryId, git
|
|
|
701
701
|
}
|
|
702
702
|
/**
|
|
703
703
|
* A conflicted entry may carry a blocked IntegrationAttempt waiting for a
|
|
704
|
-
*
|
|
704
|
+
* Task Agent decision. Requeue or supersede IS that decision — the blocked
|
|
705
705
|
* attempt is abandoned — so resolve it as rejected (terminal `failed`)
|
|
706
706
|
* rather than leaving it to block Task retirement.
|
|
707
707
|
*/
|
|
708
|
-
function resolveBlockedAttempt(store, taskId, entry, decision, now) {
|
|
708
|
+
function resolveBlockedAttempt(store, taskId, entry, decision, decidedBy, now) {
|
|
709
709
|
if (entry.integrationAttemptId === undefined)
|
|
710
710
|
return;
|
|
711
711
|
const attempt = store.getIntegrationAttempt(taskId, entry.integrationAttemptId);
|
|
@@ -714,7 +714,7 @@ function resolveBlockedAttempt(store, taskId, entry, decision, now) {
|
|
|
714
714
|
const rejected = recordResolutionDecision(attempt, {
|
|
715
715
|
action: "reject",
|
|
716
716
|
rationale: `Integration Attempt rejected by queue ${decision}.`
|
|
717
|
-
}, now());
|
|
717
|
+
}, decidedBy, now());
|
|
718
718
|
store.saveIntegrationAttempt(taskId, rejected);
|
|
719
719
|
}
|
|
720
720
|
/**
|
|
@@ -737,7 +737,7 @@ function assertRecoverableAttempt(store, taskId, entry, action) {
|
|
|
737
737
|
}
|
|
738
738
|
}
|
|
739
739
|
/** Retry a conflicted item (for example after a gate failure was fixed). */
|
|
740
|
-
export function requeueIntegrationQueueEntry(store, taskId, entryId, now = () => new Date()) {
|
|
740
|
+
export function requeueIntegrationQueueEntry(store, taskId, entryId, decidedBy, now = () => new Date()) {
|
|
741
741
|
// The committed-Attempt guard, the blocked-Attempt finalization, and the
|
|
742
742
|
// queue write must be atomic: a concurrent manual-resolution continue can
|
|
743
743
|
// commit the Attempt between the guard and the write, leaving the queue
|
|
@@ -749,20 +749,20 @@ export function requeueIntegrationQueueEntry(store, taskId, entryId, now = () =>
|
|
|
749
749
|
throw new Error(`Integration queue entry not found: ${taskId}/${entryId}.`);
|
|
750
750
|
}
|
|
751
751
|
assertRecoverableAttempt(tx, taskId, entry, "requeue");
|
|
752
|
-
resolveBlockedAttempt(tx, taskId, entry, "requeue", now);
|
|
752
|
+
resolveBlockedAttempt(tx, taskId, entry, "requeue", decidedBy, now);
|
|
753
753
|
const waiting = markIntegrationQueueRequeued(entry, now());
|
|
754
754
|
tx.saveIntegrationQueueEntry(taskId, waiting);
|
|
755
755
|
return waiting;
|
|
756
756
|
});
|
|
757
757
|
}
|
|
758
|
-
export function supersedeIntegrationQueueEntry(store, taskId, entryId, reason, now = () => new Date()) {
|
|
758
|
+
export function supersedeIntegrationQueueEntry(store, taskId, entryId, reason, decidedBy, now = () => new Date()) {
|
|
759
759
|
return store.transaction((tx) => {
|
|
760
760
|
const entry = tx.getIntegrationQueueEntry(taskId, entryId);
|
|
761
761
|
if (entry === null) {
|
|
762
762
|
throw new Error(`Integration queue entry not found: ${taskId}/${entryId}.`);
|
|
763
763
|
}
|
|
764
764
|
assertRecoverableAttempt(tx, taskId, entry, "supersede");
|
|
765
|
-
resolveBlockedAttempt(tx, taskId, entry, "supersede", now);
|
|
765
|
+
resolveBlockedAttempt(tx, taskId, entry, "supersede", decidedBy, now);
|
|
766
766
|
const superseded = markIntegrationQueueSuperseded(entry, reason, now());
|
|
767
767
|
tx.saveIntegrationQueueEntry(taskId, superseded);
|
|
768
768
|
return superseded;
|
|
@@ -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;
|
|
@@ -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())
|
|
@@ -67,13 +67,13 @@ export const CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
|
67
67
|
export const CURRENT_TASK_ROLE_SCHEMA_VERSION = 3;
|
|
68
68
|
export const CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION = 2;
|
|
69
69
|
export const CURRENT_WORK_ITEM_SCHEMA_VERSION = 12;
|
|
70
|
-
export const CURRENT_REVIEW_ROUND_SCHEMA_VERSION =
|
|
70
|
+
export const CURRENT_REVIEW_ROUND_SCHEMA_VERSION = 6;
|
|
71
71
|
export const CURRENT_CHANGE_SET_SCHEMA_VERSION = 3;
|
|
72
|
-
export const CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION =
|
|
72
|
+
export const CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION = 5;
|
|
73
73
|
export const CURRENT_MESSAGE_SCHEMA_VERSION = 3;
|
|
74
74
|
export const CURRENT_INPUT_REQUEST_SCHEMA_VERSION = 2;
|
|
75
75
|
export const CURRENT_DECISION_SCHEMA_VERSION = 1;
|
|
76
|
-
export const CURRENT_MILESTONE_SCHEMA_VERSION =
|
|
76
|
+
export const CURRENT_MILESTONE_SCHEMA_VERSION = 2;
|
|
77
77
|
export const CURRENT_EVENT_SCHEMA_VERSION = 2;
|
|
78
78
|
export const CURRENT_CAPABILITY_GRANT_SCHEMA_VERSION = 1;
|
|
79
79
|
export const CURRENT_RELEASE_WORKFLOW_SCHEMA_VERSION = 1;
|
|
@@ -2621,8 +2621,10 @@ function storedMilestone(value) {
|
|
|
2621
2621
|
validateTaskRecordReference({ taskId: milestone.taskId, localId: milestone.id }, "milestone");
|
|
2622
2622
|
requireNormalizedText(milestone.title, "Milestone title");
|
|
2623
2623
|
requireNormalizedText(milestone.summary, "Milestone summary");
|
|
2624
|
-
if (milestone.createdBy !== "
|
|
2625
|
-
|
|
2624
|
+
if (milestone.createdBy !== "user"
|
|
2625
|
+
&& milestone.createdBy !== "operator"
|
|
2626
|
+
&& milestone.createdBy !== "leader") {
|
|
2627
|
+
throw new StorageRecordError("Milestone createdBy is invalid.");
|
|
2626
2628
|
}
|
|
2627
2629
|
requireTimestamp(milestone.createdAt, "Milestone createdAt");
|
|
2628
2630
|
return milestone;
|
|
@@ -3950,9 +3952,8 @@ function validReviewRoundTransition(existing, candidate) {
|
|
|
3950
3952
|
|| !isDeepStrictEqual(existing.taskCandidate, candidate.taskCandidate)
|
|
3951
3953
|
|| !sameTaskFinalReviewContract(existing.taskFinalReviewContract, candidate.taskFinalReviewContract)
|
|
3952
3954
|
|| !compatibleExecutionGroups(existing.executionGroup, candidate.executionGroup)
|
|
3953
|
-
// Issue 06:
|
|
3954
|
-
//
|
|
3955
|
-
// the original policy/contract value to "leader".
|
|
3955
|
+
// Issue 06: an explicit Task-control retry resets a failed Task-final
|
|
3956
|
+
// Round to pending, so requestedBy may change from the original actor.
|
|
3956
3957
|
|| (existing.requestedBy !== candidate.requestedBy
|
|
3957
3958
|
&& !(existing.status === "failed"
|
|
3958
3959
|
&& candidate.status === "pending"
|