@zq-silk/yui 0.6.13 → 0.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +34 -5
  2. package/dist/cli/commandCatalog.js +173 -57
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli.js +99 -11
  5. package/dist/commands/configCommands.js +521 -171
  6. package/dist/commands/deliveryGuardPreflight.js +2 -2
  7. package/dist/commands/executionAuditCommands.js +56 -3
  8. package/dist/commands/projectCommands.js +504 -2
  9. package/dist/commands/releaseCommands.js +0 -1
  10. package/dist/commands/taskActor.js +17 -0
  11. package/dist/commands/taskBaseCommands.js +29 -0
  12. package/dist/commands/taskCommands.js +577 -126
  13. package/dist/commands/taskContextCommand.js +36 -2
  14. package/dist/commands/taskNextActionCommand.js +48 -3
  15. package/dist/commands/taskPublicationCommands.js +319 -0
  16. package/dist/commands/taskRoleRuntimeStatus.js +83 -59
  17. package/dist/commands/telemetryCommands.js +14 -13
  18. package/dist/config/yuiConfig.js +161 -8
  19. package/dist/context/sessionContextBudget.js +68 -0
  20. package/dist/context/wakeNotification.js +65 -0
  21. package/dist/controller/clientRuntime.js +2 -1
  22. package/dist/controller/ephemeralResourceReaper.js +2 -1
  23. package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
  24. package/dist/controller/jobSupervisor.js +5 -4
  25. package/dist/controller/resourceCleanupLinux.js +6 -6
  26. package/dist/controller/resourceInventoryLinux.js +3 -3
  27. package/dist/controller/runtime.js +88 -10
  28. package/dist/controller/updateReconciliation.js +4 -3
  29. package/dist/doctor/doctor.js +26 -7
  30. package/dist/executor/agentConfigurationCatalog.js +18 -0
  31. package/dist/executor/fileRoleLaunchPlanner.js +3 -3
  32. package/dist/lifecycle/contextBudgetRollover.js +81 -0
  33. package/dist/lifecycle/exactRunTerminalization.js +11 -1
  34. package/dist/lifecycle/providerErrorClass.js +33 -12
  35. package/dist/observability/executionAudit.js +214 -6
  36. package/dist/output/table.js +18 -0
  37. package/dist/repository/gitWorkspace.js +92 -0
  38. package/dist/repository/project.js +218 -4
  39. package/dist/repository/taskBaseFreshness.js +318 -0
  40. package/dist/repository/taskWorkspacePreparer.js +16 -2
  41. package/dist/review/deltaRecheck.js +232 -0
  42. package/dist/review/reviewConfig.js +31 -0
  43. package/dist/review/reviewFindingLedger.js +5 -1
  44. package/dist/review/reviewRound.js +156 -1
  45. package/dist/run/providerRetry.js +21 -3
  46. package/dist/run/providerRetryConfig.js +13 -60
  47. package/dist/run/recoveryProjection.js +199 -0
  48. package/dist/runtime/builtinAgentDrivers.js +3 -0
  49. package/dist/runtime/builtinTranscriptUsage.js +76 -32
  50. package/dist/runtime/continuationManager.js +17 -0
  51. package/dist/runtime/index.js +2 -0
  52. package/dist/runtime/launchDiagnostics.js +154 -0
  53. package/dist/runtime/lifecycleReservation.js +13 -0
  54. package/dist/runtime/providerContinuation.js +38 -0
  55. package/dist/runtime/providerContinuationReconciliationService.js +1 -0
  56. package/dist/runtime/providerErrorCodes.js +278 -0
  57. package/dist/runtime/runtimeHealthPolicy.js +20 -0
  58. package/dist/runtime/runtimeObservation.js +1 -0
  59. package/dist/runtime/runtimeProjection.js +115 -23
  60. package/dist/runtime/tmuxAdapters.js +242 -48
  61. package/dist/scheduler/activeRoleRunDelivery.js +139 -3
  62. package/dist/scheduler/activeTaskProgress.js +4 -3
  63. package/dist/scheduler/leaderWakeupProcessor.js +105 -15
  64. package/dist/scheduler/roleRunLiveness.js +2 -1
  65. package/dist/scheduler/roleRunStall.js +3 -2
  66. package/dist/scheduler/taskWake.js +72 -0
  67. package/dist/scheduler/wakeReason.js +64 -0
  68. package/dist/scheduler/wakeupQueue.js +2 -1
  69. package/dist/setup/setupCommand.js +1 -1
  70. package/dist/storage/migration/productionRegistry.js +325 -1
  71. package/dist/storage/sqliteSchema.js +61 -2
  72. package/dist/storage/sqliteStore.js +129 -2
  73. package/dist/storage/storeRpc.js +1 -0
  74. package/dist/storage/taskStore.js +262 -5
  75. package/dist/storage/upgrade/recordVersions.js +6 -1
  76. package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
  77. package/dist/task/completionReadiness.js +282 -0
  78. package/dist/task/publicationReference.js +123 -0
  79. package/dist/task/taskRecordReference.js +3 -1
  80. package/dist/telemetry/telemetryConfig.js +23 -18
  81. package/dist/telemetry/telemetryWiring.js +8 -8
  82. package/dist/tmux/tmuxManager.js +50 -9
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/view.js +18 -0
  85. package/dist/web/webSnapshot.js +100 -10
  86. package/i18n/README.zh-CN.md +5 -5
  87. package/package.json +1 -1
  88. package/skills/yui-leader/SKILL.md +49 -10
  89. package/skills/yui-operator/SKILL.md +13 -3
  90. package/skills/yui-worker/SKILL.md +8 -0
