@zq-silk/yui 0.14.0 → 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 (179) hide show
  1. package/ARCHITECTURE.md +64 -35
  2. package/README.md +258 -125
  3. package/dist/agent/argumentPolicy.js +2 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +5 -8
  5. package/dist/cli/commandCatalog.js +74 -109
  6. package/dist/cli/helpRenderer.js +1 -2
  7. package/dist/cli/interactionCandidates.js +10 -0
  8. package/dist/cli/interactionPolicy.js +76 -37
  9. package/dist/cli/roleWizard.js +5 -6
  10. package/dist/cli/updateOrchestrator.js +10 -8
  11. package/dist/cli/updatePorts.js +17 -13
  12. package/dist/cli/upgradeCommand.js +2 -5
  13. package/dist/cli.js +268 -231
  14. package/dist/commands/agentCommands.js +13 -0
  15. package/dist/commands/configCommands.js +7 -19
  16. package/dist/commands/configOverview.js +3 -2
  17. package/dist/commands/durableJobCommands.js +1 -1
  18. package/dist/commands/executionAuditCommands.js +4 -9
  19. package/dist/commands/globalRoleCommands.js +17 -0
  20. package/dist/commands/profileCommands.js +200 -30
  21. package/dist/commands/sessionCommands.js +1 -1
  22. package/dist/commands/taskActor.js +29 -127
  23. package/dist/commands/taskChangeSetCommands.js +1 -3
  24. package/dist/commands/taskCommands.js +1771 -2107
  25. package/dist/commands/taskCompletionGate.js +27 -103
  26. package/dist/commands/taskContextCommand.js +47 -80
  27. package/dist/commands/taskExecutionCommands.js +9 -27
  28. package/dist/commands/taskInputCommands.js +10 -10
  29. package/dist/commands/taskIntegrationCommands.js +69 -61
  30. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  31. package/dist/commands/taskNextActionCommand.js +8 -29
  32. package/dist/commands/taskOverviewCommand.js +25 -3
  33. package/dist/commands/taskPublicationCommands.js +151 -85
  34. package/dist/commands/taskPublicationVerifyCommand.js +181 -0
  35. package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
  36. package/dist/commands/taskRoleRuntimeStatus.js +5 -6
  37. package/dist/commands/taskUpstreamCommands.js +79 -95
  38. package/dist/config/configCatalog.js +1 -1
  39. package/dist/context/contextSnapshot.js +17 -2
  40. package/dist/context/sessionBootstrapManifest.js +18 -1
  41. package/dist/context/sourceTurnContext.js +30 -0
  42. package/dist/context/turnContextPack.js +107 -48
  43. package/dist/context/turnInputContract.js +11 -4
  44. package/dist/context/wakeNotification.js +84 -22
  45. package/dist/controller/agentRuntimeObserver.js +9 -12
  46. package/dist/controller/clientRuntime.js +13 -13
  47. package/dist/controller/controller.js +11 -11
  48. package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
  49. package/dist/controller/jobControl.js +11 -10
  50. package/dist/controller/resourceInventory.js +6 -6
  51. package/dist/controller/resourceInventoryLinux.js +2 -2
  52. package/dist/controller/runtime.js +18 -61
  53. package/dist/controller/runtimeEventInbox.js +57 -34
  54. package/dist/controller/runtimeEventProcessor.js +36 -96
  55. package/dist/controller/runtimeHookTurnFence.js +33 -57
  56. package/dist/controller/runtimeLaunchCoordinator.js +44 -40
  57. package/dist/controller/runtimeObservationHook.js +18 -12
  58. package/dist/controller/sessionNotify.js +43 -28
  59. package/dist/controller/sessionOwnerReconciliation.js +21 -21
  60. package/dist/controller/structuredProviderObservation.js +13 -16
  61. package/dist/coordination/workMailboxQueue.js +132 -0
  62. package/dist/core/controllerServer.js +1 -0
  63. package/dist/domain/agentResultTransport.js +60 -0
  64. package/dist/execution/executionHealth.js +111 -118
  65. package/dist/execution/reviewMainTurn.js +161 -0
  66. package/dist/execution/workItemExecution.js +454 -0
  67. package/dist/execution/workItemExecutionProjection.js +309 -0
  68. package/dist/execution/workItemMainTurn.js +164 -0
  69. package/dist/executor/agentExecutor.js +19 -19
  70. package/dist/executor/executorRegistry.js +7 -28
  71. package/dist/executor/fileRoleLaunchPlanner.js +51 -83
  72. package/dist/external/githubPublicationVerifier.js +114 -0
  73. package/dist/external/gitlabPublicationVerifier.js +158 -0
  74. package/dist/external/pinnedCommandRunner.js +73 -0
  75. package/dist/integration/changeSetManifest.js +0 -3
  76. package/dist/integration/deliveryObligation.js +43 -6
  77. package/dist/integration/gitIntegrationService.js +284 -101
  78. package/dist/integration/integrationAttempt.js +64 -14
  79. package/dist/integration/integrationQueueEntry.js +5 -35
  80. package/dist/integration/integrationQueueService.js +65 -194
  81. package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
  82. package/dist/lifecycle/exactTurnTerminalization.js +100 -97
  83. package/dist/message/message.js +16 -0
  84. package/dist/observability/executionAudit.js +4 -25
  85. package/dist/observability/faultClassification.js +58 -70
  86. package/dist/observability/orchestrationMetrics.js +8 -50
  87. package/dist/profile/agentProfile.js +60 -15
  88. package/dist/profile/agentProfileRuntime.js +100 -0
  89. package/dist/release/releaseWorkflowPorts.js +5 -68
  90. package/dist/repository/executionLaneGitSnapshot.js +80 -0
  91. package/dist/repository/gitWorkspace.js +9 -67
  92. package/dist/repository/taskBaseFreshness.js +3 -1
  93. package/dist/repository/taskWorkspaceCoordinator.js +32 -4
  94. package/dist/repository/taskWorkspacePreparer.js +252 -411
  95. package/dist/resources/liveReferences.js +6 -55
  96. package/dist/resources/resourceDiscovery.js +1 -30
  97. package/dist/resources/resourceRegistrar.js +0 -23
  98. package/dist/review/deltaRecheck.js +8 -65
  99. package/dist/review/reviewAcceptance.js +22 -19
  100. package/dist/review/reviewConfig.js +0 -10
  101. package/dist/review/reviewDecision.js +2 -2
  102. package/dist/review/reviewRound.js +147 -273
  103. package/dist/review/reviewerAvailability.js +13 -23
  104. package/dist/role/role.js +3 -0
  105. package/dist/runtime/agentDriverObservation.js +1 -1
  106. package/dist/runtime/agentHost.js +51 -51
  107. package/dist/runtime/builtinAgentDrivers.js +9 -2
  108. package/dist/runtime/codexAppServerRuntime.js +23 -5
  109. package/dist/runtime/exactControlPlane.js +2 -267
  110. package/dist/runtime/firstProgressAdvisory.js +1 -1
  111. package/dist/runtime/index.js +1 -1
  112. package/dist/runtime/launchBroker.js +11 -11
  113. package/dist/runtime/lifecycleReservation.js +2 -2
  114. package/dist/runtime/managedCaller.js +111 -0
  115. package/dist/runtime/ports.js +16 -3
  116. package/dist/runtime/preallocatedNativeSession.js +2 -2
  117. package/dist/runtime/processExitObservation.js +3 -2
  118. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  119. package/dist/runtime/runtimeBinding.js +1 -1
  120. package/dist/runtime/runtimeObservation.js +33 -16
  121. package/dist/runtime/runtimeSessionCandidate.js +2 -2
  122. package/dist/runtime/runtimeStopReceipt.js +11 -11
  123. package/dist/runtime/sessionLaunchRequest.js +2 -2
  124. package/dist/runtime/sessionOwnerIdentity.js +9 -9
  125. package/dist/runtime/sessionOwnerRegistry.js +13 -13
  126. package/dist/runtime/sessionReconciliation.js +4 -4
  127. package/dist/runtime/sessionTerminationGuard.js +9 -9
  128. package/dist/runtime/sessionTokenMetrics.js +5 -7
  129. package/dist/runtime/structuredProviderHost.js +5 -5
  130. package/dist/runtime/taskRuntimeIsolation.js +24 -24
  131. package/dist/runtime/tmuxAdapters.js +32 -30
  132. package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
  133. package/dist/scheduler/leaderWakeupProcessor.js +78 -55
  134. package/dist/scheduler/roleTurnLiveness.js +6 -6
  135. package/dist/scheduler/roleTurnStall.js +8 -9
  136. package/dist/scheduler/taskExecutionProjection.js +19 -41
  137. package/dist/scheduler/taskObservabilityProjection.js +75 -122
  138. package/dist/storage/sqliteSchema.js +1 -21
  139. package/dist/storage/sqliteStore.js +62 -47
  140. package/dist/storage/storageSchema.js +1 -8
  141. package/dist/storage/storageVersions.js +7 -4
  142. package/dist/storage/taskStore.js +8 -8
  143. package/dist/storage/upgrade/upgradeOrchestrator.js +112 -83
  144. package/dist/task/completionReadiness.js +17 -98
  145. package/dist/task/deliveryGuard.js +9 -11
  146. package/dist/task/draftPlan.js +142 -0
  147. package/dist/task/nextAction.js +179 -477
  148. package/dist/task/publicationVerification.js +1 -0
  149. package/dist/task/remoteDelivery.js +233 -0
  150. package/dist/task/task.js +108 -4
  151. package/dist/task/taskRecordReference.js +0 -1
  152. package/dist/turn/turn.js +109 -16
  153. package/dist/verification/verificationGateService.js +0 -26
  154. package/dist/web/assets/client/components.js +99 -121
  155. package/dist/web/assets/client/i18n.js +136 -58
  156. package/dist/web/assets/client/view.js +44 -0
  157. package/dist/web/assets/styles/cards.js +3 -15
  158. package/dist/web/assets/styles/widgets.js +4 -0
  159. package/dist/web/webSnapshot.js +29 -14
  160. package/dist/workItem/dependencyGate.js +34 -0
  161. package/dist/workItem/workItem.js +99 -266
  162. package/dist/workspace/workItemChangeSetManager.js +15 -59
  163. package/docs/task-local-identity.md +1 -1
  164. package/i18n/README.zh-CN.md +165 -59
  165. package/package.json +1 -1
  166. package/skills/yui-leader/SKILL.md +272 -782
  167. package/skills/yui-operator/SKILL.md +190 -365
  168. package/skills/yui-reviewer/SKILL.md +40 -33
  169. package/skills/yui-runtime/SKILL.md +27 -6
  170. package/skills/yui-worker/SKILL.md +19 -6
  171. package/dist/execution/candidateConvergence.js +0 -623
  172. package/dist/execution/executionGroup.js +0 -854
  173. package/dist/execution/resourceBroker.js +0 -426
  174. package/dist/executor/launchPlan.js +0 -16
  175. package/dist/review/reviewFinding.js +0 -240
  176. package/dist/review/reviewFindingLedger.js +0 -550
  177. package/dist/review/reviewOutcomeClassifier.js +0 -255
  178. package/dist/scheduler/resourceQueueProjection.js +0 -71
  179. package/dist/task/repairWave.js +0 -150
