@zq-silk/yui 0.15.11 → 0.16.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 (280) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/ARCHITECTURE.zh-CN.md +8 -4
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/artifacts/artifactCommitLock.js +16 -55
  6. package/dist/artifacts/taskArtifactRepository.js +3 -3
  7. package/dist/cli/agentConfigurationPicker.js +1 -1
  8. package/dist/cli/commandCatalog.js +92 -48
  9. package/dist/cli/completion.js +18 -18
  10. package/dist/cli/completionWizard.js +1 -1
  11. package/dist/cli/dynamicCompletion.js +1 -1
  12. package/dist/cli/interactionPolicy.js +11 -3
  13. package/dist/cli/invocationAuthority.js +64 -0
  14. package/dist/cli/managedDiagnostics.js +2 -2
  15. package/dist/cli/parseRepeatable.js +35 -0
  16. package/dist/cli/updatePorts.js +26 -5
  17. package/dist/cli.js +1303 -1235
  18. package/dist/commands/agentCommands.js +4 -4
  19. package/dist/commands/capabilityCommands.js +1 -1
  20. package/dist/commands/configCommands.js +6 -57
  21. package/dist/commands/configOverview.js +2 -2
  22. package/dist/commands/durableJobCommands.js +12 -6
  23. package/dist/commands/executionAuditCommands.js +10 -0
  24. package/dist/commands/globalRoleCommands.js +78 -29
  25. package/dist/commands/grantCommands.js +0 -3
  26. package/dist/commands/jobCommands.js +1 -4
  27. package/dist/commands/operatorCommands.js +3 -3
  28. package/dist/commands/projectCommands.js +45 -25
  29. package/dist/commands/resourcesCommands.js +10 -40
  30. package/dist/commands/roleConfiguration.js +2 -6
  31. package/dist/commands/taskActivationCommands.js +4 -8
  32. package/dist/commands/taskAgentCapabilities.js +9 -0
  33. package/dist/commands/taskCommandSupport.js +155 -0
  34. package/dist/commands/taskCommandTypes.js +1 -0
  35. package/dist/commands/taskCommands.js +355 -732
  36. package/dist/commands/taskCompletionGate.js +1 -1
  37. package/dist/commands/taskExecutionCommands.js +1 -1
  38. package/dist/commands/taskFactCommands.js +325 -0
  39. package/dist/commands/taskInputCommands.js +12 -42
  40. package/dist/commands/taskIntegrationCommands.js +20 -58
  41. package/dist/commands/taskNextActionCommand.js +7 -6
  42. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  43. package/dist/commands/taskPublicationCommands.js +12 -3
  44. package/dist/commands/taskPublicationVerifyCommand.js +24 -40
  45. package/dist/commands/taskRemoteDeliveryCommand.js +5 -86
  46. package/dist/commands/taskUpstreamCommands.js +19 -8
  47. package/dist/commands/workflowCommands.js +6 -1
  48. package/dist/completion/completionInstaller.js +26 -26
  49. package/dist/completion/completionState.js +26 -26
  50. package/dist/completion/fileCompletionManager.js +6 -11
  51. package/dist/config/configCatalog.js +0 -2
  52. package/dist/config/timeZone.js +14 -0
  53. package/dist/config/yuiConfig.js +0 -29
  54. package/dist/context/runContextPack.js +5 -3
  55. package/dist/context/taskCatalog.js +185 -0
  56. package/dist/context/taskContext.js +152 -151
  57. package/dist/context/wakeRunReferences.js +11 -0
  58. package/dist/controller/agentCapabilities.js +58 -0
  59. package/dist/controller/agentRuntimeObserver.js +6 -11
  60. package/dist/controller/clientRuntime.js +22 -14
  61. package/dist/controller/controller.js +20 -24
  62. package/dist/controller/fileSchedulerStoreAdapter.js +191 -225
  63. package/dist/controller/globalInputDelivery.js +13 -0
  64. package/dist/controller/jobClient.js +4 -4
  65. package/dist/controller/jobControl.js +60 -25
  66. package/dist/controller/jobSupervisor.js +4 -4
  67. package/dist/controller/providerRetryAdmission.js +100 -0
  68. package/dist/controller/providerRetryDelivery.js +218 -0
  69. package/dist/controller/runtime.js +85 -43
  70. package/dist/controller/runtimeEventProcessor.js +0 -5
  71. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  72. package/dist/controller/taskAgentError.js +35 -0
  73. package/dist/coordination/workMailboxQueue.js +2 -24
  74. package/dist/core/controllerClient.js +2 -2
  75. package/dist/core/fileLockOwner.js +74 -0
  76. package/dist/decision/decision.js +17 -0
  77. package/dist/doctor/doctor.js +18 -9
  78. package/dist/event/taskEvent.js +12 -0
  79. package/dist/execution/workItemExecutionProjection.js +2 -2
  80. package/dist/executor/agentCapabilityConfig.js +43 -0
  81. package/dist/executor/agentConfigurationCatalog.js +49 -11
  82. package/dist/executor/agentExecutor.js +5 -8
  83. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  84. package/dist/executor/taskAgentCapabilities.js +64 -0
  85. package/dist/integration/deliveryObligation.js +1 -69
  86. package/dist/integration/gitIntegrationService.js +79 -90
  87. package/dist/integration/integrationAttempt.js +3 -14
  88. package/dist/integration/integrationSourceApplication.js +4 -12
  89. package/dist/integration/manifestTags.js +2 -2
  90. package/dist/job/durableJob.js +4 -25
  91. package/dist/job/jobAssignmentScope.js +22 -0
  92. package/dist/job/jobOperation.js +16 -0
  93. package/dist/job/jobRunner.js +2 -1
  94. package/dist/job/stepDirectory.js +15 -0
  95. package/dist/kernel/builtinCapabilities.js +15 -11
  96. package/dist/kernel/kernelPorts.js +1 -17
  97. package/dist/lifecycle/exactRunTerminalization.js +5 -10
  98. package/dist/message/globalProviderRetry.js +15 -0
  99. package/dist/message/inputControlResolution.js +3 -4
  100. package/dist/message/message.js +17 -20
  101. package/dist/message/messageContinuation.js +1 -1
  102. package/dist/milestone/milestone.js +11 -0
  103. package/dist/observability/executionAudit.js +19 -0
  104. package/dist/observability/orchestrationMetrics.js +24 -1
  105. package/dist/observability/runtimeIdentity.js +1 -17
  106. package/dist/output/agentConfigurationPresentation.js +2 -0
  107. package/dist/output/timePresentation.js +1 -14
  108. package/dist/plugins/pluginService.js +12 -4
  109. package/dist/release/releaseWorkflowPorts.js +45 -96
  110. package/dist/release/runtimeRelease.js +9 -15
  111. package/dist/repository/gitWorkspace.js +371 -105
  112. package/dist/repository/projectMaintenanceLock.js +98 -82
  113. package/dist/repository/taskBaseFreshness.js +1 -1
  114. package/dist/repository/taskWorkspaceCoordinator.js +82 -144
  115. package/dist/repository/taskWorkspacePreparer.js +113 -47
  116. package/dist/repository/workspaceCleanupInspection.js +187 -0
  117. package/dist/resources/autoResourceGc.js +4 -77
  118. package/dist/resources/liveReferences.js +7 -54
  119. package/dist/resources/resourceDiscovery.js +5 -118
  120. package/dist/resources/resourceGc.js +233 -181
  121. package/dist/resources/resourceRegistrar.js +5 -4
  122. package/dist/resources/resourceRegistry.js +3 -37
  123. package/dist/resources/resourceRegistryStore.js +13 -36
  124. package/dist/resources/sqliteResourceRegistry.js +43 -26
  125. package/dist/review/deltaRecheck.js +1 -1
  126. package/dist/review/reviewAcceptance.js +1 -1
  127. package/dist/review/reviewDecision.js +1 -1
  128. package/dist/review/reviewRound.js +8 -7
  129. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  130. package/dist/runtime/acpProtocol.js +4 -51
  131. package/dist/runtime/acpSession.js +10 -75
  132. package/dist/runtime/acpSessionConfiguration.js +1 -8
  133. package/dist/runtime/agentEndpoint.js +3 -2
  134. package/dist/runtime/agentError.js +5 -3
  135. package/dist/runtime/agentFailureContext.js +43 -0
  136. package/dist/runtime/agentHost.js +42 -25
  137. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  138. package/dist/runtime/codexAppServerRuntime.js +34 -3
  139. package/dist/runtime/codexInteractiveHost.js +2 -2
  140. package/dist/runtime/index.js +0 -1
  141. package/dist/runtime/managedCaller.js +21 -1
  142. package/dist/runtime/providerControl.js +5 -1
  143. package/dist/runtime/providerErrors.js +38 -0
  144. package/dist/runtime/providerRetry.js +198 -0
  145. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  146. package/dist/runtime/runtimeObservation.js +0 -5
  147. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  148. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  149. package/dist/runtime/sessionTokenMetrics.js +15 -5
  150. package/dist/runtime/structuredProviderHost.js +10 -43
  151. package/dist/runtime/taskUsageMetrics.js +275 -0
  152. package/dist/runtime/tmuxAdapters.js +6 -8
  153. package/dist/scheduler/activeRoleRunDelivery.js +19 -7
  154. package/dist/scheduler/leaderWakeupProcessor.js +24 -6
  155. package/dist/scheduler/operatorEvent.js +12 -20
  156. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  157. package/dist/scheduler/ports.js +5 -8
  158. package/dist/scheduler/roleRunLiveness.js +4 -4
  159. package/dist/scheduler/roleRunStall.js +16 -23
  160. package/dist/scheduler/taskExecutionProjection.js +49 -36
  161. package/dist/scheduler/taskObservabilityProjection.js +6 -45
  162. package/dist/scheduler/taskWake.js +5 -10
  163. package/dist/scheduler/wakeReason.js +2 -0
  164. package/dist/scheduler/wakeupQueue.js +2 -4
  165. package/dist/setup/setupCommand.js +1 -1
  166. package/dist/storage/contextRecords.js +106 -0
  167. package/dist/storage/currentTaskStore.js +1 -1
  168. package/dist/storage/homeLayout.js +13 -17
  169. package/dist/storage/migrations/agentFailureContext.js +22 -0
  170. package/dist/storage/migrations/currentInputContract.js +86 -0
  171. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  172. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  173. package/dist/storage/migrations/integrationContinuation.js +5 -4
  174. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  175. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  176. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  177. package/dist/storage/migrations/verificationPolicy.js +74 -0
  178. package/dist/storage/migrations/workItemHistory.js +46 -0
  179. package/dist/storage/persistenceWorker.js +12 -4
  180. package/dist/storage/recordValidation.js +87 -0
  181. package/dist/storage/sqliteSchema.js +173 -1
  182. package/dist/storage/sqliteStore.js +172 -161
  183. package/dist/storage/storageSchema.js +2 -14
  184. package/dist/storage/storageVersions.js +1 -1
  185. package/dist/storage/storeRpc.js +16 -7
  186. package/dist/storage/taskCatalog.js +123 -0
  187. package/dist/storage/taskStore.js +5 -7
  188. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  189. package/dist/task/archiveDiagnostics.js +1 -0
  190. package/dist/task/archivePreflight.js +124 -0
  191. package/dist/task/completionReadiness.js +5 -24
  192. package/dist/task/draftPlan.js +0 -53
  193. package/dist/task/nextAction.js +7 -13
  194. package/dist/task/publicationAdoption.js +56 -0
  195. package/dist/task/publicationReference.js +10 -0
  196. package/dist/task/remoteDelivery.js +31 -16
  197. package/dist/task/remoteDeliveryService.js +89 -0
  198. package/dist/task/taskActivation.js +2 -25
  199. package/dist/task/taskActivationService.js +0 -2
  200. package/dist/task/taskRecordReference.js +0 -1
  201. package/dist/task/taskSubmission.js +3 -14
  202. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  203. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  204. package/dist/telemetry/telemetryWiring.js +4 -3
  205. package/dist/tmux/tmuxManager.js +29 -20
  206. package/dist/verification/gateArtifact.js +15 -24
  207. package/dist/verification/gateArtifactStore.js +14 -7
  208. package/dist/verification/verificationGateService.js +29 -88
  209. package/dist/verification/verificationPlan.js +27 -72
  210. package/dist/web/assets/client/app.js +92 -17
  211. package/dist/web/assets/client/components.js +55 -13
  212. package/dist/web/assets/client/i18n.js +72 -4
  213. package/dist/web/assets/client/taskSurface.js +6 -5
  214. package/dist/web/assets/client/view.js +35 -7
  215. package/dist/web/assets/shell.js +6 -0
  216. package/dist/web/assets/styles/layout.js +7 -0
  217. package/dist/web/webServer.js +12 -3
  218. package/dist/web/webSnapshot.js +14 -85
  219. package/dist/web/webTaskSurface.js +17 -46
  220. package/dist/workItem/workItem.js +1 -12
  221. package/dist/workspace/cleanupInspection.js +63 -0
  222. package/dist/workspace/workItemChangeSetManager.js +110 -50
  223. package/docs/agent-result-consumption.md +4 -0
  224. package/docs/agent-result-consumption.zh-CN.md +3 -0
  225. package/docs/agent-runtime-drivers.md +7 -0
  226. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  227. package/docs/architecture/README.md +2 -0
  228. package/docs/architecture/README.zh-CN.md +3 -1
  229. package/docs/architecture/capabilities-and-resources.md +42 -5
  230. package/docs/architecture/capabilities-and-resources.zh-CN.md +33 -3
  231. package/docs/managed-turn-and-session-runtime.md +121 -1
  232. package/docs/managed-turn-and-session-runtime.zh-CN.md +96 -2
  233. package/docs/observability/README.md +62 -0
  234. package/docs/observability/README.zh-CN.md +47 -0
  235. package/docs/plugin-sdk.md +4 -2
  236. package/docs/project-refresh.md +77 -0
  237. package/docs/project-refresh.zh-CN.md +59 -0
  238. package/docs/provider-retry.md +70 -0
  239. package/docs/provider-runtime.md +3 -1
  240. package/docs/provider-runtime.zh-CN.md +4 -2
  241. package/docs/release-workflow.md +190 -7
  242. package/docs/release-workflow.zh-CN.md +139 -5
  243. package/docs/roles-and-configuration.md +29 -0
  244. package/docs/roles-and-configuration.zh-CN.md +21 -0
  245. package/docs/sqlite-control-plane-design.md +16 -3
  246. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  247. package/docs/task-dag-semantics.md +3 -3
  248. package/docs/task-dag-semantics.zh-CN.md +2 -2
  249. package/docs/task-delivery.md +223 -17
  250. package/docs/task-delivery.zh-CN.md +171 -14
  251. package/docs/task-discovery.md +101 -0
  252. package/docs/task-discovery.zh-CN.md +85 -0
  253. package/docs/testing/verification-levels.md +161 -9
  254. package/docs/testing/verification-levels.zh-CN.md +116 -9
  255. package/i18n/README.zh-CN.md +13 -7
  256. package/package.json +1 -1
  257. package/skills/yui-leader/SKILL.md +5 -0
  258. package/skills/yui-leader/references/execution.md +3 -2
  259. package/skills/yui-leader/references/integration.md +9 -5
  260. package/skills/yui-leader/references/planning.md +9 -3
  261. package/skills/yui-operator/SKILL.md +24 -8
  262. package/skills/yui-reviewer/SKILL.md +4 -0
  263. package/skills/yui-runtime/SKILL.md +17 -2
  264. package/skills/yui-runtime/references/publication.md +26 -4
  265. package/skills/yui-runtime/references/recovery.md +71 -0
  266. package/dist/agent/agentRegistry.js +0 -10
  267. package/dist/agentRun/runIdentity.js +0 -22
  268. package/dist/commands/deliveryGuardPreflight.js +0 -30
  269. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  270. package/dist/commands/taskOverviewCommand.js +0 -376
  271. package/dist/completion/completionWizard.js +0 -125
  272. package/dist/context/dispatchContext.js +0 -110
  273. package/dist/context/wakeNotification.js +0 -144
  274. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  275. package/dist/integration/integrationQueueEntry.js +0 -191
  276. package/dist/integration/integrationQueueService.js +0 -810
  277. package/dist/release/fakeReleasePorts.js +0 -55
  278. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  279. package/dist/task/deliveryGuard.js +0 -226
  280. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -1,15 +1,17 @@
