@zq-silk/yui 0.8.3 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +40 -22
- package/README.md +66 -19
- package/dist/cli/commandCatalog.js +43 -14
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli/updatePorts.js +6 -0
- package/dist/cli.js +252 -37
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/globalRoleCommands.js +8 -4
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +527 -147
- package/dist/commands/taskCompletionGate.js +36 -24
- package/dist/commands/taskContextCommand.js +11 -4
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +38 -3
- package/dist/commands/taskOverviewCommand.js +2 -1
- package/dist/commands/taskRoleRuntimeStatus.js +2 -1
- package/dist/context/runContextPack.js +9 -5
- package/dist/context/sessionBootstrapManifest.js +158 -11
- package/dist/context/wakeNotification.js +5 -3
- package/dist/controller/clientRuntime.js +15 -15
- package/dist/controller/controller.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +67 -7
- package/dist/controller/handoverCandidate.js +10 -3
- package/dist/controller/sessionNotify.js +4 -22
- package/dist/executor/agentAdapter.js +2 -2
- package/dist/executor/agentExecutor.js +29 -9
- package/dist/executor/fileRoleLaunchPlanner.js +37 -45
- package/dist/integration/gitIntegrationService.js +50 -2
- package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
- package/dist/observability/executionAudit.js +47 -1
- package/dist/observability/faultClassification.js +6 -4
- package/dist/observability/orchestrationMetrics.js +196 -0
- package/dist/operator/operatorSessionHistory.js +36 -0
- package/dist/release/releaseHandover.js +7 -5
- package/dist/release/runtimeRelease.js +15 -0
- package/dist/repository/taskWorkspaceCoordinator.js +13 -10
- package/dist/review/deltaRecheck.js +3 -2
- package/dist/review/reviewFindingLedger.js +5 -4
- package/dist/review/reviewOutcomeClassifier.js +263 -54
- package/dist/review/taskFinalReviewContractEvent.js +1 -0
- package/dist/review/taskFinalReviewContractRebind.js +367 -0
- package/dist/run/runIdentity.js +10 -70
- package/dist/runtime/agentHost.js +3 -4
- package/dist/runtime/codexAppServerRuntime.js +6 -0
- package/dist/runtime/exactControlPlane.js +47 -37
- package/dist/runtime/firstProgressStopLoss.js +54 -0
- package/dist/runtime/launchBroker.js +10 -2
- package/dist/runtime/runtimeDeadlines.js +14 -0
- package/dist/runtime/sessionTitle.js +24 -12
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +10 -3
- package/dist/scheduler/actionability.js +4 -2
- package/dist/scheduler/activeRoleRunDelivery.js +20 -18
- package/dist/scheduler/activeTaskProgress.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +33 -2
- package/dist/scheduler/taskExecutionProjection.js +13 -4
- package/dist/scheduler/wakeReason.js +1 -0
- package/dist/storage/sqliteStore.js +18 -3
- package/dist/storage/taskStore.js +14 -3
- package/dist/task/completionReadiness.js +48 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +153 -55
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/task/taskRecordRetirement.js +72 -0
- package/dist/web/webSnapshot.js +7 -1
- package/dist/workItem/workItem.js +6 -4
- package/i18n/README.zh-CN.md +48 -9
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +73 -31
- package/skills/yui-operator/SKILL.md +58 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
- package/skills/yui-runtime/SKILL.md +6 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isReviewFindingBlocking } from "../review/reviewFinding.js";
|
|
2
2
|
import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
|
|
3
|
+
import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
3
4
|
import { reviewFindingLedgerWriteFailedFromEvents } from "../review/reviewFindingLedger.js";
|
|
4
5
|
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
5
6
|
const ACTIVE_JOB_STATUSES = new Set([
|
|
@@ -16,6 +17,7 @@ const TERMINAL_REVIEW_STATUSES = new Set(["completed", "failed"]);
|
|
|
16
17
|
const TERMINAL_LANE_STATUSES = new Set(["completed", "failed", "yielded"]);
|
|
17
18
|
export function projectCompletionReadiness(facts, options = {}) {
|
|
18
19
|
const blockers = [];
|
|
20
|
+
const advisories = [];
|
|
19
21
|
const { task } = facts;
|
|
20
22
|
const findingsGate = options.findingsGate ?? true;
|
|
21
23
|
// A pending/running Task-final Review must be resumed or blocked first.
|
|
@@ -38,7 +40,11 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
38
40
|
// escalations remain audit evidence, but a later completed full Review (or
|
|
39
41
|
// accepted delta) supersedes them instead of blocking completion forever.
|
|
40
42
|
const latestCompletedTaskReview = facts.reviewRounds
|
|
41
|
-
.filter((round) => ((round.scope ?? "work-item") === "task" && round
|
|
43
|
+
.filter((round) => ((round.scope ?? "work-item") === "task" && isSemanticReviewRound(round, {
|
|
44
|
+
listAgentRuns: () => facts.agentRuns,
|
|
45
|
+
listReviewFindings: () => facts.reviewFindings,
|
|
46
|
+
listEvents: () => facts.events
|
|
47
|
+
})))
|
|
42
48
|
.slice()
|
|
43
49
|
.sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
44
50
|
|| left.id.localeCompare(right.id, undefined, { numeric: true })))
|
|
@@ -153,12 +159,15 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
153
159
|
fix: `wait for or recover the Provider turn on run ${continuation.runId}`
|
|
154
160
|
});
|
|
155
161
|
}
|
|
156
|
-
//
|
|
157
|
-
//
|
|
162
|
+
// Terminal child workspaces are cleanup advisories: Task completion is the
|
|
163
|
+
// semantic delivery boundary, while archive remains the fail-closed resource
|
|
164
|
+
// reclamation boundary. Missing/non-terminal ownership stays conservative.
|
|
158
165
|
for (const workspace of facts.managedWorkspaces) {
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
blockers.push(
|
|
166
|
+
const disposition = workspaceCompletionDisposition(facts, task.id, workspace);
|
|
167
|
+
if (disposition?.kind === "blocker")
|
|
168
|
+
blockers.push(disposition.value);
|
|
169
|
+
if (disposition?.kind === "advisory")
|
|
170
|
+
advisories.push(disposition.value);
|
|
162
171
|
}
|
|
163
172
|
// Active non-Leader Runs must finish. The Leader's own Run is terminalized
|
|
164
173
|
// by the completion transaction itself, so it is not a readiness blocker.
|
|
@@ -209,7 +218,21 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
209
218
|
return kindOrder;
|
|
210
219
|
return left.ref.id.localeCompare(right.ref.id, undefined, { numeric: true });
|
|
211
220
|
});
|
|
212
|
-
|
|
221
|
+
const sortedAdvisories = [...advisories].sort((left, right) => {
|
|
222
|
+
const codeOrder = left.code.localeCompare(right.code);
|
|
223
|
+
if (codeOrder !== 0)
|
|
224
|
+
return codeOrder;
|
|
225
|
+
const kindOrder = left.ref.kind.localeCompare(right.ref.kind);
|
|
226
|
+
if (kindOrder !== 0)
|
|
227
|
+
return kindOrder;
|
|
228
|
+
return left.ref.id.localeCompare(right.ref.id, undefined, { numeric: true });
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
taskId: task.id,
|
|
232
|
+
ready: sorted.length === 0,
|
|
233
|
+
blockers: sorted,
|
|
234
|
+
advisories: sortedAdvisories
|
|
235
|
+
};
|
|
213
236
|
}
|
|
214
237
|
/**
|
|
215
238
|
* A terminal Run releases only its completion blocker, never its immutable
|
|
@@ -252,61 +275,67 @@ function providerContinuationBlocksCompletion(continuation, facts) {
|
|
|
252
275
|
&& activation.status === "active"));
|
|
253
276
|
return conversationIsCurrent && activationIsLive;
|
|
254
277
|
}
|
|
255
|
-
function
|
|
278
|
+
function workspaceCompletionDisposition(facts, taskId, workspace) {
|
|
256
279
|
const owner = workspace.owner;
|
|
257
280
|
switch (owner.type) {
|
|
258
281
|
case "task":
|
|
259
282
|
// The Task main workspace is cleaned at archive, not completion.
|
|
260
283
|
return null;
|
|
261
284
|
case "work-item": {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// the incomplete-work-item blocker fires too, and once it is terminal
|
|
265
|
-
// the workspace is the exact cleanup target.
|
|
266
|
-
return {
|
|
285
|
+
const item = facts.workItems.find((entry) => entry.id === owner.workItemId);
|
|
286
|
+
const value = {
|
|
267
287
|
code: "work-item-workspace-undisposed",
|
|
268
288
|
ref: ref("work-item", owner.workItemId),
|
|
269
289
|
reason: `Work Item ${owner.workItemId} has an isolated workspace that is not disposed.`,
|
|
270
290
|
fix: `yui task work cleanup ${taskId}/${owner.workItemId} --integrated|--abandon`
|
|
271
291
|
};
|
|
292
|
+
return item !== undefined && (item.status === "completed" || item.status === "retired")
|
|
293
|
+
? { kind: "advisory", value }
|
|
294
|
+
: { kind: "blocker", value };
|
|
272
295
|
}
|
|
273
296
|
case "review-round": {
|
|
274
297
|
const round = facts.reviewRounds.find((entry) => entry.id === owner.reviewRoundId);
|
|
275
|
-
// A workspace for a still-active Review is covered by the
|
|
276
|
-
// active-task-review / wait-for-owned-execution path; only project the
|
|
277
|
-
// cleanup blocker once the Round is terminal.
|
|
278
298
|
if (round !== undefined && !TERMINAL_REVIEW_STATUSES.has(round.status))
|
|
279
299
|
return null;
|
|
280
|
-
|
|
300
|
+
const value = {
|
|
281
301
|
code: "review-workspace-undisposed",
|
|
282
302
|
ref: ref("review-round", owner.reviewRoundId),
|
|
283
303
|
reason: `ReviewRound ${owner.reviewRoundId} is terminal but its workspace is not cleaned up.`,
|
|
284
304
|
fix: `yui task work review cleanup ${taskId}/${owner.reviewRoundId}`
|
|
285
305
|
};
|
|
306
|
+
return round === undefined
|
|
307
|
+
? { kind: "blocker", value }
|
|
308
|
+
: { kind: "advisory", value };
|
|
286
309
|
}
|
|
287
310
|
case "integration-attempt": {
|
|
288
311
|
const integration = facts.integrations.find((entry) => entry.id === owner.integrationAttemptId);
|
|
289
312
|
if (integration !== undefined && UNRESOLVED_INTEGRATION_STATUSES.has(integration.status)) {
|
|
290
313
|
return null;
|
|
291
314
|
}
|
|
292
|
-
|
|
315
|
+
const value = {
|
|
293
316
|
code: "integration-workspace-undisposed",
|
|
294
317
|
ref: ref("integration-attempt", owner.integrationAttemptId),
|
|
295
318
|
reason: `Integration Attempt ${owner.integrationAttemptId} is terminal but its workspace is not cleaned up.`,
|
|
296
319
|
fix: `retry or continue Integration ${owner.integrationAttemptId} so its workspace is reclaimed`
|
|
297
320
|
};
|
|
321
|
+
return integration === undefined
|
|
322
|
+
? { kind: "blocker", value }
|
|
323
|
+
: { kind: "advisory", value };
|
|
298
324
|
}
|
|
299
325
|
case "execution-lane": {
|
|
300
326
|
const lane = findExecutionLane(facts, owner.executionGroupId, owner.executionLaneId);
|
|
301
327
|
if (lane !== undefined && !TERMINAL_LANE_STATUSES.has(lane.status))
|
|
302
328
|
return null;
|
|
303
|
-
|
|
329
|
+
const value = {
|
|
304
330
|
code: "execution-lane-workspace-undisposed",
|
|
305
331
|
ref: ref("execution-lane", `${owner.executionGroupId}/${owner.executionLaneId}`),
|
|
306
332
|
reason: `Execution Lane ${owner.executionGroupId}/${owner.executionLaneId} `
|
|
307
333
|
+ "is terminal but its workspace is not cleaned up.",
|
|
308
334
|
fix: `clean up Execution Lane ${owner.executionGroupId}/${owner.executionLaneId}`
|
|
309
335
|
};
|
|
336
|
+
return lane === undefined
|
|
337
|
+
? { kind: "blocker", value }
|
|
338
|
+
: { kind: "advisory", value };
|
|
310
339
|
}
|
|
311
340
|
}
|
|
312
341
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { nextActionReviewOutcomeEvidence } from "./nextAction.js";
|
|
2
|
+
import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
1
3
|
const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
|
|
2
4
|
export function detectDeliveryDuplicates(facts, intent) {
|
|
3
5
|
switch (intent.kind) {
|
|
@@ -127,7 +129,7 @@ function detectReviewDuplicates(facts, intent) {
|
|
|
127
129
|
if (!sameCandidate)
|
|
128
130
|
continue;
|
|
129
131
|
const ref = { kind: "review-round", id: round.id };
|
|
130
|
-
if (round
|
|
132
|
+
if (isSemanticReviewRound(round, nextActionReviewOutcomeEvidence(facts))) {
|
|
131
133
|
duplicates.push({
|
|
132
134
|
severity: "exact",
|
|
133
135
|
reason: `Task-final Review ${round.id} already attests this exact head`,
|
package/dist/task/nextAction.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
|
|
3
|
+
import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
4
|
+
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
|
|
5
|
+
import { taskDeliveryPath } from "./task.js";
|
|
3
6
|
import { currentWorkItemCandidate, governingWorkItemCandidate } from "../workItem/workItem.js";
|
|
4
7
|
const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
|
|
5
8
|
export function projectNextAction(facts) {
|
|
@@ -252,16 +255,41 @@ export function projectNextAction(facts) {
|
|
|
252
255
|
});
|
|
253
256
|
}
|
|
254
257
|
if (facts.workItems.length === 0) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
if (taskDeliveryPath(task) === "direct" && !taskFinalReviewRequired(facts)) {
|
|
259
|
+
return buildAction(facts, {
|
|
260
|
+
kind: "complete-task",
|
|
261
|
+
reason: `Task ${task.id} uses direct delivery; implement and verify the managed Task main, then complete without creating a WorkItem.`,
|
|
262
|
+
refs: [ref("task", task.id)],
|
|
263
|
+
preconditions: [
|
|
264
|
+
{ fact: "Task is active", satisfied: task.status === "active", ref: ref("task", task.id) },
|
|
265
|
+
{ fact: "Task main is clean, committed, and verified", satisfied: false }
|
|
266
|
+
],
|
|
267
|
+
recommendedCommand: `yui task complete ${task.id} --summary-file -`,
|
|
268
|
+
...(facts.reviewConfig === null
|
|
269
|
+
? {}
|
|
270
|
+
: {
|
|
271
|
+
alternatives: [{
|
|
272
|
+
kind: "promote-to-integrated-delivery",
|
|
273
|
+
reason: "Before Task main advances, promote to integrated delivery when risk warrants an independently managed ReviewRound.",
|
|
274
|
+
recommendedCommand: `yui task update ${task.id} --delivery integrated`,
|
|
275
|
+
refs: [ref("task", task.id)]
|
|
276
|
+
}],
|
|
277
|
+
judgmentRequired: "Leader must decide whether the work still fits direct delivery or should be promoted before completing it."
|
|
278
|
+
})
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (taskDeliveryPath(task) === "integrated") {
|
|
282
|
+
return buildAction(facts, {
|
|
283
|
+
kind: "implement-current-work-item",
|
|
284
|
+
reason: `Task ${task.id} has no Work Item; create the first unit of work.`,
|
|
285
|
+
refs: [],
|
|
286
|
+
preconditions: [
|
|
287
|
+
{ fact: "At least one Work Item exists", satisfied: false },
|
|
288
|
+
{ fact: "Task is active", satisfied: task.status === "active" }
|
|
289
|
+
],
|
|
290
|
+
recommendedCommand: `yui task work create ${task.id} \"<objective>\"`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
265
293
|
}
|
|
266
294
|
const uncaptured = facts.workItems.find((item) => needsChangeSetCapture(facts, item));
|
|
267
295
|
if (uncaptured !== undefined) {
|
|
@@ -291,10 +319,38 @@ export function projectNextAction(facts) {
|
|
|
291
319
|
});
|
|
292
320
|
}
|
|
293
321
|
const failedFinal = latestTaskFinalReview(facts.reviewRounds);
|
|
294
|
-
|
|
322
|
+
const failedFinalOutcome = failedFinal === undefined
|
|
323
|
+
? null
|
|
324
|
+
: classifyReviewRoundOutcome(failedFinal, nextActionReviewOutcomeEvidence(facts));
|
|
325
|
+
if (failedFinal !== undefined && failedFinalOutcome?.kind === "non-semantic") {
|
|
326
|
+
return buildAction(facts, {
|
|
327
|
+
kind: "resume-review",
|
|
328
|
+
reason: `Task-final Review ${failedFinal.id} ended before a semantic review was proven.`,
|
|
329
|
+
refs: [ref("review-round", failedFinal.id)],
|
|
330
|
+
preconditions: [
|
|
331
|
+
{ fact: "Task-final Review has semantic evidence", satisfied: false, ref: ref("review-round", failedFinal.id) }
|
|
332
|
+
],
|
|
333
|
+
recommendedCommand: `yui task review force-fresh ${task.id}/${failedFinal.id}`
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (failedFinal !== undefined && failedFinalOutcome?.kind === "ambiguous") {
|
|
337
|
+
return buildAction(facts, {
|
|
338
|
+
kind: "repair-protocol-inconsistency",
|
|
339
|
+
reason: `Task-final Review ${failedFinal.id} has ambiguous semantic and infrastructure evidence: ${failedFinalOutcome.reason}`,
|
|
340
|
+
refs: [ref("review-round", failedFinal.id)],
|
|
341
|
+
conflicts: [ref("review-round", failedFinal.id)],
|
|
342
|
+
preconditions: [
|
|
343
|
+
{ fact: "Review outcome is unambiguously semantic or non-semantic", satisfied: false, ref: ref("review-round", failedFinal.id) }
|
|
344
|
+
]
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (failedFinal !== undefined
|
|
348
|
+
&& failedFinalOutcome?.kind === "semantic"
|
|
349
|
+
&& ((failedFinal.checks ?? []).some(({ outcome }) => outcome === "failed")
|
|
350
|
+
|| deltaRecheckBlocksAcceptance(failedFinal))) {
|
|
295
351
|
return buildAction(facts, {
|
|
296
352
|
kind: "route-review-findings",
|
|
297
|
-
reason: `Task-final Review ${failedFinal.id}
|
|
353
|
+
reason: `Task-final Review ${failedFinal.id} delivered semantic negative evidence; route its open findings into a repair wave on one frozen head.`,
|
|
298
354
|
refs: [ref("review-round", failedFinal.id)],
|
|
299
355
|
preconditions: [
|
|
300
356
|
{ fact: "Task-final Review is failed", satisfied: true, ref: ref("review-round", failedFinal.id) }
|
|
@@ -372,19 +428,27 @@ export function projectNextAction(facts) {
|
|
|
372
428
|
&& finalReviewRequired
|
|
373
429
|
&& !hasValidFinalReview(facts)) {
|
|
374
430
|
const reviewerRole = taskFinalReviewRole(facts);
|
|
431
|
+
const directWithoutWorkItems = taskDeliveryPath(task) === "direct"
|
|
432
|
+
&& facts.workItems.length === 0;
|
|
375
433
|
return buildAction(facts, {
|
|
376
434
|
kind: "request-final-review",
|
|
377
|
-
reason:
|
|
435
|
+
reason: directWithoutWorkItems
|
|
436
|
+
? "This direct Task already owns a final-Review obligation; completion must prepare or resume a Review of its frozen Task head."
|
|
437
|
+
: "All Work Items are delivered but no valid Task-final Review attests the integrated head.",
|
|
378
438
|
refs: [ref("task", task.id)],
|
|
379
|
-
preconditions:
|
|
380
|
-
{ fact: "
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
439
|
+
preconditions: directWithoutWorkItems
|
|
440
|
+
? [{ fact: "Valid established Task-final Review at the direct head", satisfied: false }]
|
|
441
|
+
: [
|
|
442
|
+
{ fact: "All Work Items are terminal", satisfied: true },
|
|
443
|
+
{ fact: "Every ChangeSet is committed", satisfied: true },
|
|
444
|
+
{ fact: "Valid Task-final Review at the integrated head", satisfied: false }
|
|
445
|
+
],
|
|
446
|
+
recommendedCommand: directWithoutWorkItems
|
|
447
|
+
? `yui task complete ${task.id} --summary-file -`
|
|
448
|
+
: `yui task review request ${task.id} --role ${reviewerRole ?? "<reviewer-role>"}`
|
|
385
449
|
});
|
|
386
450
|
}
|
|
387
|
-
const finalReviewOptional = task
|
|
451
|
+
const finalReviewOptional = taskDeliveryPath(task) === "integrated"
|
|
388
452
|
&& !finalReviewRequired
|
|
389
453
|
&& !hasValidFinalReview(facts);
|
|
390
454
|
const finalReviewAlternative = finalReviewOptional && facts.reviewConfig !== null
|
|
@@ -397,19 +461,26 @@ export function projectNextAction(facts) {
|
|
|
397
461
|
: [];
|
|
398
462
|
return buildAction(facts, {
|
|
399
463
|
kind: "complete-task",
|
|
400
|
-
reason:
|
|
464
|
+
reason: taskDeliveryPath(task) === "direct"
|
|
465
|
+
? "The direct Task head and its established obligations are ready; converge the Task without creating successor work."
|
|
466
|
+
: "The delivery chain is complete; converge the Task instead of creating successor work.",
|
|
401
467
|
refs: [ref("task", task.id)],
|
|
402
468
|
preconditions: [
|
|
403
469
|
{ fact: "All Work Items are terminal", satisfied: true },
|
|
404
470
|
...(task.projectBindings.length === 0
|
|
405
471
|
? []
|
|
406
|
-
:
|
|
407
|
-
{
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
472
|
+
: taskDeliveryPath(task) === "direct"
|
|
473
|
+
? [{
|
|
474
|
+
fact: "Valid established Task-final Review at the direct head",
|
|
475
|
+
satisfied: hasValidFinalReview(facts)
|
|
476
|
+
}]
|
|
477
|
+
: [
|
|
478
|
+
{ fact: "Every ChangeSet is committed", satisfied: true },
|
|
479
|
+
{
|
|
480
|
+
fact: "Valid Task-final Review at the integrated head",
|
|
481
|
+
satisfied: hasValidFinalReview(facts)
|
|
482
|
+
}
|
|
483
|
+
])
|
|
413
484
|
],
|
|
414
485
|
...(finalReviewAlternative.length === 0 ? {} : { alternatives: finalReviewAlternative }),
|
|
415
486
|
...(!finalReviewOptional
|
|
@@ -431,7 +502,8 @@ export function durableStateFingerprint(facts) {
|
|
|
431
502
|
...facts.workItems.map((item) => `work:${item.id}:${item.status}:${item.revision}:${item.updatedAt}`),
|
|
432
503
|
...facts.changeSets.map((changeSet) => `change-set:${changeSet.id}:${changeSet.headCommit}`),
|
|
433
504
|
...facts.integrations.map((attempt) => `integration:${attempt.id}:${attempt.status}:${attempt.updatedAt}`),
|
|
434
|
-
...facts.reviewRounds.map((round) => `review:${round.id}:${round.status}:${round.endedAt ?? ""}`)
|
|
505
|
+
...facts.reviewRounds.map((round) => `review:${round.id}:${round.status}:${round.endedAt ?? ""}`),
|
|
506
|
+
...facts.taskFinalReviewContractEvents.map((event) => `task-final-review-event:${event.id}:${event.createdAt}`)
|
|
435
507
|
];
|
|
436
508
|
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
437
509
|
}
|
|
@@ -578,7 +650,10 @@ function selectOpenWorkItem(workItems) {
|
|
|
578
650
|
const openItems = workItems.filter((item) => OPEN_WORK_ITEM_STATUSES.has(item.status));
|
|
579
651
|
if (openItems.length === 0)
|
|
580
652
|
return { kind: "none" };
|
|
581
|
-
const eligible = openItems.find((item) => (item.dependsOn.every((dependencyId) =>
|
|
653
|
+
const eligible = openItems.find((item) => (item.dependsOn.every((dependencyId) => {
|
|
654
|
+
const status = byId.get(dependencyId)?.status;
|
|
655
|
+
return status === "completed" || status === "retired";
|
|
656
|
+
})));
|
|
582
657
|
if (eligible !== undefined)
|
|
583
658
|
return { kind: "ready", item: eligible };
|
|
584
659
|
let current = openItems[0];
|
|
@@ -588,8 +663,10 @@ function selectOpenWorkItem(workItems) {
|
|
|
588
663
|
return { kind: "blocked", itemId: current.id, blockedBy: current.id };
|
|
589
664
|
}
|
|
590
665
|
visited.add(current.id);
|
|
591
|
-
const blockedBy = current.dependsOn
|
|
592
|
-
|
|
666
|
+
const blockedBy = current.dependsOn.find((dependencyId) => {
|
|
667
|
+
const status = byId.get(dependencyId)?.status;
|
|
668
|
+
return status !== "completed" && status !== "retired";
|
|
669
|
+
});
|
|
593
670
|
if (blockedBy === undefined)
|
|
594
671
|
return { kind: "ready", item: current };
|
|
595
672
|
const dependency = byId.get(blockedBy);
|
|
@@ -601,15 +678,16 @@ function selectOpenWorkItem(workItems) {
|
|
|
601
678
|
return { kind: "none" };
|
|
602
679
|
}
|
|
603
680
|
function taskFinalReviewContract(facts) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
return
|
|
681
|
+
return taskFinalReviewContractResolution(facts)?.effective;
|
|
682
|
+
}
|
|
683
|
+
function taskFinalReviewContractResolution(facts) {
|
|
684
|
+
return resolveRecordedTaskFinalReviewContract(facts.task.id, facts.workItems, facts.reviewRounds, facts.taskFinalReviewContractEvents);
|
|
608
685
|
}
|
|
609
686
|
function taskFinalReviewRequired(facts) {
|
|
610
687
|
return taskFinalReviewContract(facts) !== undefined
|
|
611
688
|
|| latestTaskFinalReview(facts.reviewRounds) !== undefined
|
|
612
|
-
|| facts.
|
|
689
|
+
|| (taskDeliveryPath(facts.task) === "integrated"
|
|
690
|
+
&& facts.reviewConfig?.trigger === "final");
|
|
613
691
|
}
|
|
614
692
|
function taskFinalReviewRole(facts) {
|
|
615
693
|
return taskFinalReviewContract(facts)?.reviewerRoleName
|
|
@@ -657,7 +735,11 @@ function needsChangeSetCapture(facts, item) {
|
|
|
657
735
|
}
|
|
658
736
|
function hasValidFinalReview(facts) {
|
|
659
737
|
const final = latestTaskFinalReview(facts.reviewRounds);
|
|
660
|
-
if (final === undefined
|
|
738
|
+
if (final === undefined
|
|
739
|
+
|| !isSemanticReviewRound(final, nextActionReviewOutcomeEvidence(facts)))
|
|
740
|
+
return false;
|
|
741
|
+
if ((final.checks ?? []).some(({ outcome }) => outcome === "failed")
|
|
742
|
+
|| deltaRecheckBlocksAcceptance(final))
|
|
661
743
|
return false;
|
|
662
744
|
const reviewedCommits = new Set((final.taskCandidate?.projects ?? []).map((project) => project.commit));
|
|
663
745
|
if (reviewedCommits.size === 0)
|
|
@@ -675,25 +757,41 @@ function hasValidFinalReview(facts) {
|
|
|
675
757
|
}
|
|
676
758
|
return true;
|
|
677
759
|
}
|
|
760
|
+
/** Adapts the serializable next-action evidence bundle to the shared classifier. */
|
|
761
|
+
export function nextActionReviewOutcomeEvidence(facts) {
|
|
762
|
+
const evidence = facts.reviewOutcomeEvidence;
|
|
763
|
+
if (evidence === undefined)
|
|
764
|
+
return undefined;
|
|
765
|
+
return {
|
|
766
|
+
listAgentRuns: () => evidence.agentRuns,
|
|
767
|
+
listReviewFindings: () => evidence.reviewFindings,
|
|
768
|
+
listEvents: () => evidence.events
|
|
769
|
+
};
|
|
770
|
+
}
|
|
678
771
|
function detectProtocolInconsistency(facts) {
|
|
679
772
|
const changeSetIds = new Set(facts.changeSets.map((changeSet) => changeSet.id));
|
|
680
773
|
const workItemById = new Map(facts.workItems.map((item) => [item.id, item]));
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
774
|
+
try {
|
|
775
|
+
taskFinalReviewContractResolution(facts);
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
const candidateRefs = facts.workItems.flatMap((item) => {
|
|
779
|
+
const candidate = governingWorkItemCandidate(item);
|
|
780
|
+
return candidate?.taskFinalReviewContract === undefined
|
|
781
|
+
? []
|
|
782
|
+
: [ref("candidate", `${item.id}/${candidate.id}`)];
|
|
783
|
+
});
|
|
784
|
+
const reviewRefs = facts.reviewRounds
|
|
785
|
+
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
786
|
+
&& round.taskFinalReviewContract !== undefined))
|
|
787
|
+
.map((round) => ref("review-round", round.id));
|
|
788
|
+
const eventRefs = facts.taskFinalReviewContractEvents
|
|
789
|
+
.map((event) => ref("task-event", event.id));
|
|
790
|
+
return {
|
|
791
|
+
reason: "Task-final Review contract history is inconsistent: "
|
|
792
|
+
+ (error instanceof Error ? error.message : String(error)),
|
|
793
|
+
conflicts: [...candidateRefs, ...reviewRefs, ...eventRefs]
|
|
794
|
+
};
|
|
697
795
|
}
|
|
698
796
|
for (const round of facts.reviewRounds) {
|
|
699
797
|
const reviewConflict = reviewRoundConflict(round, facts.activeRuns);
|
package/dist/task/repairWave.js
CHANGED
|
@@ -27,10 +27,23 @@ export function extractReviewFindings(round) {
|
|
|
27
27
|
}
|
|
28
28
|
return parseReportFindings(round.report ?? "");
|
|
29
29
|
}
|
|
30
|
-
export function planRepairWave(reviewRoundId, findings) {
|
|
30
|
+
export function planRepairWave(reviewRoundId, findings, strategy = "consolidated") {
|
|
31
31
|
if (findings.length === 0) {
|
|
32
32
|
return { reviewRoundId, openFindingCount: 0, groups: [] };
|
|
33
33
|
}
|
|
34
|
+
if (strategy === "consolidated") {
|
|
35
|
+
const ordered = [...findings].sort((left, right) => (left.id.localeCompare(right.id, undefined, { numeric: true })));
|
|
36
|
+
return {
|
|
37
|
+
reviewRoundId,
|
|
38
|
+
openFindingCount: findings.length,
|
|
39
|
+
groups: [{
|
|
40
|
+
id: "repair-1",
|
|
41
|
+
findingIds: ordered.map(({ id }) => id),
|
|
42
|
+
paths: [...new Set(ordered.flatMap(({ paths }) => paths))].sort(),
|
|
43
|
+
reason: "Consolidated by default to minimize repair, integration, and review churn"
|
|
44
|
+
}]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
34
47
|
const parent = new Array(findings.length).fill(0).map((_, index) => index);
|
|
35
48
|
const find = (index) => {
|
|
36
49
|
let current = index;
|
package/dist/task/task.js
CHANGED
|
@@ -319,6 +319,16 @@ export function taskProjectBinding(task, projectId) {
|
|
|
319
319
|
export function taskHasProjects(task) {
|
|
320
320
|
return task.projectBindings.length > 0;
|
|
321
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Delivery is a projection of the existing Task contract, not a second source
|
|
324
|
+
* of truth. Keeping it derived preserves every stored Task schema while giving
|
|
325
|
+
* callers one product-level name for the three supported paths.
|
|
326
|
+
*/
|
|
327
|
+
export function taskDeliveryPath(task) {
|
|
328
|
+
if (task.projectBindings.length === 0)
|
|
329
|
+
return "no-project";
|
|
330
|
+
return task.requireIntegration === true ? "integrated" : "direct";
|
|
331
|
+
}
|
|
322
332
|
export function taskProjectIds(task) {
|
|
323
333
|
return task.projectBindings.map(({ projectId }) => projectId);
|
|
324
334
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
2
|
+
export const TASK_RECORD_RETIRED_EVENT = "task.record-retired";
|
|
3
|
+
export const RETIRABLE_TASK_RECORD_KINDS = [
|
|
4
|
+
"work-item",
|
|
5
|
+
"message",
|
|
6
|
+
"agent-run"
|
|
7
|
+
];
|
|
8
|
+
/**
|
|
9
|
+
* Appends a tombstone fact without rewriting the original record. Operational
|
|
10
|
+
* projections ignore the retired identity; audit and list surfaces can still
|
|
11
|
+
* render both the original bytes and this reasoned retirement event.
|
|
12
|
+
*/
|
|
13
|
+
export function createTaskRecordRetirement(input, now) {
|
|
14
|
+
return createTaskEvent(input.eventId, input.taskId, TASK_RECORD_RETIRED_EVENT, {
|
|
15
|
+
recordKind: requireRecordKind(input.recordKind),
|
|
16
|
+
recordId: requireText(input.recordId, "Task record id"),
|
|
17
|
+
reason: requireText(input.reason, "Task record retirement reason"),
|
|
18
|
+
retiredBy: requireRetiredBy(input.retiredBy)
|
|
19
|
+
}, now);
|
|
20
|
+
}
|
|
21
|
+
export function taskRecordRetirement(event) {
|
|
22
|
+
if (event.type !== TASK_RECORD_RETIRED_EVENT)
|
|
23
|
+
return null;
|
|
24
|
+
const recordKind = event.payload.recordKind;
|
|
25
|
+
const retiredBy = event.payload.retiredBy;
|
|
26
|
+
if (!RETIRABLE_TASK_RECORD_KINDS.includes(recordKind)
|
|
27
|
+
|| (retiredBy !== "user" && retiredBy !== "operator" && retiredBy !== "leader")
|
|
28
|
+
|| event.payload.recordId?.trim().length === 0
|
|
29
|
+
|| event.payload.reason?.trim().length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
recordKind: recordKind,
|
|
34
|
+
recordId: event.payload.recordId,
|
|
35
|
+
reason: event.payload.reason,
|
|
36
|
+
retiredBy
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function retiredTaskRecordIds(events, recordKind) {
|
|
40
|
+
const ids = new Set();
|
|
41
|
+
for (const event of events) {
|
|
42
|
+
const retirement = taskRecordRetirement(event);
|
|
43
|
+
if (retirement?.recordKind === recordKind)
|
|
44
|
+
ids.add(retirement.recordId);
|
|
45
|
+
}
|
|
46
|
+
return ids;
|
|
47
|
+
}
|
|
48
|
+
export function isTaskRecordRetired(events, recordKind, recordId) {
|
|
49
|
+
return retiredTaskRecordIds(events, recordKind).has(recordId);
|
|
50
|
+
}
|
|
51
|
+
export function operationalTaskRecords(records, events, recordKind) {
|
|
52
|
+
const retired = retiredTaskRecordIds(events, recordKind);
|
|
53
|
+
return retired.size === 0 ? [...records] : records.filter(({ id }) => !retired.has(id));
|
|
54
|
+
}
|
|
55
|
+
function requireRecordKind(value) {
|
|
56
|
+
if (!RETIRABLE_TASK_RECORD_KINDS.includes(value)) {
|
|
57
|
+
throw new Error(`Task record retirement kind is invalid: ${String(value)}.`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function requireRetiredBy(value) {
|
|
62
|
+
if (value !== "user" && value !== "operator" && value !== "leader") {
|
|
63
|
+
throw new Error(`Task record retirement actor is invalid: ${String(value)}.`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function requireText(value, label) {
|
|
68
|
+
if (typeof value !== "string" || value.includes("\0") || value.trim().length === 0) {
|
|
69
|
+
throw new Error(`${label} is required.`);
|
|
70
|
+
}
|
|
71
|
+
return value.trim();
|
|
72
|
+
}
|
package/dist/web/webSnapshot.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { taskDeliveryPath } from "../task/task.js";
|
|
1
2
|
import { isRoleRunStalled, latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
|
|
2
3
|
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
3
4
|
import { summarizeExecutionGroup } from "../execution/executionGroup.js";
|
|
@@ -40,6 +41,7 @@ export function buildWebDashboardSnapshot(store, now = new Date()) {
|
|
|
40
41
|
});
|
|
41
42
|
return {
|
|
42
43
|
...task,
|
|
44
|
+
deliveryPath: taskDeliveryPath(task),
|
|
43
45
|
...(names.length === 0 ? {} : { projectNames: names }),
|
|
44
46
|
workItems: countWorkItems(reader.listWorkItems(task.id)),
|
|
45
47
|
roleCount: reader.listRoles(task.id).length,
|
|
@@ -106,7 +108,11 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
|
|
|
106
108
|
};
|
|
107
109
|
});
|
|
108
110
|
return {
|
|
109
|
-
task:
|
|
111
|
+
task: {
|
|
112
|
+
...task,
|
|
113
|
+
deliveryPath: taskDeliveryPath(task),
|
|
114
|
+
...(projectNames.length === 0 ? {} : { projectNames })
|
|
115
|
+
},
|
|
110
116
|
execution: buildTaskExecutionProjection(reader, taskId),
|
|
111
117
|
brief: reader.getTaskBrief(taskId),
|
|
112
118
|
roles,
|
|
@@ -638,8 +638,8 @@ function normalizeProjectBaseRefs(baseRefs) {
|
|
|
638
638
|
});
|
|
639
639
|
}
|
|
640
640
|
function normalizeDisposition(input, now) {
|
|
641
|
-
if (input.by !== "leader") {
|
|
642
|
-
throw new Error("
|
|
641
|
+
if (input.by !== "leader" && input.by !== "operator" && input.by !== "user") {
|
|
642
|
+
throw new Error("Work Item retirement actor is invalid.");
|
|
643
643
|
}
|
|
644
644
|
const summary = requireText(input.summary, "Work item disposition summary");
|
|
645
645
|
const replacementWorkItemId = input.replacementWorkItemId;
|
|
@@ -648,7 +648,7 @@ function normalizeDisposition(input, now) {
|
|
|
648
648
|
}
|
|
649
649
|
const result = {
|
|
650
650
|
schemaVersion: 1,
|
|
651
|
-
by:
|
|
651
|
+
by: input.by,
|
|
652
652
|
summary,
|
|
653
653
|
retiredAt: now.toISOString(),
|
|
654
654
|
...(replacementWorkItemId === undefined ? {} : { replacementWorkItemId })
|
|
@@ -659,7 +659,9 @@ function validateDisposition(disposition) {
|
|
|
659
659
|
if (disposition.schemaVersion !== 1) {
|
|
660
660
|
throw new Error("Work Item disposition must use schemaVersion 1.");
|
|
661
661
|
}
|
|
662
|
-
if (disposition.by !== "leader"
|
|
662
|
+
if (disposition.by !== "leader"
|
|
663
|
+
&& disposition.by !== "operator"
|
|
664
|
+
&& disposition.by !== "user") {
|
|
663
665
|
throw new Error("Work Item disposition actor is invalid.");
|
|
664
666
|
}
|
|
665
667
|
requireText(disposition.summary, "Work item disposition summary");
|