@@ -1,10 +1,4 @@
1
- import { changeSetDeliverySettled, governingChangeSets, integrationAttemptRequiresSettlement, latestGoverningQueueEntries } from "../integration/deliveryObligation.js";
2
- import { isReviewFindingBlocking } from "../review/reviewFinding.js";
3
- import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
4
- import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
5
- import { reviewFindingLedgerWriteFailedFromEvents } from "../review/reviewFindingLedger.js";
6
- import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractResolution.js";
7
- import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
1
+ import { governingWorkItemDeliveries, integrationAttemptRequiresSettlement, workItemDeliverySettled } from "../integration/deliveryObligation.js";
8
2
  const ACTIVE_JOB_STATUSES = new Set([
9
3
  "queued",
10
4
  "running",
@@ -17,13 +11,10 @@ const UNRESOLVED_INTEGRATION_STATUSES = new Set([
17
11
  ]);
18
12
  const TERMINAL_REVIEW_STATUSES = new Set(["completed", "failed"]);
19
13
  const TERMINAL_LANE_STATUSES = new Set(["completed", "failed", "skipped"]);
20
- export function projectCompletionReadiness(facts, options = {}) {
14
+ export function projectCompletionReadiness(facts) {
21
15
  const blockers = [];
22
16
  const advisories = [];
23
17
  const { task } = facts;
24
- const findingsGate = options.findingsGate ?? true;
25
- const taskFinalReviewContract = resolveRecordedTaskFinalReviewContract(task.id, facts.workItems, facts.reviewRounds)?.effective;
26
- const taskFinalReviewRequired = taskFinalReviewContract !== undefined;
27
18
  // A pending/running Task-final Review must be resumed or blocked first.
28
19
  for (const round of facts.reviewRounds) {
29
20
  if ((round.scope ?? "work-item") !== "task")
@@ -39,36 +30,6 @@ export function projectCompletionReadiness(facts, options = {}) {
39
30
  : `wait for Reviewer Turn on ${round.id} to finish`
40
31
  });
41
32
  }
42
- // Issue 07: only the latest completed Task-final Review can define whether
43
- // the current review lineage is accepted. Historical delta findings and
44
- // escalations remain audit evidence, but a later completed full Review (or
45
- // accepted delta) supersedes them instead of blocking completion forever.
46
- const latestCompletedTaskReview = facts.reviewRounds
47
- .filter((round) => ((round.scope ?? "work-item") === "task"
48
- && (taskFinalReviewContract === undefined || sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalReviewContract))
49
- && isSemanticReviewRound(round, {
50
- listTurns: () => facts.turns,
51
- listReviewFindings: () => facts.reviewFindings,
52
- listEvents: () => facts.events
53
- })))
54
- .slice()
55
- .sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
56
- || left.id.localeCompare(right.id, undefined, { numeric: true })))
57
- .at(-1);
58
- if (taskFinalReviewRequired
59
- && latestCompletedTaskReview !== undefined
60
- && deltaRecheckBlocksAcceptance(latestCompletedTaskReview)) {
61
- const round = latestCompletedTaskReview;
62
- const disposition = round.deltaRecheck.disposition;
63
- blockers.push({
64
- code: "delta-recheck-not-accepted",
65
- ref: ref("review-round", round.id),
66
- reason: `Delta-recheck ${round.id} disposition is ${disposition}; the head is not accepted.`,
67
- fix: disposition === "requires-full-review"
68
- ? `yui task review request ${task.id} --role <global-role>`
69
- : `repair the finding and request a new Task-final Review`
70
- });
71
- }
72
33
  // Open Input requests block protocol convergence.