1
+ import Database from "better-sqlite3";
1
2
  import { existsSync } from "node:fs";
2
3
  import { join } from "node:path";
3
- import Database from "better-sqlite3";
4
- import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
5
- import { migrateSqliteSchema } from "../storage/sqliteSchema.js";
6
4
  import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../storage/currentTaskStore.js";
5
+ import { migrateSqliteSchema } from "../storage/sqliteSchema.js";
6
+ import { AsyncTaskStoreClient } from "../storage/storeRpc.js";
7
+ import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
7
8
  /**
8
9
  * Default sidecar implementation: the telemetry tables live inside the Home's
9
10
  * authoritative `yui.db`, so a Home has one durable database
10
11
  * has one file to manage, back up, and migrate. The store opens its own
11
- * connection to that file: the write path is a single serialized writer (the
12
- * "WAL worker") and stays isolated from the business store's connection.
12
+ * connection for cold synchronous reads/explicit retention. Ingestion writes
13
+ * use the existing persistence-worker RPC, shared with the Controller when
14
+ * available; standalone consumers lazily own and close their own client.
13
15
  *
14
16
  * Writes are best-effort and never block the Controller event loop: `observe`
15
17
  * only merges into a bounded in-memory queue; a background flush drains it in
@@ -28,6 +30,10 @@ export class SqliteTelemetryStore {
28
30
  #terminalKeep;
29
31
  #turnCap;
30
32
  #maxPending;
33
+ #home;
34
+ #writer;
35
+ #ownedWriter;
36
+ #flushing;
31
37
  #db = null;
32
38
  #failed = false;
33
39
  #lastError = null;
@@ -39,6 +45,8 @@ export class SqliteTelemetryStore {
39
45
  #closed = false;
40
46
  constructor(home, options = {}) {
41
47
  this.mode = options.mode ?? "on";
48
+ this.#home = home;
49
+ this.#writer = options.writer;
42
50
  this.#path = join(home, COMMITTED_DATABASE_FILENAME);
43
51
  this.#terminalKeep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
44
52
  this.#turnCap = options.runCap ?? DEFAULT_RUN_CAP;
@@ -48,8 +56,7 @@ export class SqliteTelemetryStore {
48
56
  observe(entry) {
49
57
  if (this.#closed)
50
58
  return;
51
- const db = this.#ensureDb();
52
- if (db === null) {
59
+ if (this.#failed) {
53
60
  this.#dropped++;
54
61
  return;
55
62
  }
@@ -84,9 +91,14 @@ export class SqliteTelemetryStore {
84
91
  if (this.#closed)
85
92
  return;
86
93
  this.#closed = true;
87
- await this.#flushPending();
88
- this.#db?.close();
89
- this.#db = null;
94
+ try {
95
+ await this.#flushPending();
96
+ }
97
+ finally {
98
+ await this.#ownedWriter?.close();
99
+ this.#db?.close();
100
+ this.#db = null;
101
+ }
90
102
  }
91
103
  // -- TelemetryReader -----------------------------------------------------------
92
104
  count(taskId, runId) {
@@ -230,6 +242,7 @@ export class SqliteTelemetryStore {
230
242
  return this.#db;
231
243
  if (this.#failed || this.#closed)
232
244
  return null;
245
+ let opening;
233
246
  try {
234
247
  // The telemetry tables live in the Home's authoritative database; the
235
248
  // store never creates `yui.db` itself. Wiring fails closed on Homes
@@ -237,17 +250,19 @@ export class SqliteTelemetryStore {
237
250
  if (!existsSync(this.#path)) {
238
251
  throw new Error(`Telemetry database not found: ${this.#path}`);
239
252
  }
240
- const db = new Database(this.#path);
253
+ const db = opening = new Database(this.#path);
241
254
  db.pragma("journal_mode = WAL");
242
255
  db.pragma("synchronous = FULL");
243
256
  db.pragma("foreign_keys = ON");
244
- db.pragma("busy_timeout = 5000");
257
+ // Cold diagnostic reads/retention must not wait behind a semantic writer.
258
+ db.pragma("busy_timeout = 0");
245
259
  db.pragma("wal_autocheckpoint = 1000");
246
260
  migrateSqliteSchema(db, { mode: "validate" });
247
261
  this.#db = db;
248
262
  return db;
249
263
  }
250
264
  catch (error) {
265
+ opening?.close();
251
266
  this.#failed = true;
252
267
  this.#lastError = error instanceof Error ? error.message : String(error);
253
268
  return null;
@@ -263,43 +278,28 @@ export class SqliteTelemetryStore {
263
278
  });
264
279
  handle.unref?.();
265
280
  }
266
- async #flushPending() {
267
- if (this.#pending.size === 0)
268
- return;
269
- const db = this.#ensureDb();
270
- if (db === null) {
271
- this.#dropped += this.#pending.size;
272
- this.#pending.clear();
273
- return;
274
- }
275
- const batch = new Map(this.#pending);
276
- this.#pending.clear();
277
- const upsert = db.prepare(`INSERT INTO telemetry (task_id, role_name, turn_id, progress_id, sequence, payload, received_at)
278
- VALUES (?, ?, ?, ?, ?, ?, ?)
279
- ON CONFLICT(task_id, role_name, turn_id, progress_id) DO UPDATE SET
280
- sequence = excluded.sequence,
281
- payload = excluded.payload,
282
- received_at = excluded.received_at
283
- WHERE excluded.received_at >= telemetry.received_at`);
284
- try {
285
- const touchedRuns = new Map();
286
- db.transaction(() => {
287
- for (const entry of batch.values()) {
288
- upsert.run(entry.taskId, entry.roleName, entry.runId, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
289
- touchedRuns.set(`${entry.taskId}\0${entry.runId}`, {
290
- taskId: entry.taskId,
291
- runId: entry.runId
292
- });
293
- }
294
- for (const { taskId, runId } of touchedRuns.values()) {
295
- this.capRun(taskId, runId);
296
- }
297
- })();
298
- this.#applied += batch.size;
299
- }
300
- catch (error) {
301
- this.#lastError = error instanceof Error ? error.message : String(error);
302
- this.#dropped += batch.size;
281
+ #flushPending() {
282
+ this.#flushing ??= this.#drain().finally(() => { this.#flushing = undefined; });
283
+ return this.#flushing;
284
+ }
285
+ async #drain() {
286
+ while (this.#pending.size > 0) {
287
+ const batch = [];
288
+ for (const [key, entry] of this.#pending) {
289
+ this.#pending.delete(key);
290
+ batch.push(entry);
291
+ if (batch.length === 256)
292
+ break;
293
+ }
294
+ try {
295
+ const writer = this.#writer ?? (this.#ownedWriter ??= new AsyncTaskStoreClient(this.#home, { readPoolSize: 1 }));
296
+ await writer.flushTelemetry(batch, this.#turnCap);
297
+ this.#applied += batch.length;
298
+ }
299
+ catch (error) {
300
+ this.#lastError = error instanceof Error ? error.message : String(error);
301
+ this.#dropped += batch.length;
302
+ }
303
303
  }
304
304
  }
305
305
  }
@@ -1,16 +1,16 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { resolveRunCap, resolveTerminalKeep } from "./telemetryConfig.js";
4
3
  import { resolveTelemetryEnabled } from "../config/yuiConfig.js";
5
- import { SqliteTelemetryStore } from "./sqliteTelemetryStore.js";
6
4
  import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../storage/currentTaskStore.js";
5
+ import { SqliteTelemetryStore } from "./sqliteTelemetryStore.js";
6
+ import { resolveRunCap, resolveTerminalKeep } from "./telemetryConfig.js";
7
7
  /**
8
8
  * Open optional telemetry from the current Home's authoritative `yui.db`.
9
9
  * Disabled telemetry performs no database work. Once opened, telemetry fails
10
10
  * isolated: a broken sink increments its dropped counter without blocking the
11
11
  * semantic lane.
12
12
  */
