@zq-silk/yui 0.14.1 → 0.14.2

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 (177) hide show
  1. package/ARCHITECTURE.md +64 -35
  2. package/README.md +254 -124
  3. package/dist/agent/managedRuntimeEnvironment.js +5 -8
  4. package/dist/cli/commandCatalog.js +74 -109
  5. package/dist/cli/helpRenderer.js +1 -2
  6. package/dist/cli/interactionCandidates.js +10 -0
  7. package/dist/cli/interactionPolicy.js +76 -37
  8. package/dist/cli/roleWizard.js +5 -6
  9. package/dist/cli/updateOrchestrator.js +10 -8
  10. package/dist/cli/updatePorts.js +17 -13
  11. package/dist/cli/upgradeCommand.js +2 -5
  12. package/dist/cli.js +268 -231
  13. package/dist/commands/agentCommands.js +13 -0
  14. package/dist/commands/configCommands.js +7 -19
  15. package/dist/commands/configOverview.js +3 -2
  16. package/dist/commands/durableJobCommands.js +1 -1
  17. package/dist/commands/executionAuditCommands.js +4 -9
  18. package/dist/commands/globalRoleCommands.js +17 -0
  19. package/dist/commands/profileCommands.js +200 -30
  20. package/dist/commands/sessionCommands.js +1 -1
  21. package/dist/commands/taskActor.js +29 -127
  22. package/dist/commands/taskChangeSetCommands.js +1 -3
  23. package/dist/commands/taskCommands.js +1771 -2107
  24. package/dist/commands/taskCompletionGate.js +27 -103
  25. package/dist/commands/taskContextCommand.js +47 -80
  26. package/dist/commands/taskExecutionCommands.js +9 -27
  27. package/dist/commands/taskInputCommands.js +10 -10
  28. package/dist/commands/taskIntegrationCommands.js +69 -61
  29. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  30. package/dist/commands/taskNextActionCommand.js +8 -29
  31. package/dist/commands/taskOverviewCommand.js +25 -3
  32. package/dist/commands/taskPublicationCommands.js +151 -85
  33. package/dist/commands/taskPublicationVerifyCommand.js +181 -0
  34. package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
  35. package/dist/commands/taskRoleRuntimeStatus.js +5 -6
  36. package/dist/commands/taskUpstreamCommands.js +79 -95
  37. package/dist/config/configCatalog.js +1 -1
  38. package/dist/context/contextSnapshot.js +17 -2
  39. package/dist/context/sourceTurnContext.js +30 -0
  40. package/dist/context/turnContextPack.js +107 -48
  41. package/dist/context/turnInputContract.js +11 -4
  42. package/dist/context/wakeNotification.js +84 -22
  43. package/dist/controller/agentRuntimeObserver.js +9 -12
  44. package/dist/controller/clientRuntime.js +13 -13
  45. package/dist/controller/controller.js +11 -11
  46. package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
  47. package/dist/controller/jobControl.js +11 -10
  48. package/dist/controller/resourceInventory.js +6 -6
  49. package/dist/controller/resourceInventoryLinux.js +2 -2
  50. package/dist/controller/runtime.js +18 -61
  51. package/dist/controller/runtimeEventInbox.js +57 -34
  52. package/dist/controller/runtimeEventProcessor.js +36 -96
  53. package/dist/controller/runtimeHookTurnFence.js +33 -57
  54. package/dist/controller/runtimeLaunchCoordinator.js +44 -40
  55. package/dist/controller/runtimeObservationHook.js +18 -12
  56. package/dist/controller/sessionNotify.js +43 -28
  57. package/dist/controller/sessionOwnerReconciliation.js +21 -21
  58. package/dist/controller/structuredProviderObservation.js +13 -16
  59. package/dist/coordination/workMailboxQueue.js +132 -0
  60. package/dist/core/controllerServer.js +1 -0
  61. package/dist/domain/agentResultTransport.js +60 -0
  62. package/dist/execution/executionHealth.js +111 -118
  63. package/dist/execution/reviewMainTurn.js +161 -0
  64. package/dist/execution/workItemExecution.js +454 -0
  65. package/dist/execution/workItemExecutionProjection.js +309 -0
  66. package/dist/execution/workItemMainTurn.js +164 -0
  67. package/dist/executor/agentExecutor.js +19 -19
  68. package/dist/executor/executorRegistry.js +7 -28
  69. package/dist/executor/fileRoleLaunchPlanner.js +16 -82
  70. package/dist/external/githubPublicationVerifier.js +114 -0
  71. package/dist/external/gitlabPublicationVerifier.js +158 -0
  72. package/dist/external/pinnedCommandRunner.js +73 -0
  73. package/dist/integration/changeSetManifest.js +0 -3
  74. package/dist/integration/deliveryObligation.js +43 -6
  75. package/dist/integration/gitIntegrationService.js +284 -101
  76. package/dist/integration/integrationAttempt.js +64 -14
  77. package/dist/integration/integrationQueueEntry.js +5 -35
  78. package/dist/integration/integrationQueueService.js +65 -194
  79. package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
  80. package/dist/lifecycle/exactTurnTerminalization.js +100 -97
  81. package/dist/message/message.js +16 -0
  82. package/dist/observability/executionAudit.js +4 -25
  83. package/dist/observability/faultClassification.js +58 -70
  84. package/dist/observability/orchestrationMetrics.js +8 -50
  85. package/dist/profile/agentProfile.js +60 -15
  86. package/dist/profile/agentProfileRuntime.js +100 -0
  87. package/dist/release/releaseWorkflowPorts.js +5 -68
  88. package/dist/repository/executionLaneGitSnapshot.js +80 -0
  89. package/dist/repository/gitWorkspace.js +9 -67
  90. package/dist/repository/taskBaseFreshness.js +3 -1
  91. package/dist/repository/taskWorkspaceCoordinator.js +32 -4
  92. package/dist/repository/taskWorkspacePreparer.js +252 -411
  93. package/dist/resources/liveReferences.js +6 -55
  94. package/dist/resources/resourceDiscovery.js +1 -30
  95. package/dist/resources/resourceRegistrar.js +0 -23
  96. package/dist/review/deltaRecheck.js +8 -65
  97. package/dist/review/reviewAcceptance.js +22 -19
  98. package/dist/review/reviewConfig.js +0 -10
  99. package/dist/review/reviewDecision.js +2 -2
  100. package/dist/review/reviewRound.js +147 -273
  101. package/dist/review/reviewerAvailability.js +13 -23
  102. package/dist/role/role.js +3 -0
  103. package/dist/runtime/agentDriverObservation.js +1 -1
  104. package/dist/runtime/agentHost.js +51 -51
  105. package/dist/runtime/builtinAgentDrivers.js +9 -2
  106. package/dist/runtime/codexAppServerRuntime.js +23 -5
  107. package/dist/runtime/exactControlPlane.js +2 -267
  108. package/dist/runtime/firstProgressAdvisory.js +1 -1
  109. package/dist/runtime/index.js +1 -1
  110. package/dist/runtime/launchBroker.js +11 -11
  111. package/dist/runtime/lifecycleReservation.js +2 -2
  112. package/dist/runtime/managedCaller.js +111 -0
  113. package/dist/runtime/ports.js +16 -3
  114. package/dist/runtime/preallocatedNativeSession.js +2 -2
  115. package/dist/runtime/processExitObservation.js +3 -2
  116. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  117. package/dist/runtime/runtimeBinding.js +1 -1
  118. package/dist/runtime/runtimeObservation.js +33 -16
  119. package/dist/runtime/runtimeSessionCandidate.js +2 -2
  120. package/dist/runtime/runtimeStopReceipt.js +11 -11
  121. package/dist/runtime/sessionLaunchRequest.js +2 -2
  122. package/dist/runtime/sessionOwnerIdentity.js +9 -9
  123. package/dist/runtime/sessionOwnerRegistry.js +13 -13
  124. package/dist/runtime/sessionReconciliation.js +4 -4
  125. package/dist/runtime/sessionTerminationGuard.js +9 -9
  126. package/dist/runtime/sessionTokenMetrics.js +5 -7
  127. package/dist/runtime/structuredProviderHost.js +5 -5
  128. package/dist/runtime/taskRuntimeIsolation.js +24 -24
  129. package/dist/runtime/tmuxAdapters.js +32 -30
  130. package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
  131. package/dist/scheduler/leaderWakeupProcessor.js +78 -55
  132. package/dist/scheduler/roleTurnLiveness.js +6 -6
  133. package/dist/scheduler/roleTurnStall.js +8 -9
  134. package/dist/scheduler/taskExecutionProjection.js +19 -41
  135. package/dist/scheduler/taskObservabilityProjection.js +75 -122
  136. package/dist/storage/sqliteSchema.js +1 -21
  137. package/dist/storage/sqliteStore.js +62 -47
  138. package/dist/storage/storageSchema.js +1 -8
  139. package/dist/storage/storageVersions.js +7 -4
  140. package/dist/storage/taskStore.js +8 -8
  141. package/dist/storage/upgrade/upgradeOrchestrator.js +112 -83
  142. package/dist/task/completionReadiness.js +17 -98
  143. package/dist/task/deliveryGuard.js +9 -11
  144. package/dist/task/draftPlan.js +142 -0
  145. package/dist/task/nextAction.js +179 -477
  146. package/dist/task/publicationVerification.js +1 -0
  147. package/dist/task/remoteDelivery.js +233 -0
  148. package/dist/task/task.js +108 -4
  149. package/dist/task/taskRecordReference.js +0 -1
  150. package/dist/turn/turn.js +109 -16
  151. package/dist/verification/verificationGateService.js +0 -26
  152. package/dist/web/assets/client/components.js +99 -121
  153. package/dist/web/assets/client/i18n.js +136 -58
  154. package/dist/web/assets/client/view.js +44 -0
  155. package/dist/web/assets/styles/cards.js +3 -15
  156. package/dist/web/assets/styles/widgets.js +4 -0
  157. package/dist/web/webSnapshot.js +29 -14
  158. package/dist/workItem/dependencyGate.js +34 -0
  159. package/dist/workItem/workItem.js +99 -266
  160. package/dist/workspace/workItemChangeSetManager.js +15 -59
  161. package/docs/task-local-identity.md +1 -1
  162. package/i18n/README.zh-CN.md +164 -58
  163. package/package.json +1 -1
  164. package/skills/yui-leader/SKILL.md +272 -782
  165. package/skills/yui-operator/SKILL.md +190 -365
  166. package/skills/yui-reviewer/SKILL.md +40 -33
  167. package/skills/yui-runtime/SKILL.md +22 -0
  168. package/skills/yui-worker/SKILL.md +19 -6
  169. package/dist/execution/candidateConvergence.js +0 -623
  170. package/dist/execution/executionGroup.js +0 -854
  171. package/dist/execution/resourceBroker.js +0 -426
  172. package/dist/executor/launchPlan.js +0 -16
  173. package/dist/review/reviewFinding.js +0 -240
  174. package/dist/review/reviewFindingLedger.js +0 -550
  175. package/dist/review/reviewOutcomeClassifier.js +0 -255
  176. package/dist/scheduler/resourceQueueProjection.js +0 -71
  177. package/dist/task/repairWave.js +0 -150