@@ -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 `YUI_TELEMETRY_MODE` (default `legacy`). Only the three exact
24
- * values (case-insensitive) are accepted; anything else fails closed at
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(env = process.env) {
28
- const raw = env.YUI_TELEMETRY_MODE?.trim().toLowerCase();
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(`YUI_TELEMETRY_MODE must be one of ${TELEMETRY_MODES.join(", ")}; got ${JSON.stringify(raw)}.`);
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 (`YUI_TELEMETRY_TERMINAL_KEEP`,
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(env = process.env) {
41
- return resolvePositiveInteger(env.YUI_TELEMETRY_TERMINAL_KEEP, DEFAULT_TERMINAL_KEEP, "YUI_TELEMETRY_TERMINAL_KEEP");
42
+ export function resolveTerminalKeep(value) {
43
+ return resolvePositiveInteger(value, DEFAULT_TERMINAL_KEEP, "telemetryTerminalKeep");
42
44
  }
43
45
  /**
44
- * Resolve the active-Run hard cap (`YUI_TELEMETRY_RUN_CAP`, default 50,000).
45
- * Must be a positive integer not exceeding {@link MAX_RUN_CAP}; the 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(env = process.env) {
49
- const cap = resolvePositiveInteger(env.YUI_TELEMETRY_RUN_CAP, DEFAULT_RUN_CAP, "YUI_TELEMETRY_RUN_CAP");
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(`YUI_TELEMETRY_RUN_CAP must not exceed ${MAX_RUN_CAP.toLocaleString("en-US")} (retention cannot be disabled).`);
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.trim() === "")
58
+ if (raw === undefined || raw === null)
57
59
  return fallback;
58
- const value = Number(raw);
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 environment
8
- * (`YUI_TELEMETRY_MODE`, default `legacy`). Returns null in legacy mode so
9
- * callers keep the exact master behavior and never touch the database.
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, env = process.env) {
19
- const mode = resolveTelemetryMode(env);
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(`YUI_TELEMETRY_MODE=${mode} requires SQLite storage, but ${dbPath} does not exist. `
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(env),
30
- runCap: resolveRunCap(env)
29
+ terminalKeep: resolveTerminalKeep(config.telemetryTerminalKeep),
30
+ runCap: resolveRunCap(config.telemetryRunCap)
31
31
  });
32
32
  return { mode, sink: store, reader: store };
33
33
  }