@zq-silk/yui 0.6.12 → 0.6.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -5
- package/dist/cli/commandCatalog.js +173 -57
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli.js +99 -11
- package/dist/commands/configCommands.js +521 -171
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +56 -3
- package/dist/commands/projectCommands.js +504 -2
- package/dist/commands/releaseCommands.js +0 -1
- package/dist/commands/taskActor.js +17 -0
- package/dist/commands/taskBaseCommands.js +29 -0
- package/dist/commands/taskCommands.js +577 -126
- package/dist/commands/taskContextCommand.js +36 -2
- package/dist/commands/taskNextActionCommand.js +48 -3
- package/dist/commands/taskPublicationCommands.js +319 -0
- package/dist/commands/taskRoleRuntimeStatus.js +83 -59
- package/dist/commands/telemetryCommands.js +14 -13
- package/dist/config/yuiConfig.js +161 -8
- package/dist/context/sessionContextBudget.js +68 -0
- package/dist/context/wakeNotification.js +65 -0
- package/dist/controller/clientRuntime.js +2 -1
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
- package/dist/controller/jobSupervisor.js +5 -4
- package/dist/controller/resourceCleanupLinux.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +3 -3
- package/dist/controller/runtime.js +88 -10
- package/dist/controller/updateReconciliation.js +4 -3
- package/dist/doctor/doctor.js +26 -7
- package/dist/executor/agentConfigurationCatalog.js +18 -0
- package/dist/executor/fileRoleLaunchPlanner.js +3 -3
- package/dist/lifecycle/contextBudgetRollover.js +81 -0
- package/dist/lifecycle/exactRunTerminalization.js +11 -1
- package/dist/lifecycle/providerErrorClass.js +33 -12
- package/dist/observability/executionAudit.js +214 -6
- package/dist/output/table.js +18 -0
- package/dist/repository/gitWorkspace.js +92 -0
- package/dist/repository/project.js +218 -4
- package/dist/repository/taskBaseFreshness.js +318 -0
- package/dist/repository/taskWorkspacePreparer.js +16 -2
- package/dist/review/deltaRecheck.js +232 -0
- package/dist/review/reviewConfig.js +31 -0
- package/dist/review/reviewFindingLedger.js +5 -1
- package/dist/review/reviewRound.js +156 -1
- package/dist/run/providerRetry.js +21 -3
- package/dist/run/providerRetryConfig.js +13 -60
- package/dist/run/recoveryProjection.js +199 -0
- package/dist/runtime/builtinAgentDrivers.js +3 -0
- package/dist/runtime/builtinTranscriptUsage.js +76 -32
- package/dist/runtime/continuationManager.js +17 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/launchDiagnostics.js +154 -0
- package/dist/runtime/lifecycleReservation.js +13 -0
- package/dist/runtime/providerContinuation.js +38 -0
- package/dist/runtime/providerContinuationReconciliationService.js +1 -0
- package/dist/runtime/providerErrorCodes.js +278 -0
- package/dist/runtime/runtimeHealthPolicy.js +20 -0
- package/dist/runtime/runtimeObservation.js +1 -0
- package/dist/runtime/runtimeProjection.js +115 -23
- package/dist/runtime/tmuxAdapters.js +242 -48
- package/dist/scheduler/activeRoleRunDelivery.js +139 -3
- package/dist/scheduler/activeTaskProgress.js +4 -3
- package/dist/scheduler/leaderWakeupProcessor.js +105 -15
- package/dist/scheduler/roleRunLiveness.js +2 -1
- package/dist/scheduler/roleRunStall.js +3 -2
- package/dist/scheduler/taskWake.js +72 -0
- package/dist/scheduler/wakeReason.js +64 -0
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/migration/productionRegistry.js +325 -1
- package/dist/storage/sqliteSchema.js +61 -2
- package/dist/storage/sqliteStore.js +129 -2
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskStore.js +262 -5
- package/dist/storage/upgrade/recordVersions.js +6 -1
- package/dist/storage/upgrade/sqliteFileSet.js +50 -0
- package/dist/storage/upgrade/sqliteMigrationTarget.js +6 -4
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +6 -4
- package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
- package/dist/task/completionReadiness.js +282 -0
- package/dist/task/publicationReference.js +123 -0
- package/dist/task/taskRecordReference.js +3 -1
- package/dist/telemetry/telemetryConfig.js +23 -18
- package/dist/telemetry/telemetryWiring.js +8 -8
- package/dist/tmux/tmuxManager.js +50 -9
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/view.js +18 -0
- package/dist/web/webSnapshot.js +100 -10
- package/i18n/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +49 -10
- package/skills/yui-operator/SKILL.md +13 -3
- package/skills/yui-worker/SKILL.md +8 -0
|
@@ -189,11 +189,13 @@ function asStoredTask(value) {
|
|
|
189
189
|
decisions: asObjectMap(record.decisions),
|
|
190
190
|
milestones: asObjectMap(record.milestones),
|
|
191
191
|
events: asObjectMap(record.events),
|
|
192
|
+
wakes: asObjectMap(record.wakes),
|
|
192
193
|
leaderFailure: asNullableObject(record.leaderFailure),
|
|
193
194
|
operatorNotification: asNullableObject(record.operatorNotification),
|
|
194
195
|
idHighWaterMarks: asObject(record.idHighWaterMarks),
|
|
195
196
|
capabilityGrants: asObjectMap(record.capabilityGrants),
|
|
196
|
-
releaseWorkflows: asObjectMap(record.releaseWorkflows)
|
|
197
|
+
releaseWorkflows: asObjectMap(record.releaseWorkflows),
|
|
198
|
+
publicationReferences: asObjectMap(record.publicationReferences)
|
|
197
199
|
};
|
|
198
200
|
}
|
|
199
201
|
function tasksOf(state) {
|
|
@@ -335,6 +337,9 @@ export function populateSqliteFromState(home, state, databaseFilename) {
|
|
|
335
337
|
for (const event of Object.values(stored.events)) {
|
|
336
338
|
store.saveEvent(taskId, event);
|
|
337
339
|
}
|
|
340
|
+
for (const wake of Object.values(stored.wakes)) {
|
|
341
|
+
store.saveTaskWake(taskId, wake);
|
|
342
|
+
}
|
|
338
343
|
if (stored.leaderFailure !== null) {
|
|
339
344
|
store.saveLeaderFailure(stored.leaderFailure);
|
|
340
345
|
}
|
|
@@ -354,6 +359,9 @@ export function populateSqliteFromState(home, state, databaseFilename) {
|
|
|
354
359
|
for (const workflow of Object.values(stored.releaseWorkflows)) {
|
|
355
360
|
store.saveReleaseWorkflow(taskId, workflow);
|
|
356
361
|
}
|
|
362
|
+
for (const reference of Object.values(stored.publicationReferences)) {
|
|
363
|
+
store.savePublicationReference(taskId, reference);
|
|
364
|
+
}
|
|
357
365
|
}
|
|
358
366
|
// Work mailboxes.
|
|
359
367
|
for (const mailbox of Object.values(asObjectMap(state.mailboxes))) {
|
|
@@ -456,10 +464,12 @@ export function computeStateFamilyChecksums(state) {
|
|
|
456
464
|
const decisions = [];
|
|
457
465
|
const milestones = [];
|
|
458
466
|
const events = [];
|
|
467
|
+
const wakes = [];
|
|
459
468
|
const leaderFailures = [];
|
|
460
469
|
const operatorNotifications = [];
|
|
461
470
|
const capabilityGrants = [];
|
|
462
471
|
const releaseWorkflows = [];
|
|
472
|
+
const publicationReferences = [];
|
|
463
473
|
for (const stored of Object.values(tasks)) {
|
|
464
474
|
taskRecords.push(stored.task);
|
|
465
475
|
if (stored.brief !== null)
|
|
@@ -492,12 +502,14 @@ export function computeStateFamilyChecksums(state) {
|
|
|
492
502
|
decisions.push(...Object.values(stored.decisions));
|
|
493
503
|
milestones.push(...Object.values(stored.milestones));
|
|
494
504
|
events.push(...Object.values(stored.events));
|
|
505
|
+
wakes.push(...Object.values(stored.wakes));
|
|
495
506
|
if (stored.leaderFailure !== null)
|
|
496
507
|
leaderFailures.push(stored.leaderFailure);
|
|
497
508
|
if (stored.operatorNotification !== null)
|
|
498
509
|
operatorNotifications.push(stored.operatorNotification);
|
|
499
510
|
capabilityGrants.push(...Object.values(stored.capabilityGrants));
|
|
500
511
|
releaseWorkflows.push(...Object.values(stored.releaseWorkflows));
|
|
512
|
+
publicationReferences.push(...Object.values(stored.publicationReferences));
|
|
501
513
|
}
|
|
502
514
|
checksums.task = hashRecords(taskRecords);
|
|
503
515
|
checksums.taskBrief = hashRecords(briefs);
|
|
@@ -518,10 +530,12 @@ export function computeStateFamilyChecksums(state) {
|
|
|
518
530
|
checksums.decision = hashRecords(decisions);
|
|
519
531
|
checksums.milestone = hashRecords(milestones);
|
|
520
532
|
checksums.event = hashRecords(events);
|
|
533
|
+
checksums.taskWake = hashRecords(wakes);
|
|
521
534
|
checksums.leaderFailure = hashRecords(leaderFailures);
|
|
522
535
|
checksums.operatorNotification = hashRecords(operatorNotifications);
|
|
523
536
|
checksums.capabilityGrant = hashRecords(capabilityGrants);
|
|
524
537
|
checksums.releaseWorkflow = hashRecords(releaseWorkflows);
|
|
538
|
+
checksums.publicationReference = hashRecords(publicationReferences);
|
|
525
539
|
checksums.workMailbox = hashRecords(Object.values(asObjectMap(state.mailboxes)));
|
|
526
540
|
return checksums;
|
|
527
541
|
}
|
|
@@ -609,10 +623,12 @@ export function computeDbFamilyChecksums(home, databaseFilename) {
|
|
|
609
623
|
checksums.decision = hashPayloadTable(db, "SELECT payload FROM decisions");
|
|
610
624
|
checksums.milestone = hashPayloadTable(db, "SELECT payload FROM milestones");
|
|
611
625
|
checksums.event = hashPayloadTable(db, "SELECT payload FROM events");
|
|
626
|
+
checksums.taskWake = hashPayloadTable(db, "SELECT payload FROM task_wakes");
|
|
612
627
|
checksums.leaderFailure = hashPayloadTable(db, "SELECT payload FROM task_projections WHERE kind = 'leader-failure' AND payload IS NOT NULL");
|
|
613
628
|
checksums.operatorNotification = hashPayloadTable(db, "SELECT payload FROM task_projections WHERE kind = 'operator-notification' AND payload IS NOT NULL");
|
|
614
629
|
checksums.capabilityGrant = hashPayloadTable(db, "SELECT payload FROM capability_grants");
|
|
615
630
|
checksums.releaseWorkflow = hashPayloadTable(db, "SELECT payload FROM release_workflows");
|
|
631
|
+
checksums.publicationReference = hashPayloadTable(db, "SELECT payload FROM publication_references");
|
|
616
632
|
// Mailboxes are reconstructed from typed columns (no payload column).
|
|
617
633
|
const mailboxRows = readWorkMailboxRows(db);
|
|
618
634
|
checksums.workMailbox = hashRecords(mailboxRows.map(rowToMailbox));
|
|
@@ -730,11 +746,13 @@ export function readStateFromSqlite(home) {
|
|
|
730
746
|
decisions: {},
|
|
731
747
|
milestones: {},
|
|
732
748
|
events: {},
|
|
749
|
+
wakes: {},
|
|
733
750
|
leaderFailure: null,
|
|
734
751
|
operatorNotification: null,
|
|
735
752
|
idHighWaterMarks: {},
|
|
736
753
|
capabilityGrants: {},
|
|
737
|
-
releaseWorkflows: {}
|
|
754
|
+
releaseWorkflows: {},
|
|
755
|
+
publicationReferences: {}
|
|
738
756
|
};
|
|
739
757
|
}
|
|
740
758
|
// Per-task record families, keyed by their state.json map keys.
|
|
@@ -753,8 +771,10 @@ export function readStateFromSqlite(home) {
|
|
|
753
771
|
loadTaskPayloadMap(db, "decisions", tasks, "decisions", (record) => record.id);
|
|
754
772
|
loadTaskPayloadMap(db, "milestones", tasks, "milestones", (record) => record.id);
|
|
755
773
|
loadTaskPayloadMap(db, "events", tasks, "events", (record) => record.id);
|
|
774
|
+
loadTaskPayloadMap(db, "task_wakes", tasks, "wakes", (record) => record.id);
|
|
756
775
|
loadTaskPayloadMap(db, "capability_grants", tasks, "capabilityGrants", (record) => record.id);
|
|
757
776
|
loadTaskPayloadMap(db, "release_workflows", tasks, "releaseWorkflows", (record) => record.id);
|
|
777
|
+
loadTaskPayloadMap(db, "publication_references", tasks, "publicationReferences", (record) => record.id);
|
|
758
778
|
// Job caller key hashes: keyed by `${roleName}\0${agentId}`.
|
|
759
779
|
const callerHashRows = db.prepare("SELECT task_id, role_name, agent_id, hash FROM job_caller_key_hashes").all();
|
|
760
780
|
for (const row of callerHashRows) {
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { isReviewFindingBlocking } from "../review/reviewFinding.js";
|
|
2
|
+
import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
|
|
3
|
+
import { reviewFindingLedgerWriteFailedFromEvents } from "../review/reviewFindingLedger.js";
|
|
4
|
+
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
5
|
+
const ACTIVE_JOB_STATUSES = new Set([
|
|
6
|
+
"queued",
|
|
7
|
+
"running",
|
|
8
|
+
"unknown-needs-attention"
|
|
9
|
+
]);
|
|
10
|
+
const UNRESOLVED_INTEGRATION_STATUSES = new Set([
|
|
11
|
+
"running",
|
|
12
|
+
"blocked",
|
|
13
|
+
"validating"
|
|
14
|
+
]);
|
|
15
|
+
const TERMINAL_REVIEW_STATUSES = new Set(["completed", "failed"]);
|
|
16
|
+
const TERMINAL_LANE_STATUSES = new Set(["completed", "failed", "yielded"]);
|
|
17
|
+
export function projectCompletionReadiness(facts, options = {}) {
|
|
18
|
+
const blockers = [];
|
|
19
|
+
const { task } = facts;
|
|
20
|
+
const findingsGate = options.findingsGate ?? true;
|
|
21
|
+
// A pending/running Task-final Review must be resumed or blocked first.
|
|
22
|
+
for (const round of facts.reviewRounds) {
|
|
23
|
+
if ((round.scope ?? "work-item") !== "task")
|
|
24
|
+
continue;
|
|
25
|
+
if (round.status !== "pending" && round.status !== "running")
|
|
26
|
+
continue;
|
|
27
|
+
blockers.push({
|
|
28
|
+
code: "active-task-review",
|
|
29
|
+
ref: ref("review-round", round.id),
|
|
30
|
+
reason: `Task-final ReviewRound ${round.id} is ${round.status}.`,
|
|
31
|
+
fix: round.status === "pending"
|
|
32
|
+
? `yui task review retry ${task.id}/${round.id}`
|
|
33
|
+
: `wait for Reviewer Run on ${round.id} to finish`
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Issue 07: a completed delta-recheck that did not accept the head keeps the
|
|
37
|
+
// completion gate closed. `requires-full-review` escalates to a full Review;
|
|
38
|
+
// `finding` stays a blocker for the Leader to repair.
|
|
39
|
+
for (const round of facts.reviewRounds) {
|
|
40
|
+
if (!deltaRecheckBlocksAcceptance(round))
|
|
41
|
+
continue;
|
|
42
|
+
const disposition = round.deltaRecheck.disposition;
|
|
43
|
+
blockers.push({
|
|
44
|
+
code: "delta-recheck-not-accepted",
|
|
45
|
+
ref: ref("review-round", round.id),
|
|
46
|
+
reason: `Delta-recheck ${round.id} disposition is ${disposition}; the head is not accepted.`,
|
|
47
|
+
fix: disposition === "requires-full-review"
|
|
48
|
+
? `yui task review request ${task.id} --role <global-role>`
|
|
49
|
+
: `repair the finding and request a new Task-final Review`
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Open Input requests block protocol convergence.
|
|
53
|
+
for (const request of facts.openInputRequests) {
|
|
54
|
+
blockers.push({
|
|
55
|
+
code: "open-input-request",
|
|
56
|
+
ref: ref("input-request", request.id),
|
|
57
|
+
reason: `Input request ${request.id} is open.`,
|
|
58
|
+
fix: `yui task input answer ${task.id}/${request.id}`
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Every Work Item must be terminal (completed or retired).
|
|
62
|
+
for (const item of facts.workItems) {
|
|
63
|
+
if (item.status === "completed" || item.status === "retired")
|
|
64
|
+
continue;
|
|
65
|
+
blockers.push({
|
|
66
|
+
code: "incomplete-work-item",
|
|
67
|
+
ref: ref("work-item", item.id),
|
|
68
|
+
reason: `Work Item ${item.id} is ${item.status}.`,
|
|
69
|
+
fix: item.status === "failed"
|
|
70
|
+
? `yui task work update ${task.id}/${item.id} running`
|
|
71
|
+
: `complete or retire Work Item ${item.id}`
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Active DurableJobs (integration checks, etc.) must settle.
|
|
75
|
+
for (const job of facts.durableJobs) {
|
|
76
|
+
if (!ACTIVE_JOB_STATUSES.has(job.status))
|
|
77
|
+
continue;
|
|
78
|
+
if (job.status === "unknown-needs-attention" && job.acknowledgedAt !== undefined)
|
|
79
|
+
continue;
|
|
80
|
+
blockers.push({
|
|
81
|
+
code: "active-durable-job",
|
|
82
|
+
ref: ref("durable-job", job.id),
|
|
83
|
+
reason: `DurableJob ${job.id} is ${job.status}.`,
|
|
84
|
+
fix: `wait for DurableJob ${job.id} to finish, or cancel it`
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// requireIntegration Tasks need the full evidence chain.
|
|
88
|
+
if (task.requireIntegration === true) {
|
|
89
|
+
if (facts.workItems.length === 0) {
|
|
90
|
+
blockers.push({
|
|
91
|
+
code: "integration-evidence-missing",
|
|
92
|
+
ref: ref("task", task.id),
|
|
93
|
+
reason: `Task ${task.id} requires at least one WorkItem before completion.`,
|
|
94
|
+
fix: `yui task work create ${task.id} "<objective>"`
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (facts.changeSets.length === 0) {
|
|
98
|
+
blockers.push({
|
|
99
|
+
code: "integration-evidence-missing",
|
|
100
|
+
ref: ref("task", task.id),
|
|
101
|
+
reason: `Task ${task.id} requires at least one ChangeSet before completion.`,
|
|
102
|
+
fix: `yui task work capture ${task.id}/<work-item>`
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (!facts.integrations.some(({ status }) => status === "committed")) {
|
|
106
|
+
blockers.push({
|
|
107
|
+
code: "integration-evidence-missing",
|
|
108
|
+
ref: ref("task", task.id),
|
|
109
|
+
reason: `Task ${task.id} requires a committed Integration Attempt before completion.`,
|
|
110
|
+
fix: `yui task integration start ${task.id} --project <project> --change-set <change-set>`
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Unresolved Integration Attempts must settle.
|
|
115
|
+
for (const integration of facts.integrations) {
|
|
116
|
+
if (!UNRESOLVED_INTEGRATION_STATUSES.has(integration.status))
|
|
117
|
+
continue;
|
|
118
|
+
blockers.push({
|
|
119
|
+
code: "unresolved-integration",
|
|
120
|
+
ref: ref("integration-attempt", integration.id),
|
|
121
|
+
reason: `Integration Attempt ${integration.id} is ${integration.status}.`,
|
|
122
|
+
fix: `yui task integration continue ${task.id}/${integration.id}`
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Unsettled integration queue entries must commit or be superseded.
|
|
126
|
+
for (const entry of facts.integrationQueueEntries) {
|
|
127
|
+
if (entry.status === "committed" || entry.status === "superseded")
|
|
128
|
+
continue;
|
|
129
|
+
blockers.push({
|
|
130
|
+
code: "unsettled-integration-queue-entry",
|
|
131
|
+
ref: ref("integration-queue-entry", entry.id),
|
|
132
|
+
reason: `Integration queue entry ${entry.id} is ${entry.status}.`,
|
|
133
|
+
fix: `settle integration queue entry ${entry.id} (continue or supersede)`
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// Provider continuations that may still write the Workspace.
|
|
137
|
+
for (const continuation of blockingProviderContinuations(facts.events)) {
|
|
138
|
+
const identity = continuation.identity;
|
|
139
|
+
blockers.push({
|
|
140
|
+
code: "blocking-provider-continuation",
|
|
141
|
+
ref: ref("provider-continuation", identity.continuationId),
|
|
142
|
+
reason: `Provider continuation ${identity.continuationId} (run ${continuation.runId}) `
|
|
143
|
+
+ "may still write the Workspace or has an identity conflict.",
|
|
144
|
+
fix: `wait for or recover the Provider turn on run ${continuation.runId}`
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Undisposed managed workspaces. The Task-owned main workspace is excluded
|
|
148
|
+
// (it is cleaned at archive, not at completion).
|
|
149
|
+
for (const workspace of facts.managedWorkspaces) {
|
|
150
|
+
const blocker = workspaceBlocker(facts, task.id, workspace);
|
|
151
|
+
if (blocker !== null)
|
|
152
|
+
blockers.push(blocker);
|
|
153
|
+
}
|
|
154
|
+
// Active non-Leader Runs must finish. The Leader's own Run is terminalized
|
|
155
|
+
// by the completion transaction itself, so it is not a readiness blocker.
|
|
156
|
+
for (const run of facts.activeRuns) {
|
|
157
|
+
if (run.roleName === "leader")
|
|
158
|
+
continue;
|
|
159
|
+
blockers.push({
|
|
160
|
+
code: "active-run",
|
|
161
|
+
ref: ref("agent-run", run.id),
|
|
162
|
+
reason: `Role ${run.roleName} has an active Run ${run.id}.`,
|
|
163
|
+
fix: `wait for Run ${run.id} to yield or fail`
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Review finding ledger gate (enforce mode only).
|
|
167
|
+
// The transactional completion path runs this gate after final-review
|
|
168
|
+
// preparation: a pending/running Task-final Review is expected to resolve
|
|
169
|
+
// `fixed-pending-review` findings, so the gate only blocks when no Review
|
|
170
|
+
// is active. Mirror that timing here so the projection never blocks a
|
|
171
|
+
// completion attempt that is about to request the resolving Review.
|
|
172
|
+
const activeTaskReview = facts.reviewRounds.some((round) => ((round.scope ?? "work-item") === "task"
|
|
173
|
+
&& (round.status === "pending" || round.status === "running")));
|
|
174
|
+
if (findingsGate && facts.reviewFindingLedgerMode === "enforce" && !activeTaskReview) {
|
|
175
|
+
if (reviewFindingLedgerWriteFailedFromEvents(facts.events)) {
|
|
176
|
+
blockers.push({
|
|
177
|
+
code: "finding-ledger-unavailable",
|
|
178
|
+
ref: ref("task", task.id),
|
|
179
|
+
reason: "The Review finding ledger was unavailable while reconciling a semantic Review.",
|
|
180
|
+
fix: "recover the ledger and reconcile the Round before completing the Task"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
for (const finding of facts.reviewFindings) {
|
|
184
|
+
if (!isReviewFindingBlocking(finding))
|
|
185
|
+
continue;
|
|
186
|
+
blockers.push({
|
|
187
|
+
code: "open-review-finding",
|
|
188
|
+
ref: ref("review-finding", finding.id),
|
|
189
|
+
reason: `Open ${finding.severity.toUpperCase()} finding ${finding.id} is undispositioned.`,
|
|
190
|
+
fix: `yui task review finding dispose ${task.id}/${finding.id}`
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const sorted = [...blockers].sort((left, right) => {
|
|
195
|
+
const codeOrder = left.code.localeCompare(right.code);
|
|
196
|
+
if (codeOrder !== 0)
|
|
197
|
+
return codeOrder;
|
|
198
|
+
const kindOrder = left.ref.kind.localeCompare(right.ref.kind);
|
|
199
|
+
if (kindOrder !== 0)
|
|
200
|
+
return kindOrder;
|
|
201
|
+
return left.ref.id.localeCompare(right.ref.id, undefined, { numeric: true });
|
|
202
|
+
});
|
|
203
|
+
return { taskId: task.id, ready: sorted.length === 0, blockers: sorted };
|
|
204
|
+
}
|
|
205
|
+
function workspaceBlocker(facts, taskId, workspace) {
|
|
206
|
+
const owner = workspace.owner;
|
|
207
|
+
switch (owner.type) {
|
|
208
|
+
case "task":
|
|
209
|
+
// The Task main workspace is cleaned at archive, not completion.
|
|
210
|
+
return null;
|
|
211
|
+
case "work-item": {
|
|
212
|
+
// The preflight has always blocked on WorkItem workspaces. Project the
|
|
213
|
+
// blocker regardless of the Work Item status: if the item is still open
|
|
214
|
+
// the incomplete-work-item blocker fires too, and once it is terminal
|
|
215
|
+
// the workspace is the exact cleanup target.
|
|
216
|
+
return {
|
|
217
|
+
code: "work-item-workspace-undisposed",
|
|
218
|
+
ref: ref("work-item", owner.workItemId),
|
|
219
|
+
reason: `Work Item ${owner.workItemId} has an isolated workspace that is not disposed.`,
|
|
220
|
+
fix: `yui task work cleanup ${taskId}/${owner.workItemId} --integrated|--abandon`
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
case "review-round": {
|
|
224
|
+
const round = facts.reviewRounds.find((entry) => entry.id === owner.reviewRoundId);
|
|
225
|
+
// A workspace for a still-active Review is covered by the
|
|
226
|
+
// active-task-review / wait-for-owned-execution path; only project the
|
|
227
|
+
// cleanup blocker once the Round is terminal.
|
|
228
|
+
if (round !== undefined && !TERMINAL_REVIEW_STATUSES.has(round.status))
|
|
229
|
+
return null;
|
|
230
|
+
return {
|
|
231
|
+
code: "review-workspace-undisposed",
|
|
232
|
+
ref: ref("review-round", owner.reviewRoundId),
|
|
233
|
+
reason: `ReviewRound ${owner.reviewRoundId} is terminal but its workspace is not cleaned up.`,
|
|
234
|
+
fix: `yui task work review cleanup ${taskId}/${owner.reviewRoundId}`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
case "integration-attempt": {
|
|
238
|
+
const integration = facts.integrations.find((entry) => entry.id === owner.integrationAttemptId);
|
|
239
|
+
if (integration !== undefined && UNRESOLVED_INTEGRATION_STATUSES.has(integration.status)) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
code: "integration-workspace-undisposed",
|
|
244
|
+
ref: ref("integration-attempt", owner.integrationAttemptId),
|
|
245
|
+
reason: `Integration Attempt ${owner.integrationAttemptId} is terminal but its workspace is not cleaned up.`,
|
|
246
|
+
fix: `retry or continue Integration ${owner.integrationAttemptId} so its workspace is reclaimed`
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
case "execution-lane": {
|
|
250
|
+
const lane = findExecutionLane(facts, owner.executionGroupId, owner.executionLaneId);
|
|
251
|
+
if (lane !== undefined && !TERMINAL_LANE_STATUSES.has(lane.status))
|
|
252
|
+
return null;
|
|
253
|
+
return {
|
|
254
|
+
code: "execution-lane-workspace-undisposed",
|
|
255
|
+
ref: ref("execution-lane", `${owner.executionGroupId}/${owner.executionLaneId}`),
|
|
256
|
+
reason: `Execution Lane ${owner.executionGroupId}/${owner.executionLaneId} `
|
|
257
|
+
+ "is terminal but its workspace is not cleaned up.",
|
|
258
|
+
fix: `clean up Execution Lane ${owner.executionGroupId}/${owner.executionLaneId}`
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function findExecutionLane(facts, groupId, laneId) {
|
|
264
|
+
for (const item of facts.workItems) {
|
|
265
|
+
const group = item.executionGroups?.find((entry) => entry.id === groupId);
|
|
266
|
+
const lane = group?.lanes.find((entry) => entry.id === laneId);
|
|
267
|
+
if (lane !== undefined)
|
|
268
|
+
return lane;
|
|
269
|
+
}
|
|
270
|
+
for (const round of facts.reviewRounds) {
|
|
271
|
+
const group = round.executionGroup;
|
|
272
|
+
if (group?.id !== groupId)
|
|
273
|
+
continue;
|
|
274
|
+
const lane = group.lanes.find((entry) => entry.id === laneId);
|
|
275
|
+
if (lane !== undefined)
|
|
276
|
+
return lane;
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
function ref(kind, id) {
|
|
281
|
+
return { kind, id };
|
|
282
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
+
import { validateTaskRecordReference } from "./taskRecordReference.js";
|
|
3
|
+
export const PUBLICATION_REFERENCE_SCHEMA_VERSION = 1;
|
|
4
|
+
const PROVIDERS = new Set(["github", "gitlab"]);
|
|
5
|
+
const EXTERNAL_KINDS = new Set(["pull-request", "merge-request"]);
|
|
6
|
+
const STATES = new Set(["open", "merged", "closed"]);
|
|
7
|
+
const VERIFICATIONS = new Set(["reported", "verified"]);
|
|
8
|
+
const RECORDED_BY = new Set(["user", "operator", "leader"]);
|
|
9
|
+
const SOURCES = new Set(["manual", "release-workflow", "integration"]);
|
|
10
|
+
const GIT_SHA = /^[0-9a-f]{40}$/iu;
|
|
11
|
+
export function createPublicationReference(id, taskId, input, now) {
|
|
12
|
+
const reference = validateTaskRecordReference({ taskId, localId: id }, "publicationReference");
|
|
13
|
+
return validatePublicationReference({
|
|
14
|
+
schemaVersion: PUBLICATION_REFERENCE_SCHEMA_VERSION,
|
|
15
|
+
id: reference.localId,
|
|
16
|
+
taskId: reference.taskId,
|
|
17
|
+
projectId: requireIdentity(input.projectId, "Publication Project id"),
|
|
18
|
+
provider: requireEnum(input.provider, PROVIDERS, "Publication provider"),
|
|
19
|
+
repository: requireText(input.repository, "Publication repository"),
|
|
20
|
+
externalKind: requireEnum(input.externalKind, EXTERNAL_KINDS, "Publication external kind"),
|
|
21
|
+
externalId: requireText(input.externalId, "Publication external id"),
|
|
22
|
+
...(input.externalUrl === undefined ? {} : {
|
|
23
|
+
externalUrl: requireText(input.externalUrl, "Publication external URL")
|
|
24
|
+
}),
|
|
25
|
+
...(input.title === undefined ? {} : {
|
|
26
|
+
title: requireText(input.title, "Publication title")
|
|
27
|
+
}),
|
|
28
|
+
...(input.sourceBranch === undefined ? {} : {
|
|
29
|
+
sourceBranch: requireText(input.sourceBranch, "Publication source branch")
|
|
30
|
+
}),
|
|
31
|
+
...(input.targetBranch === undefined ? {} : {
|
|
32
|
+
targetBranch: requireText(input.targetBranch, "Publication target branch")
|
|
33
|
+
}),
|
|
34
|
+
...(input.localCommit === undefined ? {} : {
|
|
35
|
+
localCommit: requireCommit(input.localCommit, "Publication local commit")
|
|
36
|
+
}),
|
|
37
|
+
...(input.remoteCommit === undefined ? {} : {
|
|
38
|
+
remoteCommit: requireCommit(input.remoteCommit, "Publication remote commit")
|
|
39
|
+
}),
|
|
40
|
+
state: requireEnum(input.state ?? "open", STATES, "Publication state"),
|
|
41
|
+
verification: requireEnum(input.verification ?? "reported", VERIFICATIONS, "Publication verification"),
|
|
42
|
+
...(input.evidence === undefined ? {} : {
|
|
43
|
+
evidence: requireText(input.evidence, "Publication evidence")
|
|
44
|
+
}),
|
|
45
|
+
...(input.supersedes === undefined ? {} : {
|
|
46
|
+
supersedes: requireText(input.supersedes, "Publication supersedes id")
|
|
47
|
+
}),
|
|
48
|
+
recordedBy: requireEnum(input.recordedBy ?? "user", RECORDED_BY, "Publication recordedBy"),
|
|
49
|
+
source: requireEnum(input.source ?? "manual", SOURCES, "Publication source"),
|
|
50
|
+
...(input.mergedAt === undefined ? {} : {
|
|
51
|
+
mergedAt: requireTimestamp(input.mergedAt, "Publication mergedAt")
|
|
52
|
+
}),
|
|
53
|
+
createdAt: now.toISOString()
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function publicationExternalKey(reference) {
|
|
57
|
+
return `${reference.provider}/${reference.repository}/${reference.externalId}`;
|
|
58
|
+
}
|
|
59
|
+
export function validatePublicationReference(reference) {
|
|
60
|
+
if (reference.schemaVersion !== PUBLICATION_REFERENCE_SCHEMA_VERSION) {
|
|
61
|
+
throw new Error("Publication reference must use schemaVersion 1.");
|
|
62
|
+
}
|
|
63
|
+
validateTaskRecordReference({ taskId: reference.taskId, localId: reference.id }, "publicationReference");
|
|
64
|
+
requireIdentity(reference.projectId, "Publication Project id");
|
|
65
|
+
requireEnum(reference.provider, PROVIDERS, "Publication provider");
|
|
66
|
+
requireText(reference.repository, "Publication repository");
|
|
67
|
+
requireEnum(reference.externalKind, EXTERNAL_KINDS, "Publication external kind");
|
|
68
|
+
requireText(reference.externalId, "Publication external id");
|
|
69
|
+
if (reference.externalUrl !== undefined) {
|
|
70
|
+
requireText(reference.externalUrl, "Publication external URL");
|
|
71
|
+
}
|
|
72
|
+
if (reference.title !== undefined) {
|
|
73
|
+
requireText(reference.title, "Publication title");
|
|
74
|
+
}
|
|
75
|
+
if (reference.sourceBranch !== undefined) {
|
|
76
|
+
requireText(reference.sourceBranch, "Publication source branch");
|
|
77
|
+
}
|
|
78
|
+
if (reference.targetBranch !== undefined) {
|
|
79
|
+
requireText(reference.targetBranch, "Publication target branch");
|
|
80
|
+
}
|
|
81
|
+
if (reference.localCommit !== undefined) {
|
|
82
|
+
requireCommit(reference.localCommit, "Publication local commit");
|
|
83
|
+
}
|
|
84
|
+
if (reference.remoteCommit !== undefined) {
|
|
85
|
+
requireCommit(reference.remoteCommit, "Publication remote commit");
|
|
86
|
+
}
|
|
87
|
+
requireEnum(reference.state, STATES, "Publication state");
|
|
88
|
+
requireEnum(reference.verification, VERIFICATIONS, "Publication verification");
|
|
89
|
+
if (reference.verification === "verified"
|
|
90
|
+
&& (reference.state !== "merged" || reference.remoteCommit === undefined)) {
|
|
91
|
+
throw new Error("Verified publication requires state merged and a remote commit.");
|
|
92
|
+
}
|
|
93
|
+
if (reference.mergedAt !== undefined && reference.state !== "merged") {
|
|
94
|
+
throw new Error("Publication mergedAt requires state merged.");
|
|
95
|
+
}
|
|
96
|
+
if (reference.evidence !== undefined) {
|
|
97
|
+
requireText(reference.evidence, "Publication evidence");
|
|
98
|
+
}
|
|
99
|
+
if (reference.supersedes !== undefined) {
|
|
100
|
+
requireText(reference.supersedes, "Publication supersedes id");
|
|
101
|
+
if (reference.supersedes === reference.id) {
|
|
102
|
+
throw new Error("Publication reference cannot supersede itself.");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
requireEnum(reference.recordedBy, RECORDED_BY, "Publication recordedBy");
|
|
106
|
+
requireEnum(reference.source, SOURCES, "Publication source");
|
|
107
|
+
requireTimestamp(reference.createdAt, "Publication createdAt");
|
|
108
|
+
return reference;
|
|
109
|
+
}
|
|
110
|
+
function requireEnum(value, allowed, label) {
|
|
111
|
+
const normalized = requireText(value, label);
|
|
112
|
+
if (!allowed.has(normalized)) {
|
|
113
|
+
throw new Error(`${label} is invalid: ${normalized}.`);
|
|
114
|
+
}
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
function requireCommit(value, label) {
|
|
118
|
+
const commit = requireText(value, label);
|
|
119
|
+
if (!GIT_SHA.test(commit)) {
|
|
120
|
+
throw new Error(`${label} must be a full 40-character Git SHA.`);
|
|
121
|
+
}
|
|
122
|
+
return commit.toLowerCase();
|
|
123
|
+
}
|
|
@@ -14,7 +14,9 @@ export const TASK_RECORD_ID_PREFIXES = {
|
|
|
14
14
|
milestone: "milestone",
|
|
15
15
|
event: "event",
|
|
16
16
|
capabilityGrant: "capability-grant",
|
|
17
|
-
releaseWorkflow: "release-workflow"
|
|
17
|
+
releaseWorkflow: "release-workflow",
|
|
18
|
+
taskWake: "wake",
|
|
19
|
+
publicationReference: "publication"
|
|
18
20
|
};
|
|
19
21
|
export function formatTaskRecordReference(taskId, localId, kind) {
|
|
20
22
|
const reference = validateTaskRecordReference({ taskId, localId }, kind);
|
|
@@ -20,42 +20,47 @@ export const DEFAULT_RUN_CAP = TELEMETRY_RUN_CAP;
|
|
|
20
20
|
export const MAX_RUN_CAP = 10_000_000;
|
|
21
21
|
const TELEMETRY_MODES = ["legacy", "dual", "bounded"];
|
|
22
22
|
/**
|
|
23
|
-
* Resolve
|
|
24
|
-
* values (case-insensitive) are accepted; anything else
|
|
25
|
-
* startup instead of silently changing diagnostic retention.
|
|
23
|
+
* Resolve the telemetry mode from the durable config value (default `legacy`).
|
|
24
|
+
* Only the three exact values (case-insensitive) are accepted; anything else
|
|
25
|
+
* fails closed at startup instead of silently changing diagnostic retention.
|
|
26
26
|
*/
|
|
27
|
-
export function resolveTelemetryMode(
|
|
28
|
-
|
|
27
|
+
export function resolveTelemetryMode(value) {
|
|
28
|
+
if (typeof value !== "string")
|
|
29
|
+
return DEFAULT_TELEMETRY_MODE;
|
|
30
|
+
const raw = value.trim().toLowerCase();
|
|
29
31
|
if (raw === undefined || raw === "")
|
|
30
32
|
return DEFAULT_TELEMETRY_MODE;
|
|
31
33
|
if (!TELEMETRY_MODES.includes(raw)) {
|
|
32
|
-
throw new TypeError(`
|
|
34
|
+
throw new TypeError(`telemetryMode must be one of ${TELEMETRY_MODES.join(", ")}; got ${JSON.stringify(raw)}.`);
|
|
33
35
|
}
|
|
34
36
|
return raw;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
|
-
* Resolve the terminal-Run retention window
|
|
38
|
-
* default 200). Must be a positive integer.
|
|
39
|
+
* Resolve the terminal-Run retention window from the durable config value
|
|
40
|
+
* (default 200). Must be a positive integer.
|
|
39
41
|
*/
|
|
40
|
-
export function resolveTerminalKeep(
|
|
41
|
-
return resolvePositiveInteger(
|
|
42
|
+
export function resolveTerminalKeep(value) {
|
|
43
|
+
return resolvePositiveInteger(value, DEFAULT_TERMINAL_KEEP, "telemetryTerminalKeep");
|
|
42
44
|
}
|
|
43
45
|
/**
|
|
44
|
-
* Resolve the active-Run hard cap (
|
|
45
|
-
* Must be a positive integer not exceeding {@link MAX_RUN_CAP};
|
|
46
|
-
* cannot be disabled.
|
|
46
|
+
* Resolve the active-Run hard cap from the durable config value (default
|
|
47
|
+
* 50,000). Must be a positive integer not exceeding {@link MAX_RUN_CAP};
|
|
48
|
+
* the cap cannot be disabled.
|
|
47
49
|
*/
|
|
48
|
-
export function resolveRunCap(
|
|
49
|
-
const cap = resolvePositiveInteger(
|
|
50
|
+
export function resolveRunCap(value) {
|
|
51
|
+
const cap = resolvePositiveInteger(value, DEFAULT_RUN_CAP, "telemetryRunCap");
|
|
50
52
|
if (cap > MAX_RUN_CAP) {
|
|
51
|
-
throw new TypeError(`
|
|
53
|
+
throw new TypeError(`telemetryRunCap must not exceed ${MAX_RUN_CAP.toLocaleString("en-US")} (retention cannot be disabled).`);
|
|
52
54
|
}
|
|
53
55
|
return cap;
|
|
54
56
|
}
|
|
55
57
|
function resolvePositiveInteger(raw, fallback, label) {
|
|
56
|
-
if (raw === undefined || raw
|
|
58
|
+
if (raw === undefined || raw === null)
|
|
57
59
|
return fallback;
|
|
58
|
-
|
|
60
|
+
if (typeof raw !== "string" && typeof raw !== "number") {
|
|
61
|
+
throw new TypeError(`${label} must be a positive integer; got ${JSON.stringify(raw)}.`);
|
|
62
|
+
}
|
|
63
|
+
const value = typeof raw === "string" ? Number(raw) : raw;
|
|
59
64
|
if (!Number.isSafeInteger(value) || value < 1) {
|
|
60
65
|
throw new TypeError(`${label} must be a positive integer; got ${JSON.stringify(raw)}.`);
|
|
61
66
|
}
|
|
@@ -4,9 +4,9 @@ import { resolveRunCap, resolveTelemetryMode, resolveTerminalKeep } from "./tele
|
|
|
4
4
|
import { SqliteTelemetryStore } from "./sqliteTelemetryStore.js";
|
|
5
5
|
import { COMMITTED_DATABASE_FILENAME } from "../storage/upgrade/sqliteStateMigration.js";
|
|
6
6
|
/**
|
|
7
|
-
* Open the telemetry sidecar for a Home, resolved from the
|
|
8
|
-
* (
|
|
9
|
-
*
|
|
7
|
+
* Open the telemetry sidecar for a Home, resolved from the durable Yui
|
|
8
|
+
* config (default `legacy`). Returns null in legacy mode so callers keep the
|
|
9
|
+
* exact master behavior and never touch the database.
|
|
10
10
|
*
|
|
11
11
|
* Telemetry lives in the Home's authoritative `yui.db`. A Home without a
|
|
12
12
|
* database has not reached SQLite storage yet, so dual/bounded mode fails
|
|
@@ -15,19 +15,19 @@ import { COMMITTED_DATABASE_FILENAME } from "../storage/upgrade/sqliteStateMigra
|
|
|
15
15
|
* store fails isolated: a broken sidecar only increments its dropped counter
|
|
16
16
|
* and never blocks the semantic lane.
|
|
17
17
|
*/
|
|
18
|
-
export function openSchedulerTelemetry(home,
|
|
19
|
-
const mode = resolveTelemetryMode(
|
|
18
|
+
export function openSchedulerTelemetry(home, config) {
|
|
19
|
+
const mode = resolveTelemetryMode(config.telemetryMode);
|
|
20
20
|
if (mode === "legacy")
|
|
21
21
|
return null;
|
|
22
22
|
const dbPath = join(home, COMMITTED_DATABASE_FILENAME);
|
|
23
23
|
if (!existsSync(dbPath)) {
|
|
24
|
-
throw new Error(`
|
|
24
|
+
throw new Error(`telemetryMode=${mode} requires SQLite storage, but ${dbPath} does not exist. `
|
|
25
25
|
+ "Migrate this Home to the database backend first (yui upgrade).");
|
|
26
26
|
}
|
|
27
27
|
const store = new SqliteTelemetryStore(home, {
|
|
28
28
|
mode,
|
|
29
|
-
terminalKeep: resolveTerminalKeep(
|
|
30
|
-
runCap: resolveRunCap(
|
|
29
|
+
terminalKeep: resolveTerminalKeep(config.telemetryTerminalKeep),
|
|
30
|
+
runCap: resolveRunCap(config.telemetryRunCap)
|
|
31
31
|
});
|
|
32
32
|
return { mode, sink: store, reader: store };
|
|
33
33
|
}
|