@zq-silk/yui 0.14.1 → 0.15.0

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 (185) hide show
  1. package/ARCHITECTURE.md +91 -47
  2. package/README.md +275 -145
  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/updateCommand.js +17 -9
  10. package/dist/cli/updateOrchestrator.js +87 -19
  11. package/dist/cli/updatePorts.js +79 -13
  12. package/dist/cli/upgradeCommand.js +105 -23
  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/controllerCommands.js +1 -1
  18. package/dist/commands/durableJobCommands.js +1 -1
  19. package/dist/commands/executionAuditCommands.js +4 -9
  20. package/dist/commands/globalRoleCommands.js +17 -0
  21. package/dist/commands/profileCommands.js +200 -30
  22. package/dist/commands/sessionCommands.js +1 -1
  23. package/dist/commands/taskActor.js +29 -127
  24. package/dist/commands/taskChangeSetCommands.js +1 -3
  25. package/dist/commands/taskCommands.js +1771 -2107
  26. package/dist/commands/taskCompletionGate.js +27 -103
  27. package/dist/commands/taskContextCommand.js +47 -80
  28. package/dist/commands/taskExecutionCommands.js +9 -27
  29. package/dist/commands/taskInputCommands.js +10 -10
  30. package/dist/commands/taskIntegrationCommands.js +69 -61
  31. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  32. package/dist/commands/taskNextActionCommand.js +8 -29
  33. package/dist/commands/taskOverviewCommand.js +25 -3
  34. package/dist/commands/taskPublicationCommands.js +151 -85
  35. package/dist/commands/taskPublicationVerifyCommand.js +181 -0
  36. package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
  37. package/dist/commands/taskRoleRuntimeStatus.js +5 -6
  38. package/dist/commands/taskUpstreamCommands.js +79 -95
  39. package/dist/config/configCatalog.js +1 -1
  40. package/dist/context/contextSnapshot.js +17 -2
  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 +6 -5
  63. package/dist/doctor/doctor.js +37 -14
  64. package/dist/domain/agentResultTransport.js +60 -0
  65. package/dist/execution/executionHealth.js +111 -118
  66. package/dist/execution/reviewMainTurn.js +161 -0
  67. package/dist/execution/workItemExecution.js +454 -0
  68. package/dist/execution/workItemExecutionProjection.js +309 -0
  69. package/dist/execution/workItemMainTurn.js +164 -0
  70. package/dist/executor/agentExecutor.js +19 -19
  71. package/dist/executor/executorRegistry.js +7 -28
  72. package/dist/executor/fileRoleLaunchPlanner.js +16 -82
  73. package/dist/external/githubPublicationVerifier.js +114 -0
  74. package/dist/external/gitlabPublicationVerifier.js +158 -0
  75. package/dist/external/pinnedCommandRunner.js +73 -0
  76. package/dist/integration/changeSetManifest.js +0 -3
  77. package/dist/integration/deliveryObligation.js +43 -6
  78. package/dist/integration/gitIntegrationService.js +284 -101
  79. package/dist/integration/integrationAttempt.js +64 -14
  80. package/dist/integration/integrationQueueEntry.js +5 -35
  81. package/dist/integration/integrationQueueService.js +65 -194
  82. package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
  83. package/dist/lifecycle/exactTurnTerminalization.js +100 -97
  84. package/dist/message/message.js +16 -0
  85. package/dist/observability/executionAudit.js +4 -25
  86. package/dist/observability/faultClassification.js +58 -70
  87. package/dist/observability/orchestrationMetrics.js +8 -50
  88. package/dist/observability/runtimeIdentity.js +48 -50
  89. package/dist/profile/agentProfile.js +60 -15
  90. package/dist/profile/agentProfileRuntime.js +100 -0
  91. package/dist/release/releaseWorkflowPorts.js +5 -68
  92. package/dist/release/runtimeRelease.js +9 -1
  93. package/dist/repository/executionLaneGitSnapshot.js +80 -0
  94. package/dist/repository/gitWorkspace.js +9 -67
  95. package/dist/repository/taskBaseFreshness.js +3 -1
  96. package/dist/repository/taskWorkspaceCoordinator.js +32 -4
  97. package/dist/repository/taskWorkspacePreparer.js +252 -411
  98. package/dist/resources/liveReferences.js +6 -55
  99. package/dist/resources/resourceDiscovery.js +1 -30
  100. package/dist/resources/resourceRegistrar.js +0 -23
  101. package/dist/review/deltaRecheck.js +8 -65
  102. package/dist/review/reviewAcceptance.js +22 -19
  103. package/dist/review/reviewConfig.js +0 -10
  104. package/dist/review/reviewDecision.js +2 -2
  105. package/dist/review/reviewRound.js +147 -273
  106. package/dist/review/reviewerAvailability.js +13 -23
  107. package/dist/role/role.js +3 -0
  108. package/dist/runtime/agentDriverObservation.js +1 -1
  109. package/dist/runtime/agentHost.js +51 -51
  110. package/dist/runtime/builtinAgentDrivers.js +9 -2
  111. package/dist/runtime/codexAppServerRuntime.js +23 -5
  112. package/dist/runtime/exactControlPlane.js +22 -296
  113. package/dist/runtime/firstProgressAdvisory.js +1 -1
  114. package/dist/runtime/index.js +1 -1
  115. package/dist/runtime/launchBroker.js +11 -11
  116. package/dist/runtime/lifecycleReservation.js +2 -2
  117. package/dist/runtime/managedCaller.js +111 -0
  118. package/dist/runtime/ports.js +16 -3
  119. package/dist/runtime/preallocatedNativeSession.js +2 -2
  120. package/dist/runtime/processExitObservation.js +3 -2
  121. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  122. package/dist/runtime/runtimeBinding.js +1 -1
  123. package/dist/runtime/runtimeObservation.js +33 -16
  124. package/dist/runtime/runtimeSessionCandidate.js +2 -2
  125. package/dist/runtime/runtimeStopReceipt.js +11 -11
  126. package/dist/runtime/sessionLaunchRequest.js +2 -2
  127. package/dist/runtime/sessionOwnerIdentity.js +9 -9
  128. package/dist/runtime/sessionOwnerRegistry.js +13 -13
  129. package/dist/runtime/sessionReconciliation.js +4 -4
  130. package/dist/runtime/sessionTerminationGuard.js +9 -9
  131. package/dist/runtime/sessionTokenMetrics.js +5 -7
  132. package/dist/runtime/structuredProviderHost.js +5 -5
  133. package/dist/runtime/taskRuntimeIsolation.js +24 -24
  134. package/dist/runtime/tmuxAdapters.js +32 -30
  135. package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
  136. package/dist/scheduler/leaderWakeupProcessor.js +78 -55
  137. package/dist/scheduler/roleTurnLiveness.js +6 -6
  138. package/dist/scheduler/roleTurnStall.js +8 -9
  139. package/dist/scheduler/taskExecutionProjection.js +19 -41
  140. package/dist/scheduler/taskObservabilityProjection.js +75 -122
  141. package/dist/storage/currentTaskStore.js +6 -4
  142. package/dist/storage/sqliteSchema.js +135 -80
  143. package/dist/storage/sqliteStore.js +69 -52
  144. package/dist/storage/storageSchema.js +92 -230
  145. package/dist/storage/storageVersions.js +12 -13
  146. package/dist/storage/taskStore.js +8 -8
  147. package/dist/storage/upgrade/upgradeOrchestrator.js +276 -85
  148. package/dist/task/completionReadiness.js +17 -98
  149. package/dist/task/deliveryGuard.js +9 -11
  150. package/dist/task/draftPlan.js +142 -0
  151. package/dist/task/nextAction.js +179 -477
  152. package/dist/task/publicationVerification.js +1 -0
  153. package/dist/task/remoteDelivery.js +233 -0
  154. package/dist/task/task.js +108 -4
  155. package/dist/task/taskRecordReference.js +0 -1
  156. package/dist/turn/turn.js +109 -16
  157. package/dist/verification/verificationGateService.js +0 -26
  158. package/dist/version.js +3 -3
  159. package/dist/web/assets/client/components.js +99 -121
  160. package/dist/web/assets/client/i18n.js +136 -58
  161. package/dist/web/assets/client/view.js +44 -0
  162. package/dist/web/assets/styles/cards.js +3 -15
  163. package/dist/web/assets/styles/widgets.js +4 -0
  164. package/dist/web/webSnapshot.js +29 -14
  165. package/dist/workItem/dependencyGate.js +34 -0
  166. package/dist/workItem/workItem.js +99 -266
  167. package/dist/workspace/workItemChangeSetManager.js +15 -59
  168. package/docs/task-local-identity.md +9 -9
  169. package/i18n/README.zh-CN.md +189 -74
  170. package/package.json +1 -1
  171. package/skills/yui-leader/SKILL.md +272 -782
  172. package/skills/yui-operator/SKILL.md +190 -365
  173. package/skills/yui-reviewer/SKILL.md +40 -33
  174. package/skills/yui-runtime/SKILL.md +22 -0
  175. package/skills/yui-worker/SKILL.md +19 -6
  176. package/dist/execution/candidateConvergence.js +0 -623
  177. package/dist/execution/executionGroup.js +0 -854
  178. package/dist/execution/resourceBroker.js +0 -426
  179. package/dist/executor/launchPlan.js +0 -16
  180. package/dist/review/reviewFinding.js +0 -240
  181. package/dist/review/reviewFindingLedger.js +0 -550
  182. package/dist/review/reviewOutcomeClassifier.js +0 -255
  183. package/dist/scheduler/resourceQueueProjection.js +0 -71
  184. package/dist/storage/upgrade/recordVersions.js +0 -82
  185. package/dist/task/repairWave.js +0 -150