13
- export function openSchedulerTelemetry(home, config) {
13
+ export function openSchedulerTelemetry(home, config, writer) {
14
14
  if (!resolveTelemetryEnabled(config.telemetryEnabled))
15
15
  return null;
16
16
  const mode = "on";
@@ -21,6 +21,7 @@ export function openSchedulerTelemetry(home, config) {
21
21
  }
22
22
  const store = new SqliteTelemetryStore(home, {
23
23
  mode,
24
+ writer,
24
25
  terminalKeep: resolveTerminalKeep(config.telemetryTerminalKeep),
25
26
  runCap: resolveRunCap(config.telemetryRunCap)
26
27
  });
@@ -509,7 +509,9 @@ export class TmuxManager {
509
509
  let output;
510
510
  try {
511
511
  output = this.run([
512
- "list-panes", "-s", "-t", this.sessionName(taskId), "-F",
512
+ // list-panes takes a pane target: the colon keeps this an exact
513
+ // session lookup instead of falling back to a similarly named one.
514
+ "list-panes", "-s", "-t", `=${this.sessionName(taskId)}:`, "-F",
513
515
  `#{window_name}${formatSeparator}#{pane_dead}${formatSeparator}#{pane_dead_status}${formatSeparator}#{pane_pid}${formatSeparator}#{pane_current_command}`
514
516
  ]);
515
517
  }
@@ -865,7 +867,7 @@ export class TmuxManager {
865
867
  this.recordTaskTargets(taskId);
866
868
  for (const session of [...sessions.filter((name) => name !== taskSession), taskSession]) {
867
869
  try {
868
- this.run(["kill-session", "-t", session]);
870
+ this.run(["kill-session", "-t", `=${session}`]);
869
871
  }
870
872
  catch (error) {
871
873
  if (!isExplicitlyAbsentTmuxSession(error))
@@ -882,7 +884,7 @@ export class TmuxManager {
882
884
  await this.recordTaskTargetsAsync(taskId);
883
885
  for (const session of [...sessions.filter((name) => name !== taskSession), taskSession]) {
884
886
  try {
885
- await this.runAsync(["kill-session", "-t", session]);
887
+ await this.runAsync(["kill-session", "-t", `=${session}`]);
886
888
  }
887
889
  catch (error) {
888
890
  if (!isExplicitlyAbsentTmuxSession(error))
@@ -897,11 +899,23 @@ export class TmuxManager {
897
899
  }
898
900
  killRole(taskId, roleName) {
899
901
  this.recordRoleTarget(taskId, roleName);
900
- this.run(["kill-window", "-t", this.target(taskId, roleName)]);
902
+ try {
903
+ this.run(["kill-window", "-t", this.exactTarget(taskId, roleName)]);
904
+ }
905
+ catch (error) {
906
+ if (!isExplicitlyAbsentTmuxSession(error))
907
+ throw error;
908
+ }
901
909
  }
902
910
  async killRoleAsync(taskId, roleName) {
903
911
  this.recordRoleTarget(taskId, roleName);
904
- await this.runAsync(["kill-window", "-t", this.target(taskId, roleName)]);
912
+ try {
913
+ await this.runAsync(["kill-window", "-t", this.exactTarget(taskId, roleName)]);
914
+ }
915
+ catch (error) {
916
+ if (!isExplicitlyAbsentTmuxSession(error))
917
+ throw error;
918
+ }
905
919
  }
906
920
  renameRole(taskId, oldRoleName, newRoleName) {
907
921
  const target = this.target(taskId, oldRoleName);
@@ -916,7 +930,7 @@ export class TmuxManager {
916
930
  }
917
931
  hasSession(taskId) {
918
932
  try {
919
- this.run(["has-session", "-t", this.sessionName(taskId)]);
933
+ this.run(["has-session", "-t", `=${this.sessionName(taskId)}`]);
920
934
  return true;
921
935
  }
922
936
  catch (error) {
@@ -927,7 +941,7 @@ export class TmuxManager {
927
941
  }
928
942
  async hasSessionAsync(taskId) {
929
943
  try {
930
- await this.runAsync(["has-session", "-t", this.sessionName(taskId)]);
944
+ await this.runAsync(["has-session", "-t", `=${this.sessionName(taskId)}`]);
931
945
  return true;
932
946
  }
933
947
  catch (error) {
@@ -941,7 +955,7 @@ export class TmuxManager {
941
955
  return [];
942
956
  try {
943
957
  return this.run([
944
- "list-windows", "-t", this.sessionName(taskId), "-F", "#{window_name}"
958
+ "list-windows", "-t", `=${this.sessionName(taskId)}`, "-F", "#{window_name}"
945
959
  ]).split("\n").map((name) => name.trim()).filter(Boolean);
946
960
  }
947
961
  catch (error) {
@@ -953,7 +967,7 @@ export class TmuxManager {
953
967
  async sessionWindowNamesAsync(taskId) {
954
968
  try {
955
969
  const names = (await this.runAsync([
956
- "list-windows", "-t", this.sessionName(taskId), "-F", "#{window_name}"
970
+ "list-windows", "-t", `=${this.sessionName(taskId)}`, "-F", "#{window_name}"
957
971
  ])).split("\n").map((name) => name.trim()).filter(Boolean);
958
972
  return { exists: true, names };
959
973
  }
@@ -1141,16 +1155,12 @@ function writableClientSessionPrefix(roleName) {
1141
1155
  function writableLeaseMatchesRole(sessionName, roleName) {
1142
1156
  if (!sessionName.startsWith(WRITABLE_CLIENT_SESSION_PREFIX))
1143
1157
  return false;
1144
- if (roleName === undefined)
1145
- return true;
1146
- if (sessionName.startsWith(HOST_WRITABLE_CLIENT_SESSION_PREFIX))
1158
+ if (/^yui-writer-host-[a-f0-9]{24}$/u.test(sessionName))
1147
1159
  return true;
1148
- if (sessionName.startsWith(ROLE_WRITABLE_CLIENT_SESSION_PREFIX)) {
1149
- return sessionName.startsWith(writableClientSessionPrefix(roleName));
1160
+ if (/^yui-writer-role-[a-f0-9]{24}-[a-f0-9]{24}$/u.test(sessionName)) {
1161
+ return roleName === undefined || sessionName.startsWith(writableClientSessionPrefix(roleName));
1150
1162
  }
1151
- // Conservative compatibility for writer leases created before Role-scoped
1152
- // lease names existed.
1153
- return true;
1163
+ throw runtimeError(`Unverified tmux writer lease: ${sessionName}. Preserve it and inspect its owner before retrying.`);
1154
1164
  }
1155
1165
  function writableClientRowsContainMatch(clients, taskSession, roleName, excludedLease, formatSeparator, encodedSeparator) {
1156
1166
  return clients.split("\n").some((line) => {
@@ -1169,9 +1179,8 @@ function writableClientRowsContainMatch(clients, taskSession, roleName, excluded
1169
1179
  || readOnly !== "0") {
1170
1180
  return false;
1171
1181
  }
1172
- // Current Yui clients publish a lease before attach. A direct or legacy
1173
- // writable tmux client has no Role identity, so conservatively fence every
1174
- // Role in that host.
1182
+ // A direct writable tmux client has no Role identity and fences the host.
1183
+ // A Yui lease must instead prove its exact current scope.
1175
1184
  return !sessionName.startsWith(WRITABLE_CLIENT_SESSION_PREFIX)
1176
1185
  || writableLeaseMatchesRole(sessionName, roleName);
1177
1186
  });
@@ -4,10 +4,11 @@ import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp
4
4
  /**
5
5
  * Issue 08: the reusable exact-SHA gate evidence record.
6
6
  *
7
- * A GateArtifact binds one gate run to its full identity tuple: Project,
7
+ * A GateArtifact is the latest cached proof for its full identity tuple: Project,
8
8
  * candidate commit, VerificationPlan digest, toolchain digest, and (for L2)
9
- * the source/base/target boundary. The same tuple reuses the same successful
10
- * artifact; any tuple change invalidates it. Artifacts are stored in the
9
+ * the source/base/target boundary. Fresh execution withdraws the previous
10
+ * success; a failed or incomplete latest result is not reusable. Job and
11
+ * Integration records own execution history. Artifacts are stored in the
11
12
  * SQLite `gate_artifacts` / `gate_artifact_logs` tables, backend-neutral
12
13
  * through the GateArtifactStorePort.
13
14
  */
@@ -40,12 +41,11 @@ export function createGateArtifact(identity, metadata, now) {
40
41
  planVersion: requireText(metadata.planVersion, "GateArtifact planVersion"),
41
42
  planDigest: identity.planDigest,
42
43
  toolchainDigest: identity.toolchainDigest,
43
- ...(identity.boundary === undefined ? {} : { boundary: identity.boundary }),
44
+ boundary: identity.boundary,
44
45
  steps: Object.freeze([]),
45
46
  generator: requireText(metadata.generator, "GateArtifact generator"),
46
47
  status: "incomplete",
47
48
  outcome: "unknown",
48
- potentialReuseCount: 0,
49
49
  reuseCount: 0,
50
50
  createdAt: timestamp,
51
51
  lastUsedAt: timestamp
@@ -67,16 +67,7 @@ export function completeGateArtifact(artifact, steps, outcome, now) {
67
67
  lastUsedAt: timestamp
68
68
  });
69
69
  }
70
- /** Record a shadow potential-reuse observation (record mode). */
71
- export function recordGateArtifactPotentialReuse(artifact, now) {
72
- validateGateArtifact(artifact);
73
- return validateGateArtifact({
74
- ...artifact,
75
- potentialReuseCount: artifact.potentialReuseCount + 1,
76
- lastUsedAt: now.toISOString()
77
- });
78
- }
79
- /** Record an actual reuse (reuse/enforce mode). */
70
+ /** Record an actual reuse of complete successful evidence. */
80
71
  export function recordGateArtifactReuse(artifact, now) {
81
72
  validateGateArtifact(artifact);
82
73
  if (artifact.status !== "complete" || artifact.outcome !== "succeeded") {
@@ -93,7 +84,7 @@ export function validateGateArtifact(artifact) {
93
84
  throw new Error(`GateArtifact must use schemaVersion ${GATE_ARTIFACT_SCHEMA_VERSION}.`);
94
85
  }
95
86
  requireIdentity(artifact.projectId, "GateArtifact projectId");
96
- if (artifact.level !== "L1" && artifact.level !== "L2") {
87
+ if (artifact.level !== "L2") {
97
88
  throw new Error(`GateArtifact level is invalid: ${String(artifact.level)}.`);
98
89
  }
99
90
  requireCommit(artifact.commit, "GateArtifact commit");
@@ -106,17 +97,15 @@ export function validateGateArtifact(artifact) {
106
97
  commit: artifact.commit,
107
98
  planDigest: artifact.planDigest,
108
99
  toolchainDigest: artifact.toolchainDigest,
109
- ...(artifact.boundary === undefined ? {} : { boundary: artifact.boundary })
100
+ boundary: artifact.boundary
110
101
  })) {
111
102
  throw new Error("GateArtifact key does not match its identity tuple.");
112
103
  }
113
- if (artifact.level === "L2" && artifact.boundary === undefined) {
104
+ if (artifact.boundary === undefined) {
114
105
  throw new Error("An L2 GateArtifact requires a target boundary.");
115
106
  }
116
- if (artifact.boundary !== undefined) {
117
- requireText(artifact.boundary.targetRef, "GateArtifact boundary targetRef");
118
- requireCommit(artifact.boundary.baseHead, "GateArtifact boundary baseHead");
119
- }
107
+ requireText(artifact.boundary.targetRef, "GateArtifact boundary targetRef");
108
+ requireCommit(artifact.boundary.baseHead, "GateArtifact boundary baseHead");
120
109
  if (!["incomplete", "complete"].includes(artifact.status)) {
121
110
  throw new Error(`GateArtifact status is invalid: ${String(artifact.status)}.`);
122
111
  }
@@ -150,7 +139,8 @@ export function validateGateArtifact(artifact) {
150
139
  throw new Error(`GateArtifact step ${step.name} logBytes is invalid.`);
151
140
  }
152
141
  }
153
- requirePositiveInteger(artifact.potentialReuseCount + 1, "GateArtifact potentialReuseCount");
142
+ if (Object.hasOwn(artifact, "potentialReuseCount"))
143
+ throw new Error("GateArtifact contains retired shadow metrics.");
154
144
  requirePositiveInteger(artifact.reuseCount + 1, "GateArtifact reuseCount");
155
145
  requireTimestamp(artifact.createdAt, "GateArtifact createdAt");
156
146
  requireTimestamp(artifact.lastUsedAt, "GateArtifact lastUsedAt");
@@ -185,7 +175,8 @@ export function verifyGateArtifactLogs(artifact, logs) {
185
175
  });
186
176
  }
187
177
  export function isReusableGateArtifact(artifact) {
188
- return artifact.status === "complete" && artifact.outcome === "succeeded";
178
+ return artifact.status === "complete" && artifact.outcome === "succeeded"
179
+ && artifact.steps.length > 0 && artifact.steps.every(step => step.outcome === "passed" && step.exitCode === 0 && step.signal === null && !step.timedOut);
189
180
  }
190
181
  function requireCommit(value, label) {
191
182
  const normalized = requireText(value, label).toLowerCase();
@@ -30,19 +30,26 @@ export function findGateArtifact(store, identity) {
30
30
  * commit, plan digest, toolchain digest, and target ref. Unlike
31
31
  * {@link findGateArtifact} the base head is not part of the match: a release
32
32
  * consumes the gate that proved the exact frozen tree, regardless of which
33
- * base it integrated onto. Logs are verified before returning.
33
+ * base it integrated onto. The newest recorded matching result governs:
34
+ * failure/incompleteness/corruption must not fall through to an older success.
35
+ * Logs are verified before returning.
34
36
  */
35
37
  export async function findL2ArtifactForCommit(store, query) {
36
- const artifacts = store.findL2GateArtifactsForCommit(query);
37
- for (const artifact of artifacts) {
38
+ const recordedAt = (artifact) => Date.parse(artifact.completedAt ?? artifact.createdAt);
39
+ const artifacts = [...store.findL2GateArtifactsForCommit(query)]
40
+ .sort((left, right) => recordedAt(right) - recordedAt(left));
41
+ const latest = artifacts[0];
42
+ if (latest === undefined)
43
+ return null;
44
+ for (const artifact of artifacts.filter(value => recordedAt(value) === recordedAt(latest))) {
38
45
  if (!isReusableGateArtifact(artifact))
39
- continue;
46
+ return null;
40
47
  const logs = store.getGateArtifactLogs(artifact.key);
41
48
  const verification = verifyGateArtifactLogs(artifact, logs);
42
- if (verification.ok)
43
- return artifact;
49
+ if (!verification.ok)
50
+ return null;
44
51
  }
45
- return null;
52
+ return latest;
46
53
  }
47
54
  /**
48
55
  * Read each step's source log, hash it, and return the completed step
@@ -2,9 +2,10 @@ import { spawn } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { mkdir, open, rm } from "node:fs/promises";
4
4
  import { join, resolve } from "node:path";
5
- import { completeGateArtifact, createGateArtifact, gateArtifactRef, isReusableGateArtifact, parseGateArtifactRef, recordGateArtifactReuse, validateGateArtifact, verifyGateArtifactLogs } from "./gateArtifact.js";
6
- import { findGateArtifact, importGateArtifactSteps, loadGateArtifact, saveGateArtifact, touchGateArtifact } from "./gateArtifactStore.js";
7
- import { planL1JobSteps, resolveProjectVerificationPlan, resolveToolchain, selectL1Checks, toolchainDigest, verificationPlanDigest, verificationStepCommand } from "./verificationPlan.js";
5
+ import { jobStepDirectory } from "../job/stepDirectory.js";
6
+ import { completeGateArtifact, createGateArtifact, gateArtifactRef, isReusableGateArtifact, parseGateArtifactRef, verifyGateArtifactLogs } from "./gateArtifact.js";
7
+ import { findGateArtifact, importGateArtifactSteps, loadGateArtifact, saveGateArtifact } from "./gateArtifactStore.js";
8
+ import { resolveProjectVerificationPlan, resolveToolchain, toolchainDigest, verificationPlanDigest } from "./verificationPlan.js";
8
9
  /** Resolve a Project's active plan and the runtime toolchain it gates under. */
9
10
  export function resolveVerificationGate(project, _environment = process.env) {
10
11
  const plan = resolveProjectVerificationPlan(project);
@@ -13,26 +14,22 @@ export function resolveVerificationGate(project, _environment = process.env) {
13
14
  const toolchain = resolveToolchain();
14
15
  return Object.freeze({
15
16
  plan,
16
- mode: plan.mode,
17
17
  toolchain,
18
18
  planDigest: verificationPlanDigest(plan),
19
19
  toolchainDigest: toolchainDigest(plan, toolchain)
20
20
  });
21
21
  }
22
22
  export function gateIdentityForCandidate(input) {
23
- const boundary = input.level === "L2"
24
- ? {
25
- targetRef: input.targetRef ?? "master",
26
- baseHead: input.baseHead ?? input.commit
27
- }
28
- : undefined;
23
+ if (input.level !== "L2" || input.targetRef === undefined || input.baseHead === undefined) {
24
+ throw new Error("L2 verification requires its exact target ref and base head.");
25
+ }
29
26
  return Object.freeze({
30
27
  projectId: input.projectId,
31
28
  level: input.level,
32
29
  commit: input.commit,
33
30
  planDigest: input.gate.planDigest,
34
31
  toolchainDigest: input.gate.toolchainDigest,
35
- ...(boundary === undefined ? {} : { boundary })
32
+ boundary: { targetRef: input.targetRef, baseHead: input.baseHead }
36
33
  });
37
34
  }
38
35
  /**
@@ -48,6 +45,17 @@ export async function lookupReusableGateArtifact(store, identity) {
48
45
  const verification = verifyGateArtifactLogs(artifact, logs);
49
46
  return verification.ok ? artifact : null;
50
47
  }
48
+ /** Withdraw prior reusable evidence before execution. An interrupted attempt
49
+ * remains incomplete, never an excuse to return the previous success. */
50
+ export function beginGateVerification(store, identity, plan, now) {
51
+ const existing = findGateArtifact(store, identity);
52
+ const artifact = {
53
+ ...createGateArtifact(identity, { planId: plan.id, planVersion: plan.version, generator: "yui" }, now),
54
+ reuseCount: existing?.reuseCount ?? 0
55
+ };
56
+ saveGateArtifact(store, artifact, new Map());
57
+ return artifact;
58
+ }
51
59
  /**
52
60
  * Record a GateArtifact from a terminal check DurableJob. The job's step
53
61
  * logs are copied into the artifact store and bound to their digests, so the
@@ -91,38 +99,17 @@ export async function recordGateArtifactFromStepOutcomes(store, identity, plan,
91
99
  return recordGateArtifact(store, identity, plan, steps, succeeded, now);
92
100
  }
93
101
  async function recordGateArtifact(store, identity, plan, steps, succeeded, now) {
94
- const created = createGateArtifact(identity, {
95
- planId: plan.id,
96
- planVersion: plan.version,
97
- generator: "yui"
98
- }, now);
102
+ const created = beginGateVerification(store, identity, plan, now);
99
103
  const { steps: importedSteps, logs } = await importGateArtifactSteps(steps);
100
- let artifact = completeGateArtifact(created, importedSteps, succeeded ? "succeeded" : "failed", now);
101
- // Preserve shadow/reuse counters when re-recording the same identity tuple
102
- // (e.g. record mode always re-runs the gate but must not lose the potential
103
- // reuse observations from earlier runs).
104
- const existing = findGateArtifact(store, identity);
105
- if (existing !== null) {
106
- if (isReusableGateArtifact(existing) && !succeeded) {
107
- // A failed re-run must not downgrade a proven successful artifact.
108
- touchGateArtifact(store, existing);
109
- return existing;
110
- }
111
- artifact = validateGateArtifact({
112
- ...artifact,
113
- potentialReuseCount: existing.potentialReuseCount,
114
- reuseCount: existing.reuseCount,
115
- createdAt: existing.createdAt
116
- });
117
- }
104
+ const artifact = completeGateArtifact(created, importedSteps, succeeded && importedSteps.length > 0 && importedSteps.every(step => step.outcome === "passed")
105
+ ? "succeeded" : "failed", now);
118
106
  saveGateArtifact(store, artifact, logs);
119
107
  return artifact;
120
108
  }
121
109
  const GATE_STEP_TIMEOUT_MS = 30 * 60_000;
122
110
  const SIGKILL_GRACE_MS = 2_000;
123
111
  /**
124
- * Run structured gate steps in-process (L1 targeted checks and the jobless
125
- * L2 fallback). Each step gets its own log file; argv steps run without a
112
+ * Run structured gate steps in-process. Each step gets its own log file; argv steps run without a
126
113
  * shell. This is the local form of the gate; the DurableJob form is the
127
114
  * Controller-owned one used by production Integration.
128
115
  */
@@ -144,9 +131,9 @@ export async function runGateStepsInProcess(workspace, steps, environment, logsD
144
131
  const output = await open(absoluteLogPath, "w", 0o600);
145
132
  let timedOut = false;
146
133
  let child;
147
- const stepCwd = step.cwd ?? cwd;
148
134
  const stepEnv = step.env === undefined ? env : { ...env, ...step.env };
149
135
  try {
136
+ const stepCwd = jobStepDirectory(cwd, step.cwd);
150
137
  if (step.argv !== undefined) {
151
138
  const [file, ...args] = step.argv;
152
139
  child = spawn(file, args, {
@@ -228,7 +215,7 @@ function sanitizeLogName(name) {
228
215
  * environment/registry failure is never recorded as a Candidate test
229
216
  * failure; the Integration gate has not started when bootstrap fails.
230
217
  */
231
- export function checkResultsFromGateJob(job, home) {
218
+ export function checkResultsFromGateJob(job, _home) {
232
219
  const results = new Map((job.result?.steps ?? []).map((step) => [step.name, step]));
233
220
  const checks = [];
234
221
  for (const step of job.steps) {
@@ -285,6 +272,10 @@ export function checkResultsFromGateArtifact(artifact, reused = false) {
285
272
  outcome: step.outcome,
286
273
  ...(step.logPath === undefined ? {} : { logPath: step.logPath })
287
274
  }));
275
+ if (!isReusableGateArtifact(artifact) && !checks.some(check => check.outcome === "failed")) {
276
+ checks.push({ name: "verification-result", outcome: "failed",
277
+ details: `Verification is ${artifact.status}/${artifact.outcome}; successful evidence is not established.` });
278
+ }
288
279
  if (reused) {
289
280
  checks.push({
290
281
  name: gateArtifactRef(artifact.key),
@@ -331,56 +322,6 @@ export async function verifyGateArtifactForReview(store, projectId, key, expecte
331
322
  }
332
323
  return { ok: true, artifact };
333
324
  }
334
- /**
335
- * Enforce mode (rollout step 4): reject ad-hoc full-suite shell checks that
336
- * duplicate the plan's L2 steps for a configured Project. Explicit targeted
337
- * diagnostic checks (anything that is not an L2 step command) stay allowed.
338
- */
339
- export function assertNoAdHocFullSuiteChecks(plan, checkCommands) {
340
- const l2Commands = new Set(plan.l2.steps.map((step) => verificationStepCommand(step)));
341
- for (const command of checkCommands) {
342
- if (l2Commands.has(command)) {
343
- throw new Error(`Ad-hoc full-suite check is rejected for plan-enabled Project `
344
- + `${plan.id}: use the VerificationPlan L2 gate (or pass a targeted `
345
- + `diagnostic command that is not an L2 step): ${command}`);
346
- }
347
- }
348
- }
349
- /**
350
- * Run an L1 gate for a change: select the affected categories' checks, run
351
- * them in-process, and record the L1 artifact. Reuse mode returns an existing
352
- * successful artifact for the same tuple.
353
- */
354
- export async function runL1Gate(input) {
355
- const identity = gateIdentityForCandidate({
356
- projectId: input.projectId,
357
- gate: input.gate,
358
- level: "L1",
359
- commit: input.commit
360
- });
361
- if (input.gate.mode !== "record") {
362
- const existing = await lookupReusableGateArtifact(input.store, identity);
363
- if (existing !== null) {
364
- const reused = recordGateArtifactReuse(existing, input.now);
365
- touchGateArtifact(input.store, reused);
366
- return {
367
- artifact: reused,
368
- reused: true,
369
- checks: checkResultsFromGateArtifact(reused, true)
370
- };
371
- }
372
- }
373
- const selected = selectL1Checks(input.gate.plan, input.changedPaths);
374
- const outcomes = await runGateStepsInProcess(input.workspace, planL1JobSteps(selected), input.environment, input.logsDirectory, input.commit);
375
- const succeeded = outcomes.length > 0
376
- && outcomes.every((outcome) => outcome.exitCode === 0 && outcome.signal === null && !outcome.timedOut);
377
- const artifact = await recordGateArtifactFromStepOutcomes(input.store, identity, input.gate.plan, outcomes, succeeded, input.now);
378
- return {
379
- artifact,
380
- reused: false,
381
- checks: checkResultsFromGateArtifact(artifact, false)
382
- };
383
- }
384
325
  /** Stable digest of a log file, for ad-hoc evidence binding. */
385
326
  export function digestLogContent(content) {
386
327
  return createHash("sha256").update(content).digest("hex");