73
34
  for (const request of facts.openInputRequests) {
74
35
  blockers.push({
@@ -104,34 +65,16 @@ export function projectCompletionReadiness(facts, options = {}) {
104
65
  fix: `wait for DurableJob ${job.id} to finish, or cancel it`
105
66
  });
106
67
  }
107
- // Integration obligations follow only the Candidate that currently governs
108
- // each independent delivery unit. Superseded Candidate history remains
109
- // visible without keeping the Task open forever.
110
- const deliveryChangeSets = governingChangeSets(facts.workItems, facts.changeSets);
111
- for (const item of facts.workItems) {
112
- if (item.status !== "completed")
113
- continue;
114
- const candidate = item.candidates?.at(-1);
115
- const hasGitDelivery = candidate?.workspace !== undefined
116
- || candidate?.gitSnapshot !== undefined;
117
- if (hasGitDelivery
118
- && !deliveryChangeSets.some((changeSet) => changeSet.workItemId === item.id)) {
119
- blockers.push({
120
- code: "integration-evidence-missing",
121
- ref: ref("work-item", item.id),
122
- reason: `Completed WorkItem ${item.id} has an uncaptured Git delivery.`,
123
- fix: `yui task work capture ${task.id}/${item.id}`
124
- });
125
- }
126
- }
127
- for (const changeSet of deliveryChangeSets) {
128
- if (changeSetDeliverySettled(changeSet, facts.integrations, facts.integrationQueueEntries))
68
+ // Integration obligations follow the exact start/result commits of the
69
+ // Candidate that currently governs each independent delivery unit.
70
+ for (const delivery of governingWorkItemDeliveries(facts.workItems)) {
71
+ if (workItemDeliverySettled(delivery, facts.integrations))
129
72
  continue;
130
73
  blockers.push({
131
74
  code: "integration-evidence-missing",
132
- ref: ref("change-set", changeSet.id),
133
- reason: `ChangeSet ${changeSet.id} is not part of a committed Integration.`,
134
- fix: `yui task integration start ${task.id} --project ${changeSet.projectId} --change-set ${changeSet.id}`
75
+ ref: ref("work-item", delivery.workItemId),
76
+ reason: `WorkItem ${delivery.workItemId} result for Project ${delivery.projectId} is not part of a committed Integration.`,
77
+ fix: `yui task integration start ${task.id} --work-item ${delivery.workItemId} --project ${delivery.projectId} --strategy <ff|cherry-pick|merge|manual>`
135
78
  });
136
79
  }
137
80
  // Current delivery Attempts and any Attempt that may still be writing must
@@ -139,7 +82,7 @@ export function projectCompletionReadiness(facts, options = {}) {
139
82
  for (const integration of facts.integrations) {
140
83
  if (!UNRESOLVED_INTEGRATION_STATUSES.has(integration.status))
141
84
  continue;
142
- if (!integrationAttemptRequiresSettlement(integration, deliveryChangeSets))
85
+ if (!integrationAttemptRequiresSettlement(integration))
143
86
  continue;
144
87
  blockers.push({
145
88
  code: "unresolved-integration",
@@ -148,8 +91,9 @@ export function projectCompletionReadiness(facts, options = {}) {
148
91
  fix: `yui task integration continue ${task.id}/${integration.id}`
149
92
  });
150
93
  }
151
- // Unsettled integration queue entries must commit or be superseded.
152
- for (const entry of latestGoverningQueueEntries(deliveryChangeSets, facts.integrationQueueEntries)) {
94
+ // Legacy queue entries may still launch an Integration and therefore must
95
+ // settle even though ChangeSets are no longer delivery authority.
96
+ for (const entry of facts.integrationQueueEntries) {
153
97
  if (entry.status === "committed" || entry.status === "superseded")
154
98
  continue;
155
99
  blockers.push({
@@ -169,34 +113,6 @@ export function projectCompletionReadiness(facts, options = {}) {
169
113
  if (disposition?.kind === "advisory")
170
114
  advisories.push(disposition.value);
171
115
  }
172
- // Review finding ledger gate (enforce mode only).
173
- // The transactional completion path runs this gate after final-review
174
- // preparation: a pending/running Task-final Review is expected to resolve
175
- // `fixed-pending-review` findings, so the gate only blocks when no Review
176
- // is active. Mirror that timing here so the projection never blocks a
177
- // completion attempt that is about to request the resolving Review.
178
- const activeTaskReview = facts.reviewRounds.some((round) => ((round.scope ?? "work-item") === "task"
179
- && (round.status === "pending" || round.status === "running")));
180
- if (findingsGate && facts.reviewFindingLedgerMode === "enforce" && !activeTaskReview) {
181
- if (reviewFindingLedgerWriteFailedFromEvents(facts.events)) {
182
- blockers.push({
183
- code: "finding-ledger-unavailable",
184
- ref: ref("task", task.id),
185
- reason: "The Review finding ledger was unavailable while reconciling a semantic Review.",
186
- fix: "recover the ledger and reconcile the Round before completing the Task"
187
- });
188
- }
189
- for (const finding of facts.reviewFindings) {
190
- if (!isReviewFindingBlocking(finding))
191
- continue;
192
- blockers.push({
193
- code: "open-review-finding",
194
- ref: ref("review-finding", finding.id),
195
- reason: `Open ${finding.severity.toUpperCase()} finding ${finding.id} is undispositioned.`,
196
- fix: `yui task review finding dispose ${task.id}/${finding.id}`
197
- });
198
- }
199
- }
200
116
  const sorted = [...blockers].sort((left, right) => {
201
117
  const codeOrder = left.code.localeCompare(right.code);
202
118
  if (codeOrder !== 0)
@@ -271,7 +187,10 @@ function workspaceCompletionDisposition(facts, taskId, workspace) {
271
187
  }
272
188
  case "execution-lane": {
273
189
  const lane = findExecutionLane(facts, owner.executionGroupId, owner.executionLaneId);
274
- if (lane !== undefined && !TERMINAL_LANE_STATUSES.has(lane.status))
190
+ const terminal = lane !== undefined && (lane.disposition !== undefined
191
+ ? lane.disposition !== "open"
192
+ : lane.status !== undefined && TERMINAL_LANE_STATUSES.has(lane.status));
193
+ if (lane !== undefined && !terminal)
275
194
  return null;
276
195
  const value = {
277
196
  code: "execution-lane-workspace-undisposed",
@@ -1,5 +1,4 @@
1
- import { nextActionReviewOutcomeEvidence } from "./nextAction.js";
2
- import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
1
+ import { isCompletedTaskReviewEvidenceFromTurns } from "../review/reviewAcceptance.js";
3
2
  const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
4
3
  export function detectDeliveryDuplicates(facts, intent) {
5
4
  switch (intent.kind) {
@@ -84,29 +83,28 @@ function detectWorkItemDuplicates(facts, scope) {
84
83
  return duplicates;
85
84
  }
86
85
  function detectIntegrationDuplicates(facts, intent) {
87
- const wanted = new Set(intent.changeSetIds);
88
86
  const duplicates = [];
89
87
  for (const attempt of facts.integrations) {
90
88
  if (attempt.projectId !== intent.projectId)
91
89
  continue;
92
- const existing = new Set(attempt.changeSetIds);
93
- const sameSet = existing.size === wanted.size
94
- && [...wanted].every((id) => existing.has(id));
90
+ const sameSource = attempt.source.kind === "work-item"
91
+ && attempt.source.workItemId === intent.workItemId
92
+ && attempt.source.resultCommit === intent.resultCommit;
95
93
  const ref = { kind: "integration-attempt", id: attempt.id };
96
- if (sameSet && attempt.status === "committed") {
94
+ if (sameSource && attempt.status === "committed") {
97
95
  duplicates.push({
98
96
  severity: "exact",
99
- reason: `Integration ${attempt.id} already committed this exact ChangeSet set`,
97
+ reason: `Integration ${attempt.id} already committed this exact WorkItem result`,
100
98
  refs: [ref],
101
99
  reuseCommand: `yui task integration show ${facts.task.id}/${attempt.id}`
102
100
  });
103
101
  continue;
104
102
  }
105
- if (sameSet
103
+ if (sameSource
106
104
  && (attempt.status === "running" || attempt.status === "validating")) {
107
105
  duplicates.push({
108
106
  severity: "suspected",
109
- reason: `Integration ${attempt.id} is already ${attempt.status} for this exact ChangeSet set`,
107
+ reason: `Integration ${attempt.id} is already ${attempt.status} for this WorkItem result`,
110
108
  refs: [ref],
111
109
  reuseCommand: `yui task integration show ${facts.task.id}/${attempt.id}`
112
110
  });
@@ -129,7 +127,7 @@ function detectReviewDuplicates(facts, intent) {
129
127
  if (!sameCandidate)
130
128
  continue;
131
129
  const ref = { kind: "review-round", id: round.id };
132
- if (isSemanticReviewRound(round, nextActionReviewOutcomeEvidence(facts))) {
130
+ if (isCompletedTaskReviewEvidenceFromTurns(round, facts.reviewOutcomeEvidence?.turns ?? [])) {
133
131
  duplicates.push({
134
132
  severity: "exact",
135
133
  reason: `Task-final Review ${round.id} already attests this exact head`,
@@ -0,0 +1,142 @@
1
+ export function assertDraftTaskExecutionFree(store, task) {
2
+ if (task.status !== "draft") {
3
+ throw new Error(`Task is not a Draft: ${task.id}/${task.status}.`);
4
+ }
5
+ const executionFact = firstDraftExecutionFact(store, task);
6
+ if (executionFact !== undefined) {
7
+ throw new Error(`Draft Task ${task.id} has execution facts (${executionFact}); `
8
+ + "retire it and create a clean Draft instead of editing runtime history.");
9
+ }
10
+ }
11
+ export function validateDraftWorkItemEdit(store, task, candidate) {
12
+ assertDraftTaskExecutionFree(store, task);
13
+ const workItems = store.listWorkItems(task.id).map((item) => (item.id === candidate.id ? candidate : item));
14
+ const facts = draftPlanFacts(store, task, workItems);
15
+ validateWorkItemReferences(facts, candidate);
16
+ assertDraftWorkItemDependencyGraph(facts.workItems, candidate);
17
+ }
18
+ export function validateDraftTaskForActivation(store, task) {
19
+ assertDraftTaskExecutionFree(store, task);
20
+ const facts = draftPlanFacts(store, task, store.listWorkItems(task.id));
21
+ const current = facts.workItems.filter(({ status }) => status !== "retired");
22
+ for (const item of current)
23
+ validateWorkItemReferences(facts, item);
24
+ assertDraftWorkItemDependencyGraph(current);
25
+ }
26
+ /** Derive the first invalid edge in the current Draft dependency graph. */
27
+ export function draftWorkItemDependencyIssue(workItems, onlyItem) {
28
+ const current = workItems.filter(({ status }) => status !== "retired");
29
+ const byId = new Map(current.map((item) => [item.id, item]));
30
+ for (const item of onlyItem === undefined ? current : [onlyItem]) {
31
+ const dependencyId = item.dependsOn.find((id) => !byId.has(id));
32
+ if (dependencyId !== undefined) {
33
+ return { kind: "missing-or-retired", workItemId: item.id, dependencyId };
34
+ }
35
+ }
36
+ const visiting = new Set();
37
+ const visited = new Set();
38
+ const visit = (item) => {
39
+ visiting.add(item.id);
40
+ for (const dependencyId of item.dependsOn) {
41
+ if (visiting.has(dependencyId)) {
42
+ return { kind: "cycle", workItemId: item.id, dependencyId };
43
+ }
44
+ if (visited.has(dependencyId))
45
+ continue;
46
+ const dependency = byId.get(dependencyId);
47
+ if (dependency === undefined)
48
+ continue;
49
+ const issue = visit(dependency);
50
+ if (issue !== undefined)
51
+ return issue;
52
+ }
53
+ visiting.delete(item.id);
54
+ visited.add(item.id);
55
+ return undefined;
56
+ };
57
+ for (const item of current) {
58
+ if (visited.has(item.id))
59
+ continue;
60
+ const issue = visit(item);
61
+ if (issue !== undefined)
62
+ return issue;
63
+ }
64
+ return undefined;
65
+ }
66
+ function draftPlanFacts(store, task, workItems) {
67
+ const projects = new Map();
68
+ for (const binding of task.projectBindings) {
69
+ const project = store.getProject(binding.projectId);
70
+ if (project === null) {
71
+ throw new Error(`Task Project not found: ${binding.projectId}.`);
72
+ }
73
+ if (project.status !== "active") {
74
+ throw new Error(`Task Project is not active: ${project.id}/${project.status}.`);
75
+ }
76
+ projects.set(project.id, project);
77
+ }
78
+ return {
79
+ task,
80
+ workItems,
81
+ roleNames: new Set(store.listRoles(task.id).map(({ name }) => name)),
82
+ projects
83
+ };
84
+ }
85
+ function validateWorkItemReferences(facts, item) {
86
+ if (item.assignee !== undefined && !facts.roleNames.has(item.assignee)) {
87
+ throw new Error(`Work Item assignee Role not found: ${item.id}/${item.assignee}.`);
88
+ }
89
+ for (const projectId of item.writeProjectIds) {
90
+ if (!facts.projects.has(projectId)) {
91
+ throw new Error(`Work Item writable Project is not bound: ${item.id}/${projectId}.`);
92
+ }
93
+ }
94
+ for (const baseRef of item.baseRefs ?? []) {
95
+ if (!item.writeProjectIds.includes(baseRef.projectId)) {
96
+ throw new Error(`Work Item base-ref Project is not writable: ${item.id}/${baseRef.projectId}.`);
97
+ }
98
+ }
99
+ }
100
+ function assertDraftWorkItemDependencyGraph(workItems, onlyItem) {
101
+ const issue = draftWorkItemDependencyIssue(workItems, onlyItem);
102
+ if (issue === undefined)
103
+ return;
104
+ if (issue.kind === "missing-or-retired") {
105
+ throw new Error(`Work Item dependency is missing or retired: ${issue.workItemId}/${issue.dependencyId}.`);
106
+ }
107
+ throw new Error(`Work Item dependency cycle detected: ${issue.workItemId}/${issue.dependencyId}.`);
108
+ }
109
+ function firstDraftExecutionFact(store, task) {
110
+ if (task.workspaceIdentity !== undefined || task.cwd !== undefined)
111
+ return "Task workspace";
112
+ const hostOwner = store.listSessionOwners().find(({ owner }) => (owner.scope === "task" && owner.taskId === task.id));
113
+ if (hostOwner !== undefined)
114
+ return `Host owner (${hostOwner.runtimeGenerationId})`;
115
+ if (store.listTurns(task.id).length > 0)
116
+ return "Turn";
117
+ if (store.listRoleSessionSets(task.id).length > 0)
118
+ return "Role Session";
119
+ if (store.listDurableJobs(task.id).length > 0)
120
+ return "DurableJob";
121
+ if (store.listManagedWorkspaces(task.id).length > 0)
122
+ return "managed Workspace";
123
+ if (store.listReviewRounds(task.id).length > 0)
124
+ return "ReviewRound";
125
+ if (store.listIntegrationAttempts(task.id).length > 0)
126
+ return "Integration Attempt";
127
+ if (store.listChangeSets(task.id).length > 0)
128
+ return "ChangeSet";
129
+ const itemWithExecution = store.listWorkItems(task.id).find((item) => (item.executionGroups.length > 0
130
+ || item.currentExecutionGroupId !== undefined
131
+ || item.candidates.length > 0
132
+ || item.workspaceDisposition !== undefined
133
+ || !["pending", "retired"].includes(item.status)));
134
+ if (itemWithExecution !== undefined) {
135
+ return `Work Item execution (${itemWithExecution.id})`;
136
+ }
137
+ const runtimeEvent = store.listEvents(task.id).find(({ type }) => (type.startsWith("runtime.")
138
+ || type.startsWith("turn.")
139
+ || type.startsWith("review.")
140
+ || type.startsWith("integration.")));
141
+ return runtimeEvent === undefined ? undefined : `event ${runtimeEvent.id}/${runtimeEvent.type}`;
142
+ }