@zq-silk/yui 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/agentConfigurationPicker.js +1 -1
  4. package/dist/cli/commandCatalog.js +251 -13
  5. package/dist/cli/updateOrchestrator.js +8 -0
  6. package/dist/cli/updatePorts.js +76 -22
  7. package/dist/cli.js +264 -20
  8. package/dist/commands/configCommands.js +83 -9
  9. package/dist/commands/controllerCommands.js +103 -0
  10. package/dist/commands/deliveryGuardPreflight.js +35 -0
  11. package/dist/commands/durableJobCommands.js +231 -0
  12. package/dist/commands/executionAuditCommands.js +193 -0
  13. package/dist/commands/grantCommands.js +374 -0
  14. package/dist/commands/projectCommands.js +119 -81
  15. package/dist/commands/releaseCommands.js +444 -0
  16. package/dist/commands/resourcesCommands.js +274 -0
  17. package/dist/commands/sessionCommands.js +104 -0
  18. package/dist/commands/taskActor.js +117 -0
  19. package/dist/commands/taskChangeSetCommands.js +60 -0
  20. package/dist/commands/taskCommands.js +610 -201
  21. package/dist/commands/taskCompletionGate.js +78 -1
  22. package/dist/commands/taskContextCommand.js +24 -6
  23. package/dist/commands/taskInputCommands.js +1 -1
  24. package/dist/commands/taskIntegrationCommands.js +136 -33
  25. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  26. package/dist/commands/taskNextActionCommand.js +85 -0
  27. package/dist/commands/taskOverlapCommands.js +120 -0
  28. package/dist/commands/taskOverviewCommand.js +36 -8
  29. package/dist/commands/telemetryCommands.js +330 -0
  30. package/dist/commands/workflowCommands.js +415 -0
  31. package/dist/config/yuiConfig.js +60 -0
  32. package/dist/controller/clientRuntime.js +42 -1
  33. package/dist/controller/controller.js +413 -61
  34. package/dist/controller/controllerMain.js +25 -2
  35. package/dist/controller/domainIdentity.js +16 -8
  36. package/dist/controller/ephemeralResourceReaper.js +2 -1
  37. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  38. package/dist/controller/handoverCandidate.js +168 -0
  39. package/dist/controller/jobClient.js +102 -0
  40. package/dist/controller/jobControl.js +613 -0
  41. package/dist/controller/jobSupervisor.js +498 -0
  42. package/dist/controller/providerHookRunFence.js +34 -5
  43. package/dist/controller/resourceCleanupLinux.js +18 -9
  44. package/dist/controller/resourceInventoryLinux.js +90 -39
  45. package/dist/controller/resourceInventoryRpc.js +85 -0
  46. package/dist/controller/resourceInventoryWorker.js +50 -0
  47. package/dist/controller/runtime.js +238 -22
  48. package/dist/controller/runtimeEventInbox.js +234 -57
  49. package/dist/controller/runtimeEventProcessor.js +549 -42
  50. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  51. package/dist/core/boundedRpc.js +475 -0
  52. package/dist/core/controllerServer.js +416 -27
  53. package/dist/core/controllerTelemetry.js +167 -0
  54. package/dist/doctor/doctor.js +113 -16
  55. package/dist/domain/validation.js +9 -0
  56. package/dist/execution/executionGroup.js +40 -3
  57. package/dist/executor/agentExecutor.js +6 -3
  58. package/dist/executor/effectiveLaunch.js +52 -0
  59. package/dist/executor/executorRegistry.js +50 -0
  60. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  61. package/dist/grant/capabilityGrant.js +282 -0
  62. package/dist/integration/changeSet.js +16 -3
  63. package/dist/integration/changeSetManifest.js +46 -0
  64. package/dist/integration/gitIntegrationService.js +528 -147
  65. package/dist/integration/integrationAttempt.js +54 -5
  66. package/dist/integration/integrationQueueEntry.js +221 -0
  67. package/dist/integration/integrationQueueService.js +955 -0
  68. package/dist/integration/manifestTags.js +99 -0
  69. package/dist/integration/overlapDiagnostics.js +211 -0
  70. package/dist/job/durableJob.js +449 -0
  71. package/dist/job/jobRunner.js +350 -0
  72. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  73. package/dist/lifecycle/providerErrorClass.js +126 -0
  74. package/dist/message/message.js +16 -3
  75. package/dist/observability/executionAudit.js +545 -0
  76. package/dist/observability/faultClassification.js +160 -0
  77. package/dist/observability/runtimeIdentity.js +367 -0
  78. package/dist/release/fakeReleasePorts.js +55 -0
  79. package/dist/release/releaseHandover.js +475 -0
  80. package/dist/release/releaseIdempotencyStore.js +165 -0
  81. package/dist/release/releaseWorkflow.js +459 -0
  82. package/dist/release/releaseWorkflowEngine.js +688 -0
  83. package/dist/release/releaseWorkflowPorts.js +1720 -0
  84. package/dist/release/runtimeRelease.js +495 -0
  85. package/dist/release/workflowFileLock.js +218 -0
  86. package/dist/repository/gitWorkspace.js +177 -1
  87. package/dist/repository/projectMaintenanceLock.js +315 -0
  88. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  89. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  90. package/dist/resources/autoResourceGc.js +116 -0
  91. package/dist/resources/liveReferences.js +574 -0
  92. package/dist/resources/resourceDiscovery.js +477 -0
  93. package/dist/resources/resourceGc.js +645 -0
  94. package/dist/resources/resourceRegistrar.js +256 -0
  95. package/dist/resources/resourceRegistry.js +150 -0
  96. package/dist/resources/resourceRegistryStore.js +41 -0
  97. package/dist/resources/resourceTypes.js +42 -0
  98. package/dist/resources/sqliteResourceRegistry.js +111 -0
  99. package/dist/review/reviewConfig.js +10 -0
  100. package/dist/review/reviewFinding.js +240 -0
  101. package/dist/review/reviewFindingLedger.js +545 -0
  102. package/dist/review/reviewOutcomeClassifier.js +61 -0
  103. package/dist/review/reviewRound.js +56 -4
  104. package/dist/run/agentRun.js +80 -4
  105. package/dist/run/providerRetry.js +84 -0
  106. package/dist/run/providerRetryConfig.js +63 -0
  107. package/dist/run/yieldReceipt.js +65 -0
  108. package/dist/runtime/exactControlPlane.js +79 -2
  109. package/dist/runtime/index.js +4 -0
  110. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  111. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  112. package/dist/runtime/sessionReconciliation.js +93 -0
  113. package/dist/runtime/sessionTerminationGuard.js +211 -0
  114. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  115. package/dist/runtime/tmuxAdapters.js +34 -1
  116. package/dist/scheduler/actionability.js +155 -0
  117. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  118. package/dist/scheduler/activeTaskProgress.js +60 -0
  119. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  120. package/dist/scheduler/roleRunStall.js +135 -29
  121. package/dist/scheduler/taskExecutionProjection.js +11 -0
  122. package/dist/setup/setupCommand.js +27 -4
  123. package/dist/storage/compatibleTaskStore.js +112 -5
  124. package/dist/storage/migration/productionRegistry.js +769 -1
  125. package/dist/storage/persistenceWorker.js +194 -0
  126. package/dist/storage/sqliteSchema.js +705 -0
  127. package/dist/storage/sqliteStore.js +1695 -0
  128. package/dist/storage/storageVersions.js +9 -2
  129. package/dist/storage/storeRpc.js +298 -0
  130. package/dist/storage/taskStore.js +982 -21
  131. package/dist/storage/upgrade/homeClassification.js +157 -12
  132. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  133. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  134. package/dist/storage/upgrade/recordVersions.js +10 -1
  135. package/dist/storage/upgrade/sqliteMigrationTarget.js +351 -0
  136. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  137. package/dist/storage/upgrade/sqliteStateMigration.js +713 -0
  138. package/dist/storage/upgrade/upgradeOrchestrator.js +510 -18
  139. package/dist/task/deliveryGuard.js +226 -0
  140. package/dist/task/nextAction.js +343 -0
  141. package/dist/task/repairWave.js +137 -0
  142. package/dist/task/taskRecordReference.js +6 -1
  143. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  144. package/dist/telemetry/telemetryCompaction.js +251 -0
  145. package/dist/telemetry/telemetryConfig.js +64 -0
  146. package/dist/telemetry/telemetryRouter.js +32 -0
  147. package/dist/telemetry/telemetryStore.js +19 -0
  148. package/dist/telemetry/telemetryWiring.js +33 -0
  149. package/dist/tmux/tmuxManager.js +20 -1
  150. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  151. package/dist/verification/gateArtifact.js +216 -0
  152. package/dist/verification/gateArtifactStore.js +87 -0
  153. package/dist/verification/verificationGateService.js +414 -0
  154. package/dist/verification/verificationPlan.js +308 -0
  155. package/dist/workspace/gitChangeSetCapture.js +12 -2
  156. package/dist/workspace/workItemChangeSetManager.js +60 -3
  157. package/package.json +2 -1