@@ -1,16 +1,16 @@
1
- import { normalizedUniqueIdentities, normalizedUniqueText, requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
1
+ import { normalizedUniqueText, requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
2
2
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
3
3
  import { normalizeCheckResult } from "./checkResult.js";
4
4
  export function createIntegrationAttempt(input, now) {
5
5
  const timestamp = now.toISOString();
6
6
  return validateIntegrationAttempt({
7
- schemaVersion: 5,
7
+ schemaVersion: 6,
8
8
  id: input.id,
9
9
  taskId: input.taskId,
10
10
  projectId: input.projectId,
11
11
  targetRef: input.targetRef,
12
- expectedHead: input.expectedHead,
13
- changeSetIds: [...input.changeSetIds],
12
+ source: input.source,
13
+ beforeCommit: input.beforeCommit,
14
14
  checkCommands: normalizedUniqueText(input.checkCommands ?? [], "Integration check command"),
15
15
  status: "running",
16
16
  createdAt: timestamp,
@@ -123,8 +123,8 @@ export function supersedeIntegration(attempt, reason, now) {
123
123
  });
124
124
  }
125
125
  export function validateIntegrationAttempt(attempt) {
126
- if (attempt.schemaVersion !== 5) {
127
- throw new Error("IntegrationAttempt must use schemaVersion 5.");
126
+ if (attempt.schemaVersion !== 6) {
127
+ throw new Error("IntegrationAttempt must use schemaVersion 6.");
128
128
  }
129
129
  validateTaskRecordReference({
130
130
  taskId: attempt.taskId,
@@ -132,18 +132,29 @@ export function validateIntegrationAttempt(attempt) {
132
132
  }, "integrationAttempt");
133
133
  requireIdentity(attempt.projectId, "Project id");
134
134
  requireText(attempt.targetRef, "Integration target ref");
135
- requireCommit(attempt.expectedHead, "Integration expected head");
136
- normalizedUniqueIdentities(attempt.changeSetIds, "ChangeSet id");
137
- for (const changeSetId of attempt.changeSetIds) {
138
- validateTaskRecordReference({ taskId: attempt.taskId, localId: changeSetId }, "changeSet");
139
- }
135
+ requireCommit(attempt.beforeCommit, "Integration before commit");
136
+ validateIntegrationSource(attempt.taskId, attempt.source);
140
137
  normalizedUniqueText(attempt.checkCommands, "Integration check command");
141
- if (attempt.changeSetIds.length === 0) {
142
- throw new Error("IntegrationAttempt requires at least one ChangeSet.");
143
- }
144
138
  if (attempt.candidateCommit !== undefined) {
145
139
  requireCommit(attempt.candidateCommit, "Integration candidate commit");
146
140
  }
141
+ if (attempt.afterCommit !== undefined) {
142
+ requireCommit(attempt.afterCommit, "Integration after commit");
143
+ }
144
+ if (attempt.summary !== undefined)
145
+ requireText(attempt.summary, "Integration summary");
146
+ if (attempt.status === "committed") {
147
+ requireCommit(attempt.afterCommit ?? "", "Committed Integration after commit");
148
+ requireText(attempt.summary ?? "", "Committed Integration summary");
149
+ if (attempt.candidateCommit !== attempt.afterCommit) {
150
+ throw new Error("Committed Integration candidate and afterCommit must match.");
151
+ }
152
+ if (attempt.source.kind === "work-item"
153
+ && attempt.source.strategy === "manual"
154
+ && attempt.resolution?.action !== "manual-resolution") {
155
+ throw new Error("Committed manual Integration requires a Task-control resolution decision.");
156
+ }
157
+ }
147
158
  if (attempt.jobId !== undefined) {
148
159
  validateTaskRecordReference({ taskId: attempt.taskId, localId: attempt.jobId }, "durableJob");
149
160
  }
@@ -179,6 +190,45 @@ export function validateIntegrationAttempt(attempt) {
179
190
  }
180
191
  return attempt;
181
192
  }
193
+ function validateIntegrationSource(taskId, source) {
194
+ if (typeof source !== "object" || source === null) {
195
+ throw new Error("Integration source is required.");
196
+ }
197
+ if (source.kind === "work-item") {
198
+ validateTaskRecordReference({ taskId, localId: source.workItemId }, "workItem");
199
+ requireCommit(source.startCommit, "WorkItem Integration start commit");
200
+ requireCommit(source.resultCommit, "WorkItem Integration result commit");
201
+ if (!["ff", "cherry-pick", "merge", "manual"].includes(source.strategy)) {
202
+ throw new Error(`WorkItem Integration strategy is invalid: ${String(source.strategy)}.`);
203
+ }
204
+ return;
205
+ }
206
+ if (source.kind === "upstream") {
207
+ requireText(source.branch, "Upstream branch");
208
+ requireCommit(source.remoteCommit, "Upstream remote commit");
209
+ requireCommit(source.taskBaseCommit, "Upstream Task base commit");
210
+ const strategy = source.strategy;
211
+ if (strategy !== "rebase") {
212
+ throw new Error(`Upstream Integration strategy is invalid: ${String(strategy)}.`);
213
+ }
214
+ return;
215
+ }
216
+ if (source.kind === "historical-change-sets") {
217
+ if (!Array.isArray(source.changeSetIds) || source.changeSetIds.length === 0) {
218
+ throw new Error("Historical Integration source requires ChangeSet ids.");
219
+ }
220
+ const seen = new Set();
221
+ for (const changeSetId of source.changeSetIds) {
222
+ validateTaskRecordReference({ taskId, localId: changeSetId }, "changeSet");
223
+ if (seen.has(changeSetId)) {
224
+ throw new Error(`Historical Integration ChangeSet is duplicated: ${changeSetId}.`);
225
+ }
226
+ seen.add(changeSetId);
227
+ }
228
+ return;
229
+ }
230
+ throw new Error("Integration source is invalid.");
231
+ }
182
232
  function normalizeConflictReport(report) {
183
233
  const affectedPaths = normalizedUniqueText(report.affectedPaths, "Conflict path");
184
234
  return {
@@ -47,7 +47,7 @@ export function createConvergedIntegrationQueueEntry(input, now) {
47
47
  }
48
48
  export function markIntegrationQueueRunning(entry, targetBefore, now) {
49
49
  validateIntegrationQueueEntry(entry);
50
- if (!["queued", "conflicted", "validated"].includes(entry.status)) {
50
+ if (!["queued", "conflicted"].includes(entry.status)) {
51
51
  throw new Error(`Integration queue entry cannot run from ${entry.status}: ${entry.id}.`);
52
52
  }
53
53
  const { conflictSummary: _previousConflict, ...rest } = entry;
@@ -95,36 +95,10 @@ export function markIntegrationQueueCommitted(entry, targetAfter, now) {
95
95
  endedAt: timestamp
96
96
  });
97
97
  }
98
- /**
99
- * Records that this entry's reusable evidence was checked against an exact
100
- * target head. The evidence only covers `evidenceTargetHead`; a target
101
- * advance invalidates it unless the delta is provably empty. Path
102
- * disjointness alone never justifies this transition: a gate may read any
103
- * file, so a non-empty target increment cannot be proven irrelevant.
104
- */
105
- export function markIntegrationQueueValidated(entry, now, evidenceTargetHead) {
106
- validateIntegrationQueueEntry(entry);
107
- if (entry.status !== "queued") {
108
- throw new Error(`Integration queue entry cannot validate from ${entry.status}: ${entry.id}.`);
109
- }
110
- if (entry.evidenceRefs.length === 0) {
111
- throw new Error(`Integration queue entry has no reusable evidence: ${entry.id}.`);
112
- }
113
- return validateIntegrationQueueEntry({
114
- ...entry,
115
- status: "validated",
116
- ...(evidenceTargetHead === undefined ? {} : { evidenceTargetHead }),
117
- updatedAt: now.toISOString()
118
- });
119
- }
120
- /**
121
- * A validated entry whose ChangeSet became affected by a later target advance
122
- * loses its evidence coverage and runs its checks again; a conflicted entry is
123
- * manually retried after its underlying issue was fixed.
124
- */
98
+ /** A conflicted entry is manually retried after its underlying issue was fixed. */
125
99
  export function markIntegrationQueueRequeued(entry, now) {
126
100
  validateIntegrationQueueEntry(entry);
127
- if (entry.status !== "validated" && entry.status !== "conflicted") {
101
+ if (entry.status !== "conflicted") {
128
102
  throw new Error(`Integration queue entry cannot requeue from ${entry.status}: ${entry.id}.`);
129
103
  }
130
104
  return validateIntegrationQueueEntry({
@@ -135,7 +109,7 @@ export function markIntegrationQueueRequeued(entry, now) {
135
109
  }
136
110
  export function recordIntegrationQueueAffectedPaths(entry, affectedPaths, now) {
137
111
  validateIntegrationQueueEntry(entry);
138
- if (!["queued", "validated"].includes(entry.status)) {
112
+ if (entry.status !== "queued") {
139
113
  throw new Error(`Integration queue entry cannot recompute overlap from ${entry.status}: ${entry.id}.`);
140
114
  }
141
115
  const previous = new Set(entry.affectedPaths ?? []);
@@ -148,7 +122,7 @@ export function recordIntegrationQueueAffectedPaths(entry, affectedPaths, now) {
148
122
  }
149
123
  export function markIntegrationQueueSuperseded(entry, reason, now) {
150
124
  validateIntegrationQueueEntry(entry);
151
- if (!["queued", "conflicted", "validated"].includes(entry.status)) {
125
+ if (!["queued", "conflicted"].includes(entry.status)) {
152
126
  throw new Error(`Integration queue entry cannot be superseded from ${entry.status}: ${entry.id}.`);
153
127
  }
154
128
  const timestamp = now.toISOString();
@@ -174,7 +148,6 @@ export function validateIntegrationQueueEntry(entry) {
174
148
  "queued",
175
149
  "running",
176
150
  "conflicted",
177
- "validated",
178
151
  "committed",
179
152
  "superseded"
180
153
  ].includes(entry.status)) {
@@ -184,9 +157,6 @@ export function validateIntegrationQueueEntry(entry) {
184
157
  requireCommit(entry.targetBefore, "Integration queue targetBefore");
185
158
  if (entry.targetAfter !== undefined)
186
159
  requireCommit(entry.targetAfter, "Integration queue targetAfter");
187
- if (entry.evidenceTargetHead !== undefined) {
188
- requireCommit(entry.evidenceTargetHead, "Integration queue evidenceTargetHead");
189
- }
190
160
  if (entry.integrationAttemptId !== undefined) {
191
161
  normalizedUniqueIdentities([entry.integrationAttemptId], "Integration attempt id");
192
162
  }
@@ -1,9 +1,8 @@
1
1
  import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
2
2
  import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
3
3
  import { createIntegrationAttempt, recordResolutionDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
4
- import { createConvergedIntegrationQueueEntry, createIntegrationQueueEntry, markIntegrationQueueBlocked, markIntegrationQueueCommitted, markIntegrationQueueRequeued, markIntegrationQueueRunning, markIntegrationQueueSuperseded, markIntegrationQueueValidated, recordIntegrationQueueAffectedPaths, recordIntegrationQueueAttempt } from "./integrationQueueEntry.js";
4
+ import { createConvergedIntegrationQueueEntry, createIntegrationQueueEntry, markIntegrationQueueBlocked, markIntegrationQueueCommitted, markIntegrationQueueRequeued, markIntegrationQueueRunning, markIntegrationQueueSuperseded, recordIntegrationQueueAffectedPaths, recordIntegrationQueueAttempt } from "./integrationQueueEntry.js";
5
5
  import { GitIntegrationService } from "./gitIntegrationService.js";
6
- import { gateArtifactCoversCheckCommands } from "../verification/verificationGateService.js";
7
6
  /**
8
7
  * Producer WorkItem statuses that close the WorkItem lifecycle. A ChangeSet
9
8
  * captured from a WorkItem that reached one of these without ever producing a
@@ -197,13 +196,14 @@ export async function enqueueIntegrationQueueEntry(input) {
197
196
  throw new Error(`Task main worktree is not ready; reconcile the Task first: ${task.id}.`);
198
197
  }
199
198
  const canonicalTargetRef = canonicalizeTargetRef(targetRef);
200
- const targetHead = (await git.inspect(project.path, exactBranchRef(canonicalTargetRef))).baseCommit;
201
- let equivalent = await findEquivalentCommit(git, project.path, changeSet.headCommit, targetHead, changeSet.baseCommit);
199
+ const repositoryPath = taskMainRepositoryPath(input.store, task.id, project.id);
200
+ const targetHead = (await git.inspect(repositoryPath, exactBranchRef(canonicalTargetRef))).baseCommit;
201
+ let equivalent = await findEquivalentCommit(git, repositoryPath, changeSet.headCommit, targetHead, changeSet.baseCommit);
202
202
  if (equivalent === null) {
203
203
  // The identical change may already have landed through another commit
204
204
  // while the target also moved on with unrelated work: the whole-tree
205
205
  // check misses it, but the touched paths already agree.
206
- equivalent = await findContainedChangeSet(git, project.path, changeSet, targetHead);
206
+ equivalent = await findContainedChangeSet(git, repositoryPath, changeSet, targetHead);
207
207
  }
208
208
  return input.store.transactionAsync(async (tx) => {
209
209
  const duplicate = findActiveQueueDuplicate(tx, task.id, input.projectId, input.changeSetId);
@@ -252,7 +252,7 @@ export async function enqueueIntegrationQueueEntry(input) {
252
252
  // proof if it moved.
253
253
  let effectiveTargetHead = targetHead;
254
254
  if (equivalent !== null) {
255
- effectiveTargetHead = (await git.inspect(project.path, exactBranchRef(canonicalTargetRef))).baseCommit;
255
+ effectiveTargetHead = (await git.inspect(repositoryPath, exactBranchRef(canonicalTargetRef))).baseCommit;
256
256
  if (effectiveTargetHead !== targetHead) {
257
257
  equivalent = null;
258
258
  }
@@ -261,7 +261,7 @@ export async function enqueueIntegrationQueueEntry(input) {
261
261
  // advance the target during the call, so the returned head may
262
262
  // already be stale. Verify with a second read; if it moved, the
263
263
  // convergence proof is stale and the entry must queue.
264
- const verifiedHead = (await git.inspect(project.path, exactBranchRef(canonicalTargetRef))).baseCommit;
264
+ const verifiedHead = (await git.inspect(repositoryPath, exactBranchRef(canonicalTargetRef))).baseCommit;
265
265
  if (verifiedHead !== effectiveTargetHead) {
266
266
  equivalent = null;
267
267
  effectiveTargetHead = verifiedHead;
@@ -269,25 +269,14 @@ export async function enqueueIntegrationQueueEntry(input) {
269
269
  }
270
270
  }
271
271
  if (equivalent === null || requestedChecks.length > 0) {
272
- let entry = createIntegrationQueueEntry({
272
+ const entry = createIntegrationQueueEntry({
273
273
  id,
274
274
  taskId: task.id,
275
275
  projectId: input.projectId,
276
276
  changeSetId: input.changeSetId,
277
277
  targetRef: canonicalTargetRef,
278
- checkCommands: requestedChecks,
279
- evidenceRefs: changeSet.manifest.evidenceRefs
278
+ checkCommands: requestedChecks
280
279
  }, now());
281
- // Durable positive exact-SHA evidence on an unchanged target, at the lane
282
- // head, validates the entry so processing skips its checks. The process-
283
- // path evidence fence still invalidates the binding if the target moves
284
- // before this entry runs, so a non-empty advance never rebinds old
285
- // evidence. A converged ChangeSet with explicit gates queues normally:
286
- // the no-op apply still runs the caller's checks against the current
287
- // target instead of waiving them.
288
- if (await canReuseEvidenceAtHead(tx, task.id, tx.listIntegrationQueueEntries(task.id), entry, changeSet, effectiveTargetHead)) {
289
- entry = markIntegrationQueueValidated(entry, now(), effectiveTargetHead);
290
- }
291
280
  tx.saveIntegrationQueueEntry(task.id, entry);
292
281
  return { entry, outcome: "queued" };
293
282
  }
@@ -314,10 +303,21 @@ export async function enqueueIntegrationQueueEntry(input) {
314
303
  taskId: task.id,
315
304
  projectId: input.projectId,
316
305
  targetRef: canonicalTargetRef,
317
- expectedHead: effectiveTargetHead,
318
- changeSetIds: [input.changeSetId],
306
+ beforeCommit: effectiveTargetHead,
307
+ source: {
308
+ kind: "work-item",
309
+ workItemId: changeSet.workItemId,
310
+ startCommit: changeSet.baseCommit,
311
+ resultCommit: changeSet.headCommit,
312
+ strategy: "cherry-pick"
313
+ },
319
314
  checkCommands: []
320
- }, now()), { status: "committed", candidateCommit: effectiveTargetHead }, now());
315
+ }, now()), {
316
+ status: "committed",
317
+ candidateCommit: effectiveTargetHead,
318
+ afterCommit: effectiveTargetHead,
319
+ summary: `WorkItem ${changeSet.workItemId} result was already represented; Task head was unchanged.`
320
+ }, now());
321
321
  const convergedEntry = createConvergedIntegrationQueueEntry({
322
322
  id,
323
323
  taskId: task.id,
@@ -331,7 +331,7 @@ export async function enqueueIntegrationQueueEntry(input) {
331
331
  integrationAttemptId: attempt.id
332
332
  }, now());
333
333
  try {
334
- await git.assertRefAt(project.path, exactBranchRef(canonicalTargetRef), effectiveTargetHead);
334
+ await git.assertRefAt(repositoryPath, exactBranchRef(canonicalTargetRef), effectiveTargetHead);
335
335
  }
336
336
  catch {
337
337
  const queuedEntry = createIntegrationQueueEntry({
@@ -340,8 +340,7 @@ export async function enqueueIntegrationQueueEntry(input) {
340
340
  projectId: input.projectId,
341
341
  changeSetId: input.changeSetId,
342
342
  targetRef: canonicalTargetRef,
343
- checkCommands: requestedChecks,
344
- evidenceRefs: changeSet.manifest.evidenceRefs
343
+ checkCommands: requestedChecks
345
344
  }, now());
346
345
  tx.saveIntegrationQueueEntry(task.id, queuedEntry);
347
346
  return { entry: queuedEntry, outcome: "queued" };
@@ -358,82 +357,10 @@ function findActiveQueueDuplicate(store, taskId, projectId, changeSetId) {
358
357
  && entry.status !== "superseded");
359
358
  }
360
359
  /**
361
- * Whether a freshly queued entry may start `validated` (skipping its checks):
362
- * its ChangeSet carries durable positive exact-SHA evidence, the target is
363
- * unchanged since the ChangeSet was based (so integrating fast-forwards to
364
- * the reviewed head), no earlier lane entry will advance the target first,
365
- * and the evidence covers every requested check command. Entries behind a
366
- * lane mate stay `queued`: the mate's commit moves the target, so the
367
- * evidence would not cover their integration anyway. A review check that
368
- * passed for command X never waives a different gate Y.
369
- */
370
- async function canReuseEvidenceAtHead(store, taskId, entries, entry, changeSet, targetHead) {
371
- if (entry.evidenceRefs.length === 0)
372
- return false;
373
- if (targetHead !== changeSet.baseCommit)
374
- return false;
375
- const lane = queueLaneKey(entry);
376
- if (entries.some((existing) => queueLaneKey(existing) === lane
377
- && existing.status !== "committed"
378
- && existing.status !== "superseded"))
379
- return false;
380
- return evidenceCoversCheckCommands(store, taskId, entry.evidenceRefs, entry.checkCommands, changeSet.headCommit, entry.projectId);
381
- }
382
- /**
383
- * Resolve each reusable evidence reference to its ReviewRound and collect the
384
- * names of checks that passed. A requested gate command is covered only when
385
- * a resolved round recorded that exact command as passed. A review that ran
386
- * `true` (or any other unrelated check) never waives a different gate.
387
- *
388
- * A check is reusable only when it ran on the frozen candidate tree. A
389
- * ReviewRound must attest to this by recording its `evidenceCommit` as the
390
- * exact `reviewBaseCommit`. A missing `evidenceCommit` is ambiguous: the
391
- * reviewer may have run checks with uncommitted dirty diagnostics, so the
392
- * absence cannot be treated as frozen-tree proof. A round whose
393
- * `evidenceCommit` differs from its `reviewBaseCommit` ran its checks on a
394
- * diagnostic tree (the reviewer's own commit with review-only changes); those
395
- * checks proved a different tree and cannot waive the candidate gate.
396
- */
397
- async function evidenceCoversCheckCommands(store, taskId, evidenceRefs, checkCommands, candidateCommit, projectId) {
398
- if (checkCommands.length === 0)
399
- return true;
400
- const covered = new Set();
401
- for (const ref of evidenceRefs) {
402
- // Issue 08: a `gate-artifact:` ref covers the exact commands its L2
403
- // artifact passed on the exact candidate commit. The store resolves the
404
- // artifact through its persistence backend (SQLite or file).
405
- if (ref.startsWith("gate-artifact:")) {
406
- if (await gateArtifactCoversCheckCommands(store, projectId, ref, checkCommands, candidateCommit)) {
407
- for (const command of checkCommands)
408
- covered.add(command);
409
- }
410
- continue;
411
- }
412
- const roundId = parseReviewRoundRef(ref);
413
- if (roundId === undefined)
414
- continue;
415
- const round = store.getReviewRound(taskId, roundId);
416
- if (round === null || round.status !== "completed")
417
- continue;
418
- if (round.evidenceCommit !== round.reviewBaseCommit)
419
- continue;
420
- for (const check of round.checks ?? []) {
421
- if (check.outcome === "passed")
422
- covered.add(check.name);
423
- }
424
- }
425
- return checkCommands.every((cmd) => covered.has(cmd));
426
- }
427
- function parseReviewRoundRef(ref) {
428
- const prefix = "review-round:";
429
- return ref.startsWith(prefix) ? ref.slice(prefix.length) : undefined;
430
- }
431
- /**
432
- * Process queued items in id order. Each item gets a fresh IntegrationAttempt
360
+ * Process queued items in id order. Each item gets a fresh IntegrationAttempt
433
361
  * on the exact current target; conflicts and gate failures map the item to
434
362
  * `conflicted` without touching the others. After every commit the remaining
435
- * items recompute overlap, and an item whose own paths became affected loses
436
- * its evidence coverage and runs its checks again.
363
+ * items recompute overlap diagnostics.
437
364
  *
438
365
  * A lane (one Project/targetRef pair) integrates one item at a time: while an
439
366
  * item is `running` — claimed by this or another processor — no other item of
@@ -467,22 +394,8 @@ export async function processIntegrationQueue(store, home, taskId, options = {})
467
394
  const project = store.getProject(candidate.projectId);
468
395
  if (project === null)
469
396
  throw new Error(`Project not found: ${candidate.projectId}.`);
470
- const targetBefore = (await git.inspect(project.path, exactBranchRef(candidate.targetRef))).baseCommit;
471
- // Evidence fence: a validated entry's reusable evidence only covers the
472
- // exact target head it was validated against. An out-of-band target
473
- // advance (another Task) that touches the entry's paths, or whose impact
474
- // cannot be proven, invalidates the evidence: requeue and run its checks.
475
- if (candidate.status === "validated"
476
- && await evidenceTargetAdvanced(git, project.path, candidate, targetBefore, store, task.id)) {
477
- store.transaction((tx) => {
478
- assertTaskActive(tx, task.id);
479
- const current = tx.getIntegrationQueueEntry(task.id, candidate.id);
480
- if (current !== null && current.status === "validated") {
481
- tx.saveIntegrationQueueEntry(task.id, markIntegrationQueueRequeued(current, now()));
482
- }
483
- });
484
- continue;
485
- }
397
+ const repositoryPath = taskMainRepositoryPath(store, task.id, project.id);
398
+ const targetBefore = (await git.inspect(repositoryPath, exactBranchRef(candidate.targetRef))).baseCommit;
486
399
  // CAS claim: re-read the entry inside the write transaction. Another
487
400
  // store instance may have taken it since selection; if the status changed,
488
401
  // do not create an Attempt and re-select instead. The running barrier is
@@ -490,8 +403,7 @@ export async function processIntegrationQueue(store, home, taskId, options = {})
490
403
  // between selection and claim serializes this entry behind it.
491
404
  const claimed = store.transaction((tx) => {
492
405
  const current = tx.getIntegrationQueueEntry(task.id, candidate.id);
493
- if (current === null
494
- || (current.status !== "queued" && current.status !== "validated")) {
406
+ if (current === null || current.status !== "queued") {
495
407
  return { skipped: true };
496
408
  }
497
409
  if (laneBlockedByRunningEntry(tx.listIntegrationQueueEntries(task.id), current)) {
@@ -506,8 +418,8 @@ export async function processIntegrationQueue(store, home, taskId, options = {})
506
418
  // entered retry (or whose latest Candidate no longer matches) must not
507
419
  // advance the target. Mark it conflicted so the Leader can supersede
508
420
  // or re-enqueue a fresh ChangeSet.
421
+ const changeSet = tx.getChangeSet(task.id, current.changeSetId);
509
422
  try {
510
- const changeSet = tx.getChangeSet(task.id, current.changeSetId);
511
423
  if (changeSet === null) {
512
424
  throw new Error(`ChangeSet not found: ${current.changeSetId}.`);
513
425
  }
@@ -519,14 +431,23 @@ export async function processIntegrationQueue(store, home, taskId, options = {})
519
431
  tx.saveIntegrationQueueEntry(task.id, markIntegrationQueueBlocked(running, error instanceof Error ? error.message : String(error), now()));
520
432
  return { skipped: true };
521
433
  }
434
+ if (changeSet === null) {
435
+ throw new Error(`ChangeSet not found after validation: ${current.changeSetId}.`);
436
+ }
522
437
  const attempt = createIntegrationAttempt({
523
438
  id: tx.nextIntegrationAttemptId(task.id),
524
439
  taskId: task.id,
525
440
  projectId: current.projectId,
526
441
  targetRef: current.targetRef,
527
- expectedHead: targetBefore,
528
- changeSetIds: [current.changeSetId],
529
- checkCommands: current.status === "validated" ? [] : current.checkCommands
442
+ beforeCommit: targetBefore,
443
+ source: {
444
+ kind: "work-item",
445
+ workItemId: changeSet.workItemId,
446
+ startCommit: changeSet.baseCommit,
447
+ resultCommit: changeSet.headCommit,
448
+ strategy: "cherry-pick"
449
+ },
450
+ checkCommands: current.checkCommands
530
451
  }, now());
531
452
  tx.saveIntegrationAttempt(task.id, attempt);
532
453
  const entry = recordIntegrationQueueAttempt(markIntegrationQueueRunning(current, targetBefore, now()), attempt.id, now());
@@ -553,10 +474,7 @@ export async function processIntegrationQueue(store, home, taskId, options = {})
553
474
  return entry;
554
475
  });
555
476
  if (result.status === "committed") {
556
- const project = store.getProject(settled.projectId);
557
- if (project !== null) {
558
- await recomputeAffectedPaths(store, task.id, settled, project.path, git, now);
559
- }
477
+ await recomputeAffectedPaths(store, task.id, settled, taskMainRepositoryPath(store, task.id, settled.projectId), git, now);
560
478
  }
561
479
  processed.push({ entry: settled, attempt: result.attempt, result });
562
480
  }
@@ -588,7 +506,7 @@ function queueLaneKey(entry) {
588
506
  return `${entry.projectId} ${canonicalizeTargetRef(entry.targetRef)}`;
589
507
  }
590
508
  /**
591
- * The first entry a processor may claim: the earliest queued/validated entry
509
+ * The first entry a processor may claim: the earliest queued entry
592
510
  * whose lane has no running entry anywhere in it. A running entry serializes
593
511
  * its whole lane — predecessors and successors alike — so a requeued
594
512
  * predecessor cannot slip past a successor that is already running.
@@ -605,7 +523,7 @@ function firstClaimableQueueEntry(entries, projectId) {
605
523
  for (const entry of entries) {
606
524
  if (projectId !== undefined && entry.projectId !== projectId)
607
525
  continue;
608
- if ((entry.status === "queued" || entry.status === "validated")
526
+ if (entry.status === "queued"
609
527
  && !blockedLanes.has(queueLaneKey(entry))) {
610
528
  return entry;
611
529
  }
@@ -624,50 +542,6 @@ function laneBlockedByRunningEntry(entries, candidate) {
624
542
  && entry.status === "running"
625
543
  && queueLaneKey(entry) === lane);
626
544
  }
627
- /**
628
- * Whether a validated entry's reusable evidence no longer covers the current
629
- * target. The evidence only proves the gate as of `entry.evidenceTargetHead`;
630
- * an out-of-band target advance (another Task) on the entry's own paths, or any
631
- * advance whose impact on the entry's gate cannot be proven, invalidates it.
632
- * Returns false (evidence still covers the target) only when proven unaffected.
633
- */
634
- async function evidenceTargetAdvanced(git, repositoryPath, entry, currentTargetHead, store, taskId) {
635
- const boundary = entry.evidenceTargetHead;
636
- if (boundary === undefined)
637
- return true;
638
- if (boundary === currentTargetHead)
639
- return false;
640
- // The target advanced out of band since the evidence boundary. Compute the
641
- // real path delta and re-run the gate unless the entry is provably unaffected.
642
- if (git.changedFilesBetween === undefined)
643
- return true;
644
- let delta;
645
- try {
646
- delta = await git.changedFilesBetween({
647
- repositoryPath,
648
- fromCommit: boundary,
649
- toCommit: currentTargetHead
650
- });
651
- }
652
- catch {
653
- return true;
654
- }
655
- const changeSet = store.getChangeSet(taskId, entry.changeSetId);
656
- if (changeSet === null)
657
- return true;
658
- const ownPaths = new Set([
659
- ...changeSet.changedPaths,
660
- ...changeSet.manifest.deletedPaths
661
- ]);
662
- if (delta.some((path) => ownPaths.has(path)))
663
- return true;
664
- // The delta does not touch the entry's own paths, but the entry's gate may
665
- // read any file — including the exact target SHA the evidence is bound to.
666
- // A changed target SHA invalidates exact-SHA evidence regardless of tree
667
- // identity, so any entry with checks must re-run its gate. An entry
668
- // without checks has nothing to re-run and may commit directly.
669
- return entry.checkCommands.length > 0;
670
- }
671
545
  /**
672
546
  * A conflicted entry whose manual-resolution Attempt committed converges. The
673
547
  * settle happens in one transaction; the downstream affected-path recompute
@@ -693,10 +567,7 @@ export async function reconcileIntegrationQueueEntry(store, taskId, entryId, git
693
567
  tx.saveIntegrationQueueEntry(taskId, settled);
694
568
  return settled;
695
569
  });
696
- const project = store.getProject(committed.projectId);
697
- if (project !== null) {
698
- await recomputeAffectedPaths(store, taskId, committed, project.path, git, now);
699
- }
570
+ await recomputeAffectedPaths(store, taskId, committed, taskMainRepositoryPath(store, taskId, committed.projectId), git, now);
700
571
  return committed;
701
572
  }
702
573
  /**
@@ -786,10 +657,7 @@ async function reconcileTerminalAttempts(store, taskId, git, now) {
786
657
  // Crash window: the settle marked this entry committed but died before
787
658
  // recomputing downstream affectedPaths. Replay that update idempotently
788
659
  // so waiting entries regain their overlap evidence.
789
- const project = store.getProject(entry.projectId);
790
- if (project !== null) {
791
- await recomputeAffectedPaths(store, taskId, entry, project.path, git, now);
792
- }
660
+ await recomputeAffectedPaths(store, taskId, entry, taskMainRepositoryPath(store, taskId, entry.projectId), git, now);
793
661
  continue;
794
662
  }
795
663
  if (entry.status !== "conflicted" && entry.status !== "running")
@@ -800,10 +668,7 @@ async function reconcileTerminalAttempts(store, taskId, git, now) {
800
668
  if (attempt.status === "committed") {
801
669
  const committed = markIntegrationQueueCommitted(entry, committedTargetAfter(attempt), now());
802
670
  store.saveIntegrationQueueEntry(taskId, committed);
803
- const project = store.getProject(committed.projectId);
804
- if (project !== null) {
805
- await recomputeAffectedPaths(store, taskId, committed, project.path, git, now);
806
- }
671
+ await recomputeAffectedPaths(store, taskId, committed, taskMainRepositoryPath(store, taskId, committed.projectId), git, now);
807
672
  continue;
808
673
  }
809
674
  // Crash window: the Attempt persisted a terminal failed/blocked outcome
@@ -832,7 +697,7 @@ async function recomputeAffectedPaths(store, taskId, committed, projectPath, git
832
697
  continue;
833
698
  if (canonicalizeTargetRef(entry.targetRef) !== canonicalizeTargetRef(committed.targetRef))
834
699
  continue;
835
- if (entry.status !== "queued" && entry.status !== "validated")
700
+ if (entry.status !== "queued")
836
701
  continue;
837
702
  const changeSet = store.getChangeSet(taskId, entry.changeSetId);
838
703
  if (changeSet === null)
@@ -853,21 +718,16 @@ async function recomputeAffectedPaths(store, taskId, committed, projectPath, git
853
718
  const current = tx.getIntegrationQueueEntry(taskId, entry.id);
854
719
  if (current === null)
855
720
  return;
856
- if (current.status !== "queued" && current.status !== "validated")
721
+ if (current.status !== "queued")
857
722
  return;
858
- let updated = recordIntegrationQueueAffectedPaths(current, affected, now());
859
- if (updated.status === "validated" && (updated.affectedPaths?.length ?? 0) > 0) {
860
- // The target advanced onto this entry's own paths: its evidence no
861
- // longer covers the target, so the gate must run again.
862
- updated = markIntegrationQueueRequeued(updated, now());
863
- }
723
+ const updated = recordIntegrationQueueAffectedPaths(current, affected, now());
864
724
  // A queued entry keeps its place and runs its checks against the exact
865
725
  // current target at process time. Disjoint changedPaths must never
866
726
  // rebind its evidence to the new target head: a gate may read any file,
867
727
  // so a non-empty target increment cannot be proven irrelevant from path
868
728
  // metadata alone.
869
- if (updated.status !== current.status
870
- || (updated.affectedPaths ?? []).join("\n") !== (current.affectedPaths ?? []).join("\n")) {
729
+ if ((updated.affectedPaths ?? []).join("\n")
730
+ !== (current.affectedPaths ?? []).join("\n")) {
871
731
  tx.saveIntegrationQueueEntry(taskId, updated);
872
732
  }
873
733
  });
@@ -937,3 +797,14 @@ function taskMainBranch(store, taskId, projectId) {
937
797
  return undefined;
938
798
  return workspaceProjectEntry(mainWorkspace, projectId)?.branch;
939
799
  }
800
+ function taskMainRepositoryPath(store, taskId, projectId) {
801
+ const mainWorkspace = store.getTaskWorkspace(taskId);
802
+ if (mainWorkspace === null || mainWorkspace.owner.type !== "task") {
803
+ throw new Error(`Task has no authoritative main workspace: ${taskId}.`);
804
+ }
805
+ const entry = workspaceProjectEntry(mainWorkspace, projectId);
806
+ if (entry === undefined) {
807
+ throw new Error(`Task main workspace has no Project: ${taskId}/${projectId}.`);
808
+ }
809
+ return entry.path;
810
+ }