@@ -1,117 +1,305 @@
1
- import { existsSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { basename, dirname, join } from "node:path";
2
+ import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
3
+ import Database from "better-sqlite3";
4
+ import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation } from "../../runtime/runtimeObservation.js";
5
+ import { RUNTIME_PROCESS_EXIT_TASK_EVENT, validateRuntimeProcessExitObservation } from "../../runtime/processExitObservation.js";
6
+ import { validateAgentProfile } from "../../profile/agentProfile.js";
7
+ import { validateRoleSessionSet } from "../../executor/agentExecutor.js";
8
+ import { validateReviewRound } from "../../review/reviewRound.js";
9
+ import { validateTurn } from "../../turn/turn.js";
10
+ import { validateWorkItem } from "../../workItem/workItem.js";
3
11
  import { SqliteTaskStore } from "../sqliteStore.js";
4
- import { inspectStorageSchema } from "../storageSchema.js";
5
- const CURRENT_DATABASE_FILENAME = "yui.db";
12
+ import { migrateSqliteSchema, storageMigrationPlan } from "../sqliteSchema.js";
13
+ import { CURRENT_DATABASE_FILENAME, inspectStorageSchema } from "../storageSchema.js";
14
+ import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../storageVersions.js";
6
15
  /**
7
- * Current-only storage admission. Historical Homes are preserved byte-for-byte
8
- * and rejected; Yui never repairs, normalizes, or upgrades them in place.
16
+ * Upgrade one valid Home through the complete linear migration chain.
17
+ *
18
+ * Supported earlier versions are readable only here. Ordinary stores still
19
+ * admit exactly {@link CURRENT_STORAGE_VERSION}; no old-shape normalizer or
20
+ * dual read path enters runtime code. Execute mode is an offline primitive:
21
+ * its caller must own the maintenance fence and keep the Controller quiesced
22
+ * for the whole backup, migration, and validation interval.
9
23
  */