@@ -0,0 +1,226 @@
1
+ const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
2
+ export function detectDeliveryDuplicates(facts, intent) {
3
+ switch (intent.kind) {
4
+ case "create-work-item":
5
+ return detectWorkItemDuplicates(facts, intent.scope);
6
+ case "integration-start":
7
+ return detectIntegrationDuplicates(facts, intent);
8
+ case "review-request":
9
+ return detectReviewDuplicates(facts, intent);
10
+ case "complete-task":
11
+ return detectCompleteDuplicates(facts);
12
+ }
13
+ }
14
+ /**
15
+ * Apply the configured mode. `display` never interferes; `warn` reports every
16
+ * match as a warning; `enforce` hard-blocks on exact evidence and warns on
17
+ * suspected duplicates.
18
+ */
19
+ export function evaluateDeliveryGuard(duplicates, mode) {
20
+ if (mode === "display" || duplicates.length === 0) {
21
+ return { blocked: null, warnings: [] };
22
+ }
23
+ const exact = duplicates.filter((duplicate) => duplicate.severity === "exact");
24
+ const suspected = duplicates.filter((duplicate) => duplicate.severity === "suspected");
25
+ if (mode === "enforce" && exact.length > 0) {
26
+ return { blocked: exact[0], warnings: suspected };
27
+ }
28
+ return { blocked: null, warnings: [...exact, ...suspected] };
29
+ }
30
+ export function formatDeliveryDuplicate(duplicate) {
31
+ const refs = duplicate.refs.map((ref) => `${ref.kind} ${ref.id}`).join(", ");
32
+ const reuse = duplicate.reuseCommand === undefined
33
+ ? ""
34
+ : ` Existing proof: ${duplicate.reuseCommand}`;
35
+ return `${duplicate.severity === "exact" ? "Exact duplicate" : "Suspected duplicate"}: ${duplicate.reason} (${refs}).${reuse}`;
36
+ }
37
+ function detectWorkItemDuplicates(facts, scope) {
38
+ const wanted = normalizeScope(scope);
39
+ const duplicates = [];
40
+ for (const item of facts.workItems) {
41
+ const existing = normalizeScope({
42
+ title: item.title,
43
+ objective: item.objective,
44
+ acceptance: item.acceptance,
45
+ writeProjectIds: item.writeProjectIds
46
+ });
47
+ const sameScope = existing.title === wanted.title
48
+ && existing.objective === wanted.objective
49
+ && existing.acceptance === wanted.acceptance
50
+ && existing.writeProjectIds === wanted.writeProjectIds;
51
+ const ref = { kind: "work-item", id: item.id };
52
+ if (sameScope && OPEN_WORK_ITEM_STATUSES.has(item.status)) {
53
+ duplicates.push({
54
+ severity: "exact",
55
+ reason: `Work Item ${item.id} is already open with the identical scope`,
56
+ refs: [ref],
57
+ reuseCommand: `yui task work show ${facts.task.id}/${item.id}`
58
+ });
59
+ continue;
60
+ }
61
+ if (sameScope && item.status === "completed") {
62
+ duplicates.push({
63
+ severity: "suspected",
64
+ reason: `Work Item ${item.id} already delivered the identical scope; re-creating it may be a duplicate successor`,
65
+ refs: [ref],
66
+ reuseCommand: `yui task work show ${facts.task.id}/${item.id}`
67
+ });
68
+ continue;
69
+ }
70
+ if (!sameScope
71
+ && OPEN_WORK_ITEM_STATUSES.has(item.status)
72
+ && existing.writeProjectIds === wanted.writeProjectIds
73
+ && wanted.writeProjectIds.length > 0
74
+ && overlap(existing.acceptance, wanted.acceptance)) {
75
+ duplicates.push({
76
+ severity: "suspected",
77
+ reason: `Open Work Item ${item.id} shares the same Project scope and acceptance lines`,
78
+ refs: [ref]
79
+ });
80
+ }
81
+ }
82
+ return duplicates;
83
+ }
84
+ function detectIntegrationDuplicates(facts, intent) {
85
+ const wanted = new Set(intent.changeSetIds);
86
+ const duplicates = [];
87
+ for (const attempt of facts.integrations) {
88
+ if (attempt.projectId !== intent.projectId)
89
+ continue;
90
+ const existing = new Set(attempt.changeSetIds);
91
+ const sameSet = existing.size === wanted.size
92
+ && [...wanted].every((id) => existing.has(id));
93
+ const ref = { kind: "integration-attempt", id: attempt.id };
94
+ if (sameSet && attempt.status === "committed") {
95
+ duplicates.push({
96
+ severity: "exact",
97
+ reason: `Integration ${attempt.id} already committed this exact ChangeSet set`,
98
+ refs: [ref],
99
+ reuseCommand: `yui task integration show ${facts.task.id}/${attempt.id}`
100
+ });
101
+ continue;
102
+ }
103
+ if (sameSet
104
+ && (attempt.status === "running" || attempt.status === "validating")) {
105
+ duplicates.push({
106
+ severity: "suspected",
107
+ reason: `Integration ${attempt.id} is already ${attempt.status} for this exact ChangeSet set`,
108
+ refs: [ref],
109
+ reuseCommand: `yui task integration show ${facts.task.id}/${attempt.id}`
110
+ });
111
+ }
112
+ }
113
+ return duplicates;
114
+ }
115
+ function detectReviewDuplicates(facts, intent) {
116
+ const wanted = new Set(intent.taskCandidateCommits.map((commit) => commit.toLowerCase()));
117
+ const duplicates = [];
118
+ for (const round of facts.reviewRounds) {
119
+ if ((round.scope ?? "work-item") !== "task")
120
+ continue;
121
+ if (round.reviewerRoleName !== intent.reviewerRoleName)
122
+ continue;
123
+ const commits = new Set((round.taskCandidate?.projects ?? []).map((project) => project.commit.toLowerCase()));
124
+ const sameCandidate = commits.size === wanted.size
125
+ && commits.size > 0
126
+ && [...wanted].every((commit) => commits.has(commit));
127
+ if (!sameCandidate)
128
+ continue;
129
+ const ref = { kind: "review-round", id: round.id };
130
+ if (round.status === "completed") {
131
+ duplicates.push({
132
+ severity: "exact",
133
+ reason: `Task-final Review ${round.id} already attests this exact head`,
134
+ refs: [ref]
135
+ });
136
+ }
137
+ else if (round.status === "pending" || round.status === "running") {
138
+ duplicates.push({
139
+ severity: "suspected",
140
+ reason: `Task-final Review ${round.id} is already ${round.status} for this exact head`,
141
+ refs: [ref]
142
+ });
143
+ }
144
+ }
145
+ return duplicates;
146
+ }
147
+ function detectCompleteDuplicates(facts) {
148
+ if (facts.task.status === "completed" || facts.task.status === "archived") {
149
+ return [{
150
+ severity: "exact",
151
+ reason: `Task ${facts.task.id} is already ${facts.task.status}`,
152
+ refs: [{ kind: "task", id: facts.task.id }]
153
+ }];
154
+ }
155
+ return [];
156
+ }
157
+ function normalizeScope(scope) {
158
+ return {
159
+ title: scope.title.trim().toLowerCase(),
160
+ objective: scope.objective.trim().toLowerCase(),
161
+ acceptance: [...scope.acceptance]
162
+ .map((line) => line.trim().toLowerCase())
163
+ .filter((line) => line.length > 0)
164
+ .sort()
165
+ .join("\n"),
166
+ writeProjectIds: [...scope.writeProjectIds].map((id) => id.trim()).sort().join(",")
167
+ };
168
+ }
169
+ function overlap(left, right) {
170
+ const rightLines = new Set(right.split("\n"));
171
+ return left.split("\n").some((line) => line.length > 0 && rightLines.has(line));
172
+ }
173
+ /**
174
+ * Default number of consecutive yielded Leader turns that must produce no
175
+ * durable delivery change before the budget is exhausted.
176
+ */
177
+ export const DEFAULT_SEMANTIC_BUDGET_TURNS = 3;
178
+ /**
179
+ * Evaluate the semantic-progress budget from existing records only. The
180
+ * budget is exhausted when the last `turns` Leader Runs all yielded and no
181
+ * WorkItem/ChangeSet/Integration/Review record changed at or after the first
182
+ * of those Runs. Active execution (any Run) never exhausts the budget: a
183
+ * slow but progressing Worker or Leader is never interrupted.
184
+ */
185
+ export function evaluateSemanticBudget(facts, turns = DEFAULT_SEMANTIC_BUDGET_TURNS) {
186
+ if (facts.activeRuns.length > 0) {
187
+ return {
188
+ exhausted: false,
189
+ reason: "Active execution is in flight; the budget never interrupts a progressing Run.",
190
+ evidence: facts.activeRuns.map((run) => run.id)
191
+ };
192
+ }
193
+ const recentLeaderRuns = facts.leaderRuns
194
+ .filter((run) => run.status === "yielded")
195
+ .slice(-turns);
196
+ if (recentLeaderRuns.length < turns) {
197
+ return {
198
+ exhausted: false,
199
+ reason: `Fewer than ${turns} consecutive yielded Leader turns.`,
200
+ evidence: recentLeaderRuns.map((run) => run.id)
201
+ };
202
+ }
203
+ const firstStartedAt = Math.min(...recentLeaderRuns.map((run) => Date.parse(run.createdAt)));
204
+ const changed = latestDeliveryChangeAt(facts);
205
+ if (changed >= firstStartedAt) {
206
+ return {
207
+ exhausted: false,
208
+ reason: "A delivery record changed during the recent Leader turns.",
209
+ evidence: recentLeaderRuns.map((run) => run.id)
210
+ };
211
+ }
212
+ return {
213
+ exhausted: true,
214
+ reason: `${turns} consecutive Leader turns produced no durable delivery change; record a diagnosis/yield and wait for new facts instead of creating more records.`,
215
+ evidence: recentLeaderRuns.map((run) => run.id)
216
+ };
217
+ }
218
+ function latestDeliveryChangeAt(facts) {
219
+ const timestamps = [
220
+ ...facts.workItems.map((item) => Date.parse(item.updatedAt)),
221
+ ...facts.changeSets.map((changeSet) => Date.parse(changeSet.createdAt)),
222
+ ...facts.integrations.map((attempt) => Math.max(Date.parse(attempt.updatedAt), Date.parse(attempt.endedAt ?? attempt.updatedAt))),
223
+ ...facts.reviewRounds.map((round) => Math.max(Date.parse(round.createdAt), Date.parse(round.endedAt ?? round.createdAt)))
224
+ ];
225
+ return timestamps.length === 0 ? 0 : Math.max(...timestamps);
226
+ }
@@ -0,0 +1,343 @@
1
+ import { createHash } from "node:crypto";
2
+ import { currentWorkItemCandidate } from "../workItem/workItem.js";
3
+ const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
4
+ export function projectNextAction(facts) {
5
+ const { task } = facts;
6
+ if (task.status !== "active" && task.status !== "draft") {
7
+ return buildAction(facts, {
8
+ kind: "complete-task",
9
+ reason: `Task ${task.id} is ${task.status}; no further protocol action is available.`,
10
+ refs: [ref("task", task.id)],
11
+ preconditions: [
12
+ { fact: `Task status is ${task.status}`, satisfied: true, ref: ref("task", task.id) }
13
+ ]
14
+ });
15
+ }
16
+ const openInput = facts.openInputRequests[0];
17
+ if (openInput !== undefined) {
18
+ return buildAction(facts, {
19
+ kind: "resolve-input",
20
+ reason: `Input ${openInput.id} is open and blocks protocol convergence.`,
21
+ refs: [ref("input-request", openInput.id)],
22
+ preconditions: [
23
+ { fact: "Input request is open", satisfied: true, ref: ref("input-request", openInput.id) }
24
+ ],
25
+ recommendedCommand: `yui task input answer ${task.id}/${openInput.id}`
26
+ });
27
+ }
28
+ const inconsistency = detectProtocolInconsistency(facts);
29
+ if (inconsistency !== null) {
30
+ return buildAction(facts, {
31
+ kind: "repair-protocol-inconsistency",
32
+ reason: inconsistency.reason,
33
+ refs: inconsistency.conflicts,
34
+ conflicts: inconsistency.conflicts,
35
+ preconditions: inconsistency.conflicts.map((entry) => ({ fact: `Conflicting record ${entry.kind} ${entry.id}`, satisfied: false, ref: entry })),
36
+ recommendedCommand: inconsistency.recommendedCommand
37
+ });
38
+ }
39
+ const activeLeader = facts.activeRuns.find((run) => run.roleName === "leader");
40
+ if (activeLeader !== undefined) {
41
+ return buildAction(facts, {
42
+ kind: "wait-for-owned-execution",
43
+ reason: `Leader Run ${activeLeader.id} is active; the protocol position is being executed.`,
44
+ refs: [ref("agent-run", activeLeader.id)],
45
+ preconditions: [
46
+ { fact: "Leader Run is active", satisfied: true, ref: ref("agent-run", activeLeader.id) }
47
+ ]
48
+ });
49
+ }
50
+ const candidateReady = facts.workItems
51
+ .find((item) => item.status === "awaiting_acceptance");
52
+ if (candidateReady !== undefined) {
53
+ const candidate = currentWorkItemCandidate(candidateReady);
54
+ if (candidate !== undefined && isEmptyDirectCandidate(candidate)) {
55
+ const candidateRef = ref("candidate", `${candidateReady.id}/${candidate.id}`);
56
+ return buildAction(facts, {
57
+ kind: "repair-protocol-inconsistency",
58
+ reason: `Candidate ${candidate.id} is a direct Task-main delivery with base==head (no commits); reject it and re-dispatch real work.`,
59
+ refs: [ref("work-item", candidateReady.id), candidateRef],
60
+ conflicts: [candidateRef],
61
+ preconditions: [
62
+ { fact: "Direct Candidate contains at least one commit", satisfied: false, ref: candidateRef }
63
+ ],
64
+ recommendedCommand: `yui task work reject ${task.id}/${candidateReady.id} --summary \"empty base==head candidate\"`
65
+ });
66
+ }
67
+ const refs = [
68
+ ref("work-item", candidateReady.id),
69
+ ...(candidate === undefined ? [] : [ref("candidate", `${candidateReady.id}/${candidate.id}`)])
70
+ ];
71
+ return buildAction(facts, {
72
+ kind: "accept-or-reject-candidate",
73
+ reason: `Work Item ${candidateReady.id} has a Candidate awaiting Leader disposition.`,
74
+ refs,
75
+ preconditions: [
76
+ { fact: "Work Item is awaiting acceptance", satisfied: true, ref: refs[0] },
77
+ ...(candidate === undefined
78
+ ? [{ fact: "Candidate record exists", satisfied: false }]
79
+ : [{ fact: "Candidate record exists", satisfied: true, ref: refs[1] }])
80
+ ],
81
+ recommendedCommand: `yui task work accept ${task.id}/${candidateReady.id} --summary \"<decision>\"`
82
+ });
83
+ }
84
+ const activeWorkers = facts.activeRuns.filter((run) => run.roleName !== "leader");
85
+ if (activeWorkers.length > 0) {
86
+ return buildAction(facts, {
87
+ kind: "wait-for-owned-execution",
88
+ reason: `${activeWorkers.length} delegated Run(s) are active; wait for their delivery.`,
89
+ refs: activeWorkers.map((run) => ref("agent-run", run.id)),
90
+ preconditions: activeWorkers.map((run) => ({ fact: `Delegated Run ${run.id} is active`, satisfied: true, ref: ref("agent-run", run.id) }))
91
+ });
92
+ }
93
+ const failedWork = facts.workItems.find((item) => item.status === "failed");
94
+ if (failedWork !== undefined) {
95
+ const failedReview = latestFailedReviewFor(facts.reviewRounds, failedWork.id);
96
+ if (failedReview !== undefined) {
97
+ return buildAction(facts, {
98
+ kind: "route-review-findings",
99
+ reason: `Work Item ${failedWork.id} failed with Review ${failedReview.id}; route its open findings into a repair wave.`,
100
+ refs: [ref("work-item", failedWork.id), ref("review-round", failedReview.id)],
101
+ preconditions: [
102
+ { fact: "Work Item is failed", satisfied: true, ref: ref("work-item", failedWork.id) },
103
+ { fact: "Review Round is failed", satisfied: true, ref: ref("review-round", failedReview.id) }
104
+ ]
105
+ });
106
+ }
107
+ return buildAction(facts, {
108
+ kind: "implement-current-work-item",
109
+ reason: `Work Item ${failedWork.id} failed without a Review verdict; retry implementation.`,
110
+ refs: [ref("work-item", failedWork.id)],
111
+ preconditions: [
112
+ { fact: "Work Item is failed", satisfied: true, ref: ref("work-item", failedWork.id) }
113
+ ],
114
+ recommendedCommand: `yui task work update ${task.id}/${failedWork.id} running`
115
+ });
116
+ }
117
+ const openWork = facts.workItems
118
+ .find((item) => OPEN_WORK_ITEM_STATUSES.has(item.status));
119
+ if (openWork !== undefined) {
120
+ return buildAction(facts, {
121
+ kind: "implement-current-work-item",
122
+ reason: `Work Item ${openWork.id} is ${openWork.status}; dispatch or continue its implementation.`,
123
+ refs: [ref("work-item", openWork.id)],
124
+ preconditions: [
125
+ { fact: `Work Item is ${openWork.status}`, satisfied: true, ref: ref("work-item", openWork.id) }
126
+ ],
127
+ recommendedCommand: `yui task work dispatch ${task.id}/${openWork.id}`
128
+ });
129
+ }
130
+ if (facts.workItems.length === 0) {
131
+ return buildAction(facts, {
132
+ kind: "implement-current-work-item",
133
+ reason: `Task ${task.id} has no Work Item; create the first unit of work.`,
134
+ refs: [],
135
+ preconditions: [
136
+ { fact: "At least one Work Item exists", satisfied: false },
137
+ { fact: "Task is active", satisfied: task.status === "active" }
138
+ ],
139
+ recommendedCommand: task.status === "draft"
140
+ ? `yui task activate ${task.id}`
141
+ : `yui task work create ${task.id} \"<objective>\"`
142
+ });
143
+ }
144
+ const uncaptured = facts.workItems.find((item) => needsChangeSetCapture(facts, item));
145
+ if (uncaptured !== undefined) {
146
+ return buildAction(facts, {
147
+ kind: "capture-change-set",
148
+ reason: `Work Item ${uncaptured.id} is completed but has no ChangeSet; capture its delivery boundary.`,
149
+ refs: [ref("work-item", uncaptured.id)],
150
+ preconditions: [
151
+ { fact: "Work Item is completed", satisfied: true, ref: ref("work-item", uncaptured.id) },
152
+ { fact: "ChangeSet exists for the Work Item", satisfied: false }
153
+ ],
154
+ recommendedCommand: `yui task work capture ${task.id}/${uncaptured.id}`
155
+ });
156
+ }
157
+ const unintegrated = facts.changeSets
158
+ .find((changeSet) => !hasCommittedIntegration(facts.integrations, changeSet.id));
159
+ if (unintegrated !== undefined) {
160
+ return buildAction(facts, {
161
+ kind: "integrate-change-set",
162
+ reason: `ChangeSet ${unintegrated.id} has no committed Integration.`,
163
+ refs: [ref("change-set", unintegrated.id)],
164
+ preconditions: [
165
+ { fact: "ChangeSet exists", satisfied: true, ref: ref("change-set", unintegrated.id) },
166
+ { fact: "Committed Integration references the ChangeSet", satisfied: false }
167
+ ],
168
+ recommendedCommand: `yui task integration start ${task.id} --project ${unintegrated.projectId} --change-set ${unintegrated.id}`
169
+ });
170
+ }
171
+ const failedFinal = latestTaskFinalReview(facts.reviewRounds);
172
+ if (failedFinal !== undefined && failedFinal.status === "failed") {
173
+ return buildAction(facts, {
174
+ kind: "route-review-findings",
175
+ reason: `Task-final Review ${failedFinal.id} failed; route its open findings into a repair wave on one frozen head.`,
176
+ refs: [ref("review-round", failedFinal.id)],
177
+ preconditions: [
178
+ { fact: "Task-final Review is failed", satisfied: true, ref: ref("review-round", failedFinal.id) }
179
+ ]
180
+ });
181
+ }
182
+ if (task.projectBindings.length > 0 && !hasValidFinalReview(facts)) {
183
+ return buildAction(facts, {
184
+ kind: "request-final-review",
185
+ reason: "All Work Items are delivered but no valid Task-final Review attests the integrated head.",
186
+ refs: [ref("task", task.id)],
187
+ preconditions: [
188
+ { fact: "All Work Items are terminal", satisfied: true },
189
+ { fact: "Every ChangeSet is committed", satisfied: true },
190
+ { fact: "Valid Task-final Review at the integrated head", satisfied: false }
191
+ ],
192
+ recommendedCommand: `yui task review request ${task.id} --role <global-reviewer>`
193
+ });
194
+ }
195
+ return buildAction(facts, {
196
+ kind: "complete-task",
197
+ reason: "The delivery chain is complete; converge the Task instead of creating successor work.",
198
+ refs: [ref("task", task.id)],
199
+ preconditions: [
200
+ { fact: "All Work Items are terminal", satisfied: true },
201
+ ...(task.projectBindings.length === 0
202
+ ? []
203
+ : [
204
+ { fact: "Every ChangeSet is committed", satisfied: true },
205
+ { fact: "Valid Task-final Review at the integrated head", satisfied: true }
206
+ ])
207
+ ],
208
+ recommendedCommand: `yui task complete ${task.id} --summary-file -`
209
+ });
210
+ }
211
+ /**
212
+ * Stable fingerprint of the durable delivery position. It changes exactly
213
+ * when a delivery record changes, so the semantic-progress budget can compare
214
+ * positions across Leader turns without persisting anything new.
215
+ */
216
+ export function durableStateFingerprint(facts) {
217
+ const parts = [
218
+ `task:${facts.task.status}`,
219
+ ...facts.workItems.map((item) => `work:${item.id}:${item.status}:${item.revision}:${item.updatedAt}`),
220
+ ...facts.changeSets.map((changeSet) => `change-set:${changeSet.id}:${changeSet.headCommit}`),
221
+ ...facts.integrations.map((attempt) => `integration:${attempt.id}:${attempt.status}:${attempt.updatedAt}`),
222
+ ...facts.reviewRounds.map((round) => `review:${round.id}:${round.status}:${round.endedAt ?? ""}`)
223
+ ];
224
+ return createHash("sha256").update(parts.join("\n")).digest("hex");
225
+ }
226
+ function buildAction(facts, input) {
227
+ const fingerprintSource = [
228
+ input.kind,
229
+ ...input.refs.map((entry) => `${entry.kind}:${entry.id}`)
230
+ ].join("|");
231
+ return {
232
+ taskId: facts.task.id,
233
+ kind: input.kind,
234
+ reason: input.reason,
235
+ refs: input.refs,
236
+ preconditions: input.preconditions,
237
+ ...(input.recommendedCommand === undefined
238
+ ? {}
239
+ : { recommendedCommand: input.recommendedCommand }),
240
+ ...(input.conflicts === undefined ? {} : { conflicts: input.conflicts }),
241
+ fingerprint: createHash("sha256").update(fingerprintSource).digest("hex")
242
+ };
243
+ }
244
+ function ref(kind, id) {
245
+ return { kind, id };
246
+ }
247
+ function isEmptyDirectCandidate(candidate) {
248
+ const snapshot = candidate.taskMainSnapshot;
249
+ if (snapshot === undefined)
250
+ return false;
251
+ return snapshot.projects.every((project) => project.baseCommit === project.headCommit);
252
+ }
253
+ function latestFailedReviewFor(rounds, workItemId) {
254
+ return [...rounds]
255
+ .reverse()
256
+ .find((round) => round.workItemId === workItemId && round.status === "failed");
257
+ }
258
+ function latestTaskFinalReview(rounds) {
259
+ return [...rounds]
260
+ .reverse()
261
+ .find((round) => (round.scope ?? "work-item") === "task");
262
+ }
263
+ function hasCommittedIntegration(integrations, changeSetId) {
264
+ return integrations.some((attempt) => attempt.status === "committed" && attempt.changeSetIds.includes(changeSetId));
265
+ }
266
+ function needsChangeSetCapture(facts, item) {
267
+ if (item.status !== "completed")
268
+ return false;
269
+ if (facts.changeSets.some((changeSet) => changeSet.workItemId === item.id))
270
+ return false;
271
+ const candidate = item.candidates.at(-1);
272
+ if (candidate === undefined)
273
+ return false;
274
+ // A metadata-only direct Task-main Candidate has no WorkItem Develop
275
+ // workspace to capture; its boundary is the Task-main head itself.
276
+ if (candidate.workspace === undefined
277
+ && candidate.gitSnapshot === undefined
278
+ && candidate.taskMainSnapshot !== undefined) {
279
+ return false;
280
+ }
281
+ return candidate.workspace !== undefined || candidate.gitSnapshot !== undefined;
282
+ }
283
+ function hasValidFinalReview(facts) {
284
+ const final = latestTaskFinalReview(facts.reviewRounds);
285
+ if (final === undefined || final.status !== "completed")
286
+ return false;
287
+ const reviewedCommits = new Set((final.taskCandidate?.projects ?? []).map((project) => project.commit));
288
+ if (reviewedCommits.size === 0)
289
+ return false;
290
+ const integratedHeads = new Set(facts.integrations
291
+ .filter((attempt) => attempt.status === "committed")
292
+ .flatMap((attempt) => facts.changeSets
293
+ .filter((changeSet) => attempt.changeSetIds.includes(changeSet.id))
294
+ .map((changeSet) => changeSet.headCommit)));
295
+ if (integratedHeads.size === 0)
296
+ return false;
297
+ for (const head of integratedHeads) {
298
+ if (!reviewedCommits.has(head))
299
+ return false;
300
+ }
301
+ return true;
302
+ }
303
+ function detectProtocolInconsistency(facts) {
304
+ const changeSetIds = new Set(facts.changeSets.map((changeSet) => changeSet.id));
305
+ const workItemById = new Map(facts.workItems.map((item) => [item.id, item]));
306
+ for (const attempt of facts.integrations) {
307
+ if (attempt.status !== "committed")
308
+ continue;
309
+ const dangling = attempt.changeSetIds
310
+ .filter((id) => !changeSetIds.has(id));
311
+ if (dangling.length > 0) {
312
+ return {
313
+ reason: `Committed Integration ${attempt.id} references missing ChangeSet(s): ${dangling.join(", ")}.`,
314
+ conflicts: [
315
+ ref("integration-attempt", attempt.id),
316
+ ...dangling.map((id) => ref("change-set", id))
317
+ ]
318
+ };
319
+ }
320
+ }
321
+ for (const round of facts.reviewRounds) {
322
+ if (round.status !== "pending" && round.status !== "running")
323
+ continue;
324
+ const item = workItemById.get(round.workItemId);
325
+ if (item !== undefined && item.status === "retired") {
326
+ return {
327
+ reason: `Review ${round.id} is still ${round.status} but its Work Item ${item.id} is retired.`,
328
+ conflicts: [ref("review-round", round.id), ref("work-item", item.id)]
329
+ };
330
+ }
331
+ }
332
+ for (const item of facts.workItems) {
333
+ if (item.status !== "awaiting_acceptance")
334
+ continue;
335
+ if (item.candidates.length === 0) {
336
+ return {
337
+ reason: `Work Item ${item.id} is awaiting acceptance but has no Candidate record.`,
338
+ conflicts: [ref("work-item", item.id)]
339
+ };
340
+ }
341
+ }
342
+ return null;
343
+ }