10
24
  export async function runStorageUpgrade(options) {
11
- const schema = inspectStorageSchema(options.home);
12
- const classification = classifyCurrentHome(schema, options.latest);
13
- if (schema.status === "uninitialized") {
14
- return blocked(classification, "uninitialized", "Yui storage is not initialized for this Home.", "Run `yui setup` with a new Home.");
25
+ const state = inspectStorageSchema(options.home);
26
+ if (state.status === "uninitialized") {
27
+ return blocked({ ...corruptedClassification("Storage is not initialized."), uninitialized: true }, "uninitialized", "Yui storage is not initialized for this Home.", "Run `yui setup` with a new Home.");
15
28
  }
16
- if (schema.status === "invalid") {
17
- return blocked(classification, "corruption", `Storage schema is invalid: ${schema.detail}`, "Preserve this Home for diagnosis and initialize a new Home.");
29
+ if (state.status === "invalid") {
30
+ return blocked(corruptedClassification(state.detail), "corruption", `Storage is invalid: ${state.detail}`, "Preserve this Home for diagnosis and restore it from a known-good backup.");
18
31
  }
19
- if (schema.status === "unsupported") {
20
- return blocked(classification, "unsupported", `Storage contract is ${schema.direction}: ${schema.incompatibleComponent}.`, "Open the old Home only with its original Yui version to export a summary; then let the new Operator create a new Task in a new Home.");
32
+ if (state.status === "unsupported") {
33
+ const classification = unsupportedClassification(state);
34
+ return blocked(classification, "unsupported", classification.classification.verdict === "NEEDS_NEW_VERSION"
35
+ ? classification.classification.blocker.message
36
+ : "Storage is unsupported.", classification.classification.verdict === "NEEDS_NEW_VERSION"
37
+ ? classification.classification.blocker.action
38
+ : "Use a compatible Yui release.");
21
39
  }
22
- if (!existsSync(join(options.home, CURRENT_DATABASE_FILENAME))) {
23
- return blocked(corruptedClassification(classification, "The current SQLite database is missing."), "corruption", "The current SQLite Home is incomplete: yui.db is missing.", "Preserve this Home for diagnosis and initialize a new Home.");
24
- }
25
- try {
26
- const store = new SqliteTaskStore(options.home);
40
+ if (state.status === "current") {
27
41
  try {
28
- store.getConfig();
42
+ validateCurrentStore(options.home);
29
43
  }
30
- finally {
31
- store.close();
44
+ catch (error) {
45
+ return blocked(corruptedClassification(messageOf(error)), "corruption", `Current storage validation failed: ${messageOf(error)}`, "Preserve this Home for diagnosis and restore it from a known-good backup.");
46
+ }
47
+ const classification = currentClassification();
48
+ if (options.mode === "update-preflight") {
49
+ return {
50
+ outcome: "update-preflight",
51
+ status: "already-current",
52
+ stepCount: 0,
53
+ steps: [],
54
+ classification
55
+ };
32
56
  }
57
+ return {
58
+ outcome: "already-current",
59
+ classification,
60
+ report: {
61
+ outcome: "already-current",
62
+ mode: options.mode,
63
+ sourceVersion: CURRENT_STORAGE_VERSION,
64
+ targetVersion: CURRENT_STORAGE_VERSION,
65
+ steps: []
66
+ }
67
+ };
33
68
  }
34
- catch (error) {
35
- const message = error instanceof Error ? error.message : String(error);
36
- return blocked(corruptedClassification(classification, message), "corruption", `Current storage validation failed: ${message}`, "Preserve this Home for diagnosis and initialize a new Home.");
69
+ const plan = storageMigrationPlan(state.currentVersion);
70
+ if (plan === null) {
71
+ return blocked(corruptedClassification(`No complete migration path exists from ${state.currentVersion} `
72
+ + `to ${CURRENT_STORAGE_VERSION}.`), "corruption", "The storage migration registry is incomplete.", "Install a Yui release that carries the complete migration chain.");
37
73
  }
74
+ const classification = migratableClassification(state.currentVersion);
38
75
  if (options.mode === "update-preflight") {
39
76
  return {
40
77
  outcome: "update-preflight",
41
- status: "already-current",
42
- stepCount: 0,
78
+ status: "migration-ready",
79
+ stepCount: plan.length,
80
+ steps: plan,
43
81
  classification
44
82
  };
45
83
  }
46
- const report = {
47
- outcome: "already-current",
48
- mode: options.mode,
49
- source: options.latest,
50
- target: options.latest
51
- };
52
- return { outcome: "already-current", classification, report };
53
- }
54
- function classifyCurrentHome(schema, latest) {
55
- const base = {
56
- latestLayoutVersion: latest.layout,
57
- latestAggregateVersion: latest.aggregate
58
- };
59
- if (schema.status === "uninitialized") {
84
+ if (options.mode === "dry-run") {
60
85
  return {
61
- ...base,
62
- classification: { verdict: "USABLE", status: "current" },
63
- uninitialized: true
86
+ outcome: "upgrade-plan",
87
+ classification,
88
+ report: {
89
+ outcome: "upgrade-plan",
90
+ mode: "dry-run",
91
+ sourceVersion: state.currentVersion,
92
+ targetVersion: CURRENT_STORAGE_VERSION,
93
+ steps: plan
94
+ }
64
95
  };
65
96
  }
66
- if (schema.status === "invalid") {
67
- return corruptedClassification({
68
- ...base,
69
- classification: { verdict: "CORRUPTED", status: "unsupported", detail: schema.detail }
70
- }, schema.detail);
71
- }
72
- if (schema.status === "unsupported") {
73
- const blocker = schema.direction === "newer"
74
- ? {
75
- reason: "future-version",
76
- axis: schema.incompatibleComponent,
77
- ...(schema.recordFamily === undefined ? {} : { recordKind: schema.recordFamily }),
78
- found: schema.currentVersion,
79
- supported: schema.latestVersion,
80
- message: "Historical storage contracts are not supported by this release.",
81
- action: "Initialize a new Home."
82
- }
83
- : {
84
- reason: "missing-step",
85
- axis: schema.incompatibleComponent,
86
- ...(schema.recordFamily === undefined ? {} : { recordKind: schema.recordFamily }),
87
- from: schema.currentVersion,
88
- to: schema.latestVersion,
89
- message: "Historical storage contracts are not supported by this release.",
90
- action: "Initialize a new Home."
91
- };
97
+ let backupPath;
98
+ try {
99
+ backupPath = await createDatabaseBackup(options.home, state.currentVersion, options.now ?? new Date());
100
+ }
101
+ catch (error) {
102
+ return {
103
+ outcome: "failed",
104
+ stage: "backup",
105
+ message: `Storage backup failed: ${messageOf(error)}`,
106
+ action: "Storage was not modified. Resolve the backup path, permissions, or free-space problem "
107
+ + "and rerun `yui upgrade`.",
108
+ classification,
109
+ sceneUnchanged: true
110
+ };
111
+ }
112
+ let migrationCommitted = false;
113
+ try {
114
+ const database = new Database(join(options.home, CURRENT_DATABASE_FILENAME));
115
+ try {
116
+ database.pragma("journal_mode = WAL");
117
+ database.pragma("synchronous = FULL");
118
+ database.pragma("foreign_keys = ON");
119
+ database.pragma("busy_timeout = 5000");
120
+ migrateSqliteSchema(database, { mode: "apply" });
121
+ migrationCommitted = true;
122
+ }
123
+ finally {
124
+ database.close();
125
+ }
126
+ validateCurrentStore(options.home);
127
+ rmSync(join(options.home, "schema.json"), { force: true });
128
+ }
129
+ catch (error) {
130
+ const restoration = migrationCommitted
131
+ ? tryRestoreDatabaseBackup(options.home, backupPath)
132
+ : { restored: true };
92
133
  return {
93
- ...base,
94
- classification: {
95
- verdict: "NEEDS_NEW_VERSION",
96
- status: "unsupported",
97
- blocker
98
- },
99
- layoutVersion: schema.currentLayoutVersion,
100
- aggregateVersion: schema.currentAggregateSchemaVersion,
101
- incompatibleComponent: schema.incompatibleComponent
134
+ outcome: "failed",
135
+ stage: "migration",
136
+ message: `Storage migration failed: ${messageOf(error)}`,
137
+ action: restoration.restored
138
+ ? "The original database was restored from the timestamped backup. "
139
+ + "Resolve the reported problem and rerun `yui upgrade`."
140
+ : "Automatic restore also failed. Keep the Home quiesced and restore "
141
+ + `${backupPath} manually before retrying. Restore error: ${restoration.error}`,
142
+ backupPath,
143
+ classification,
144
+ sceneUnchanged: restoration.restored
102
145
  };
103
146
  }
147
+ const finalState = inspectStorageSchema(options.home);
148
+ if (finalState.status !== "current") {
149
+ const restoration = tryRestoreDatabaseBackup(options.home, backupPath);
150
+ return {
151
+ outcome: "failed",
152
+ stage: "migration",
153
+ message: `Storage migration did not reach version ${CURRENT_STORAGE_VERSION}.`,
154
+ action: restoration.restored
155
+ ? "The original database was restored from the timestamped backup. "
156
+ + "Inspect the migration registry before retrying."
157
+ : "Automatic restore also failed. Keep the Home quiesced and restore "
158
+ + `${backupPath} manually before retrying. Restore error: ${restoration.error}`,
159
+ backupPath,
160
+ classification,
161
+ sceneUnchanged: restoration.restored
162
+ };
163
+ }
164
+ return {
165
+ outcome: "upgraded",
166
+ classification: currentClassification(),
167
+ report: {
168
+ outcome: "upgraded",
169
+ mode: "execute",
170
+ sourceVersion: state.currentVersion,
171
+ targetVersion: CURRENT_STORAGE_VERSION,
172
+ steps: plan,
173
+ backupPath
174
+ }
175
+ };
176
+ }
177
+ function validateCurrentStore(home) {
178
+ const store = new SqliteTaskStore(home);
179
+ try {
180
+ store.getConfig();
181
+ for (const profile of store.listAgentProfiles())
182
+ validateAgentProfile(profile);
183
+ for (const sessions of store.listGlobalRoleSessionSets()) {
184
+ validateRoleSessionSet(sessions);
185
+ }
186
+ for (const owner of store.listSessionOwners()) {
187
+ if (owner.schemaVersion !== 2
188
+ || typeof owner.runtimeGenerationId !== "string"
189
+ || Object.hasOwn(owner, "launchId")) {
190
+ throw new Error("Session owner runtime generation identity is invalid.");
191
+ }
192
+ }
193
+ for (const taskId of store.listTasks().map(({ id }) => id)) {
194
+ for (const item of store.listWorkItems(taskId))
195
+ validateWorkItem(item);
196
+ for (const round of store.listReviewRounds(taskId))
197
+ validateReviewRound(round);
198
+ for (const turn of store.listTurns(taskId))
199
+ validateTurn(turn);
200
+ for (const sessions of store.listRoleSessionSets(taskId)) {
201
+ validateRoleSessionSet(sessions);
202
+ }
203
+ for (const event of store.listEvents(taskId)) {
204
+ if (event.type === RUNTIME_OBSERVATION_TASK_EVENT) {
205
+ createRuntimeObservation(JSON.parse(event.payload.observation ?? ""));
206
+ }
207
+ else if (event.type === RUNTIME_PROCESS_EXIT_TASK_EVENT) {
208
+ validateRuntimeProcessExitObservation(JSON.parse(event.payload.observation ?? ""));
209
+ }
210
+ }
211
+ }
212
+ const quickCheck = store.databaseHandle().pragma("quick_check", { simple: true });
213
+ if (quickCheck !== "ok") {
214
+ throw new Error(`SQLite quick_check failed: ${String(quickCheck)}.`);
215
+ }
216
+ }
217
+ finally {
218
+ store.close();
219
+ }
220
+ }
221
+ async function createDatabaseBackup(home, sourceVersion, now) {
222
+ const source = join(home, CURRENT_DATABASE_FILENAME);
223
+ if (!existsSync(source))
224
+ throw new Error("The authoritative yui.db is missing.");
225
+ const backupDirectory = join(dirname(home), `${basename(home)}-backups`);
226
+ mkdirSync(backupDirectory, { recursive: true, mode: 0o700 });
227
+ const timestamp = now.toISOString().replaceAll(":", "-");
228
+ const backupPath = join(backupDirectory, `pre-storage-v${sourceVersion}-${timestamp}.db`);
229
+ const database = new Database(source, { readonly: true, fileMustExist: true });
230
+ try {
231
+ await database.backup(backupPath);
232
+ }
233
+ finally {
234
+ database.close();
235
+ }
236
+ return backupPath;
237
+ }
238
+ function restoreDatabaseBackup(home, backupPath) {
239
+ const databasePath = join(home, CURRENT_DATABASE_FILENAME);
240
+ rmSync(`${databasePath}-wal`, { force: true });
241
+ rmSync(`${databasePath}-shm`, { force: true });
242
+ copyFileSync(backupPath, databasePath);
243
+ }
244
+ function tryRestoreDatabaseBackup(home, backupPath) {
245
+ try {
246
+ restoreDatabaseBackup(home, backupPath);
247
+ return { restored: true };
248
+ }
249
+ catch (error) {
250
+ return { restored: false, error: messageOf(error) };
251
+ }
252
+ }
253
+ function currentClassification() {
104
254
  return {
105
- ...base,
106
255
  classification: { verdict: "USABLE", status: "current" },
107
- layoutVersion: schema.currentLayoutVersion,
108
- aggregateVersion: schema.currentAggregateSchemaVersion
256
+ storageVersion: CURRENT_STORAGE_VERSION,
257
+ currentStorageVersion: CURRENT_STORAGE_VERSION,
258
+ minimumSupportedStorageVersion: MIN_SUPPORTED_STORAGE_VERSION
109
259
  };
110
260
  }
111
- function corruptedClassification(classification, detail) {
261
+ function migratableClassification(storageVersion) {
112
262
  return {
113
- ...classification,
114
- classification: { verdict: "CORRUPTED", status: "unsupported", detail }
263
+ classification: { verdict: "MIGRATABLE", status: "migration-ready" },
264
+ storageVersion,
265
+ currentStorageVersion: CURRENT_STORAGE_VERSION,
266
+ minimumSupportedStorageVersion: MIN_SUPPORTED_STORAGE_VERSION
267
+ };
268
+ }
269
+ function unsupportedClassification(state) {
270
+ const future = state.direction === "newer";
271
+ const message = future
272
+ ? `Storage version ${state.currentVersion} is newer than this CLI supports `
273
+ + `(${CURRENT_STORAGE_VERSION}).`
274
+ : `Storage version ${state.currentVersion} is older than the minimum supported `
275
+ + `migration version ${MIN_SUPPORTED_STORAGE_VERSION}.`;
276
+ const action = future
277
+ ? "Use a newer Yui release."
278
+ : "Preserve this Home for use with its matching historical Yui release, "
279
+ + "or initialize a new Home with Yui 0.15.0 or later.";
280
+ return {
281
+ classification: {
282
+ verdict: "NEEDS_NEW_VERSION",
283
+ status: "unsupported",
284
+ blocker: {
285
+ reason: future ? "future-version" : "below-minimum",
286
+ found: state.currentVersion,
287
+ current: CURRENT_STORAGE_VERSION,
288
+ minimum: MIN_SUPPORTED_STORAGE_VERSION,
289
+ message,
290
+ action
291
+ }
292
+ },
293
+ storageVersion: state.currentVersion,
294
+ currentStorageVersion: CURRENT_STORAGE_VERSION,
295
+ minimumSupportedStorageVersion: MIN_SUPPORTED_STORAGE_VERSION
296
+ };
297
+ }
298
+ function corruptedClassification(detail) {
299
+ return {
300
+ classification: { verdict: "CORRUPTED", status: "unsupported", detail },
301
+ currentStorageVersion: CURRENT_STORAGE_VERSION,
302
+ minimumSupportedStorageVersion: MIN_SUPPORTED_STORAGE_VERSION
115
303
  };
116
304
  }
117
305
  function blocked(classification, stage, message, action) {
@@ -124,3 +312,6 @@ function blocked(classification, stage, message, action) {
124
312
  sceneUnchanged: true
125
313
  };
126
314
  }
315
+ function messageOf(error) {
316
+ return error instanceof Error ? error.message : String(error);
317
+ }
@@ -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`,