@zq-silk/yui 0.6.0 → 0.6.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 (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
@@ -0,0 +1,87 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { gateArtifactKey, isReusableGateArtifact, validateGateArtifact, verifyGateArtifactLogs } from "./gateArtifact.js";
4
+ /**
5
+ * Issue 08: the GateArtifact store adapter.
6
+ *
7
+ * Every function delegates to a {@link GateArtifactStorePort} (satisfied by
8
+ * both SqliteTaskStore and FileTaskStore). The artifact record and its step
9
+ * logs are persisted atomically by the store implementation; this module
10
+ * owns the domain logic (key computation, log hashing, reuse lookup,
11
+ * retention) but no filesystem paths.
12
+ */
13
+ export function saveGateArtifact(store, artifact, logs) {
14
+ validateGateArtifact(artifact);
15
+ store.saveGateArtifact(artifact, logs);
16
+ }
17
+ /** Update only the artifact record (counters, timestamps); logs are untouched. */
18
+ export function touchGateArtifact(store, artifact) {
19
+ validateGateArtifact(artifact);
20
+ store.touchGateArtifact(artifact);
21
+ }
22
+ export function loadGateArtifact(store, projectId, key) {
23
+ return store.getGateArtifact(projectId, key);
24
+ }
25
+ export function findGateArtifact(store, identity) {
26
+ return store.findGateArtifactByIdentity(identity);
27
+ }
28
+ /**
29
+ * Find a reusable L2 artifact for an exact commit, matched on Project,
30
+ * commit, plan digest, toolchain digest, and target ref. Unlike
31
+ * {@link findGateArtifact} the base head is not part of the match: a release
32
+ * consumes the gate that proved the exact frozen tree, regardless of which
33
+ * base it integrated onto. Logs are verified before returning.
34
+ */
35
+ export async function findL2ArtifactForCommit(store, query) {
36
+ const artifacts = store.findL2GateArtifactsForCommit(query);
37
+ for (const artifact of artifacts) {
38
+ if (!isReusableGateArtifact(artifact))
39
+ continue;
40
+ const logs = store.getGateArtifactLogs(artifact.key);
41
+ const verification = verifyGateArtifactLogs(artifact, logs);
42
+ if (verification.ok)
43
+ return artifact;
44
+ }
45
+ return null;
46
+ }
47
+ /**
48
+ * Read each step's source log, hash it, and return the completed step
49
+ * records plus a map of log content keyed by step name. A missing source
50
+ * log is a malformed gate result: the caller must not record a successful
51
+ * artifact without its evidence.
52
+ */
53
+ export async function importGateArtifactSteps(steps) {
54
+ const imported = [];
55
+ const logs = new Map();
56
+ for (const step of steps) {
57
+ const content = await readFile(step.sourceLogPath);
58
+ logs.set(step.name, content);
59
+ imported.push(Object.freeze({
60
+ name: step.name,
61
+ command: step.command,
62
+ ...(step.argv === undefined ? {} : { argv: step.argv }),
63
+ outcome: step.outcome,
64
+ exitCode: step.exitCode,
65
+ signal: step.signal,
66
+ timedOut: step.timedOut,
67
+ durationMs: step.durationMs,
68
+ logPath: step.logName,
69
+ logDigest: createHash("sha256").update(content).digest("hex"),
70
+ logBytes: content.length
71
+ }));
72
+ }
73
+ return {
74
+ steps: Object.freeze(imported),
75
+ logs
76
+ };
77
+ }
78
+ /**
79
+ * Minimal retention for one Project's artifacts. Referenced artifacts are
80
+ * always retained. An unreferenced artifact is deleted only when its
81
+ * `lastUsedAt` is older than the TTL window; the reference check runs at
82
+ * deletion time, so a freshly referenced artifact survives the sweep.
83
+ */
84
+ export function pruneGateArtifacts(store, projectId, options) {
85
+ return store.pruneGateArtifacts(projectId, options);
86
+ }
87
+ export { gateArtifactKey };
@@ -0,0 +1,414 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, open, rm } from "node:fs/promises";
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";
8
+ /** Resolve a Project's active plan and the runtime toolchain it gates under. */
9
+ export function resolveVerificationGate(project, _environment = process.env) {
10
+ const plan = resolveProjectVerificationPlan(project);
11
+ if (plan === undefined)
12
+ return undefined;
13
+ const toolchain = resolveToolchain();
14
+ return Object.freeze({
15
+ plan,
16
+ mode: plan.mode,
17
+ toolchain,
18
+ planDigest: verificationPlanDigest(plan),
19
+ toolchainDigest: toolchainDigest(plan, toolchain)
20
+ });
21
+ }
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;
29
+ return Object.freeze({
30
+ projectId: input.projectId,
31
+ level: input.level,
32
+ commit: input.commit,
33
+ planDigest: input.gate.planDigest,
34
+ toolchainDigest: input.gate.toolchainDigest,
35
+ ...(boundary === undefined ? {} : { boundary })
36
+ });
37
+ }
38
+ /**
39
+ * Look up a reusable artifact for an identity tuple. Only a complete,
40
+ * successful artifact whose logs still verify is reusable; an incomplete
41
+ * artifact from a crashed gate is never returned.
42
+ */
43
+ export async function lookupReusableGateArtifact(store, identity) {
44
+ const artifact = findGateArtifact(store, identity);
45
+ if (artifact === null || !isReusableGateArtifact(artifact))
46
+ return null;
47
+ const logs = store.getGateArtifactLogs(artifact.key);
48
+ const verification = verifyGateArtifactLogs(artifact, logs);
49
+ return verification.ok ? artifact : null;
50
+ }
51
+ /**
52
+ * Record a GateArtifact from a terminal check DurableJob. The job's step
53
+ * logs are copied into the artifact store and bound to their digests, so the
54
+ * artifact is self-contained evidence after the job's own logs are cleaned.
55
+ */
56
+ export async function recordGateArtifactFromJob(store, home, identity, plan, job, now) {
57
+ const steps = (job.result?.steps ?? []).map((result) => {
58
+ const planned = job.steps.find((step) => step.name === result.name);
59
+ return {
60
+ name: result.name,
61
+ command: planned?.command ?? result.name,
62
+ ...(planned?.argv === undefined ? {} : { argv: planned.argv }),
63
+ outcome: !result.timedOut && result.signal === null && result.exitCode === 0
64
+ ? "passed"
65
+ : "failed",
66
+ exitCode: result.exitCode,
67
+ signal: result.signal,
68
+ timedOut: result.timedOut,
69
+ durationMs: result.durationMs,
70
+ sourceLogPath: join(resolve(home), job.artifactsLocator, "logs", result.logPath),
71
+ logName: result.logPath
72
+ };
73
+ });
74
+ return recordGateArtifact(store, identity, plan, steps, job.result?.outcome === "succeeded", now);
75
+ }
76
+ export async function recordGateArtifactFromStepOutcomes(store, identity, plan, outcomes, succeeded, now) {
77
+ const steps = outcomes.map((outcome) => ({
78
+ name: outcome.name,
79
+ command: outcome.command,
80
+ ...(outcome.argv === undefined ? {} : { argv: outcome.argv }),
81
+ outcome: !outcome.timedOut && outcome.signal === null && outcome.exitCode === 0
82
+ ? "passed"
83
+ : "failed",
84
+ exitCode: outcome.exitCode,
85
+ signal: outcome.signal,
86
+ timedOut: outcome.timedOut,
87
+ durationMs: outcome.durationMs,
88
+ sourceLogPath: outcome.sourceLogPath,
89
+ logName: outcome.logName
90
+ }));
91
+ return recordGateArtifact(store, identity, plan, steps, succeeded, now);
92
+ }
93
+ 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);
99
+ 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
+ }
118
+ saveGateArtifact(store, artifact, logs);
119
+ return artifact;
120
+ }
121
+ const GATE_STEP_TIMEOUT_MS = 30 * 60_000;
122
+ const SIGKILL_GRACE_MS = 2_000;
123
+ /**
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
126
+ * shell. This is the local form of the gate; the DurableJob form is the
127
+ * Controller-owned one used by production Integration.
128
+ */
129
+ export async function runGateStepsInProcess(workspace, steps, environment, logsDirectory, _head) {
130
+ await rm(logsDirectory, { recursive: true, force: true });
131
+ await mkdir(logsDirectory, { recursive: true, mode: 0o700 });
132
+ const outcomes = [];
133
+ for (const [index, step] of steps.entries()) {
134
+ const logName = `${String(index + 1).padStart(3, "0")}-${sanitizeLogName(step.name)}.log`;
135
+ const absoluteLogPath = join(logsDirectory, logName);
136
+ const outcome = await runOneStep(step, workspace, environment, absoluteLogPath, logName);
137
+ outcomes.push(outcome);
138
+ if (outcome.exitCode !== 0 || outcome.signal !== null || outcome.timedOut)
139
+ break;
140
+ }
141
+ return Object.freeze(outcomes);
142
+ async function runOneStep(step, cwd, env, absoluteLogPath, logName) {
143
+ const startedAt = Date.now();
144
+ const output = await open(absoluteLogPath, "w", 0o600);
145
+ let timedOut = false;
146
+ let child;
147
+ const stepCwd = step.cwd ?? cwd;
148
+ const stepEnv = step.env === undefined ? env : { ...env, ...step.env };
149
+ try {
150
+ if (step.argv !== undefined) {
151
+ const [file, ...args] = step.argv;
152
+ child = spawn(file, args, {
153
+ cwd: stepCwd,
154
+ env: stepEnv,
155
+ stdio: ["ignore", output.fd, output.fd],
156
+ detached: true
157
+ });
158
+ }
159
+ else {
160
+ child = spawn("/bin/sh", ["-lc", step.command], {
161
+ cwd: stepCwd,
162
+ env: stepEnv,
163
+ stdio: ["ignore", output.fd, output.fd],
164
+ detached: true
165
+ });
166
+ }
167
+ }
168
+ catch (error) {
169
+ await output.close();
170
+ return {
171
+ name: step.name,
172
+ command: step.command,
173
+ ...(step.argv === undefined ? {} : { argv: step.argv }),
174
+ exitCode: null,
175
+ signal: null,
176
+ timedOut: false,
177
+ durationMs: Date.now() - startedAt,
178
+ sourceLogPath: absoluteLogPath,
179
+ logName
180
+ };
181
+ }
182
+ const timeout = setTimeout(() => {
183
+ timedOut = true;
184
+ if (child.pid !== undefined) {
185
+ const pid = child.pid;
186
+ try {
187
+ process.kill(-pid, "SIGTERM");
188
+ }
189
+ catch {
190
+ // The process group may already be gone.
191
+ }
192
+ setTimeout(() => {
193
+ try {
194
+ process.kill(-pid, "SIGKILL");
195
+ }
196
+ catch {
197
+ // The process group may already be gone.
198
+ }
199
+ }, SIGKILL_GRACE_MS).unref();
200
+ }
201
+ }, GATE_STEP_TIMEOUT_MS);
202
+ timeout.unref();
203
+ const completion = await new Promise((settle) => {
204
+ child.once("error", () => settle({ code: null, signal: null }));
205
+ child.once("close", (code, signal) => settle({ code, signal: signal ?? null }));
206
+ });
207
+ clearTimeout(timeout);
208
+ await output.close();
209
+ return {
210
+ name: step.name,
211
+ command: step.command,
212
+ ...(step.argv === undefined ? {} : { argv: step.argv }),
213
+ exitCode: completion.code,
214
+ signal: completion.signal,
215
+ timedOut,
216
+ durationMs: Date.now() - startedAt,
217
+ sourceLogPath: absoluteLogPath,
218
+ logName
219
+ };
220
+ }
221
+ }
222
+ function sanitizeLogName(name) {
223
+ return name.replaceAll(/[^A-Za-z0-9_.-]/gu, "_");
224
+ }
225
+ /**
226
+ * Map a terminal gate DurableJob to the attempt's CheckResult shape.
227
+ * Bootstrap steps are classified explicitly (`[bootstrap]` prefix) so an
228
+ * environment/registry failure is never recorded as a Candidate test
229
+ * failure; the Integration gate has not started when bootstrap fails.
230
+ */
231
+ export function checkResultsFromGateJob(job, home) {
232
+ const results = new Map((job.result?.steps ?? []).map((step) => [step.name, step]));
233
+ const checks = [];
234
+ for (const step of job.steps) {
235
+ const result = results.get(step.name);
236
+ if (result === undefined) {
237
+ checks.push({
238
+ name: step.name,
239
+ outcome: "skipped"
240
+ });
241
+ continue;
242
+ }
243
+ const logPath = `${job.artifactsLocator}/logs/${result.logPath}`;
244
+ const bootstrap = step.name.startsWith("bootstrap-");
245
+ const passed = !result.timedOut && result.signal === null && result.exitCode === 0;
246
+ const reason = result.timedOut
247
+ ? "Command timed out after 1800 seconds."
248
+ : result.signal !== null
249
+ ? `Command terminated by ${result.signal}.`
250
+ : result.exitCode !== 0
251
+ ? `Command exited with code ${result.exitCode}.`
252
+ : undefined;
253
+ checks.push({
254
+ name: step.name,
255
+ outcome: passed ? "passed" : "failed",
256
+ ...(reason === undefined
257
+ ? {}
258
+ : { details: bootstrap ? `[bootstrap] ${reason}` : reason }),
259
+ logPath
260
+ });
261
+ }
262
+ if (checks.length > 0
263
+ && !checks.some((check) => check.outcome === "failed")
264
+ && job.result?.outcome !== "succeeded") {
265
+ // Fail closed: a job that ended without proving the gate (cancelled,
266
+ // timed-out, unknown) must never pass the Integration gate.
267
+ const firstName = checks[0].name;
268
+ const isBootstrap = firstName.startsWith("bootstrap-");
269
+ checks[0] = {
270
+ name: firstName,
271
+ outcome: "failed",
272
+ details: job.result?.outcome === "unknown-needs-attention"
273
+ ? `[bootstrap] Check job unknown-needs-attention: ${job.result.unknownReason ?? "runner outcome is unproven"}.`
274
+ : `${isBootstrap ? "[bootstrap] " : ""}Check job ended ${job.result?.outcome ?? job.status} without proving the checks.`
275
+ };
276
+ }
277
+ return checks;
278
+ }
279
+ /** Map an artifact to the attempt's CheckResult shape. The reuse-ref entry
280
+ * is only included when the artifact was actually reused (not freshly
281
+ * recorded), so a fresh run never claims "Reused exact-SHA". */
282
+ export function checkResultsFromGateArtifact(artifact, reused = false) {
283
+ const checks = artifact.steps.map((step) => ({
284
+ name: step.name,
285
+ outcome: step.outcome,
286
+ ...(step.logPath === undefined ? {} : { logPath: step.logPath })
287
+ }));
288
+ if (reused) {
289
+ checks.push({
290
+ name: gateArtifactRef(artifact.key),
291
+ outcome: "passed",
292
+ details: `Reused exact-SHA ${artifact.level} gate artifact (commit ${artifact.commit}).`
293
+ });
294
+ }
295
+ return checks;
296
+ }
297
+ /**
298
+ * Verify a gate artifact for Review consumption: the record exists, is
299
+ * complete and successful, binds the expected commit (when given), and every
300
+ * step log still matches its digest. A failed verification is a verification
301
+ * gap: the Reviewer runs a targeted check for it instead of trusting prose.
302
+ */
303
+ export async function verifyGateArtifactForReview(store, projectId, key, expected = {}) {
304
+ const artifact = loadGateArtifact(store, projectId, key);
305
+ if (artifact === null) {
306
+ return { ok: false, reason: `Gate artifact not found: ${key}.` };
307
+ }
308
+ if (!isReusableGateArtifact(artifact)) {
309
+ return {
310
+ ok: false,
311
+ reason: `Gate artifact is not complete/successful: ${artifact.status}/${artifact.outcome}.`
312
+ };
313
+ }
314
+ if (expected.commit !== undefined && artifact.commit !== expected.commit) {
315
+ return {
316
+ ok: false,
317
+ reason: `Gate artifact commit ${artifact.commit} does not match ${expected.commit}.`
318
+ };
319
+ }
320
+ const logs = store.getGateArtifactLogs(artifact.key);
321
+ const verification = verifyGateArtifactLogs(artifact, logs);
322
+ if (!verification.ok) {
323
+ const parts = [
324
+ ...verification.missing.map((name) => `missing log: ${name}`),
325
+ ...verification.corrupted.map((name) => `corrupted log: ${name}`)
326
+ ];
327
+ return {
328
+ ok: false,
329
+ reason: `Gate artifact logs failed verification: ${parts.join("; ")}.`
330
+ };
331
+ }
332
+ return { ok: true, artifact };
333
+ }
334
+ /**
335
+ * Whether a `gate-artifact:` evidence reference covers the requested check
336
+ * commands for an exact candidate commit. Used by the integration queue's
337
+ * evidence fence: a passed gate step covers only its own command, and the
338
+ * artifact must bind the exact candidate tree.
339
+ */
340
+ export async function gateArtifactCoversCheckCommands(store, projectId, ref, checkCommands, candidateCommit) {
341
+ const key = parseGateArtifactRef(ref);
342
+ if (key === undefined)
343
+ return false;
344
+ const artifact = loadGateArtifact(store, projectId, key);
345
+ if (artifact === null || !isReusableGateArtifact(artifact))
346
+ return false;
347
+ if (artifact.commit !== candidateCommit)
348
+ return false;
349
+ const logs = store.getGateArtifactLogs(artifact.key);
350
+ const verification = verifyGateArtifactLogs(artifact, logs);
351
+ if (!verification.ok)
352
+ return false;
353
+ if (checkCommands.length === 0)
354
+ return true;
355
+ const covered = new Set(artifact.steps
356
+ .filter((step) => step.outcome === "passed")
357
+ .map((step) => step.command));
358
+ return checkCommands.every((command) => covered.has(command));
359
+ }
360
+ /**
361
+ * Enforce mode (rollout step 4): reject ad-hoc full-suite shell checks that
362
+ * duplicate the plan's L2 steps for a configured Project. Explicit targeted
363
+ * diagnostic checks (anything that is not an L2 step command) stay allowed.
364
+ */
365
+ export function assertNoAdHocFullSuiteChecks(plan, checkCommands) {
366
+ const l2Commands = new Set(plan.l2.steps.map((step) => verificationStepCommand(step)));
367
+ for (const command of checkCommands) {
368
+ if (l2Commands.has(command)) {
369
+ throw new Error(`Ad-hoc full-suite check is rejected for plan-enabled Project `
370
+ + `${plan.id}: use the VerificationPlan L2 gate (or pass a targeted `
371
+ + `diagnostic command that is not an L2 step): ${command}`);
372
+ }
373
+ }
374
+ }
375
+ /**
376
+ * Run an L1 gate for a change: select the affected categories' checks, run
377
+ * them in-process, and record the L1 artifact. Reuse mode returns an existing
378
+ * successful artifact for the same tuple.
379
+ */
380
+ export async function runL1Gate(input) {
381
+ const identity = gateIdentityForCandidate({
382
+ projectId: input.projectId,
383
+ gate: input.gate,
384
+ level: "L1",
385
+ commit: input.commit
386
+ });
387
+ if (input.gate.mode !== "record") {
388
+ const existing = await lookupReusableGateArtifact(input.store, identity);
389
+ if (existing !== null) {
390
+ const reused = recordGateArtifactReuse(existing, input.now);
391
+ touchGateArtifact(input.store, reused);
392
+ return {
393
+ artifact: reused,
394
+ reused: true,
395
+ checks: checkResultsFromGateArtifact(reused, true)
396
+ };
397
+ }
398
+ }
399
+ const selected = selectL1Checks(input.gate.plan, input.changedPaths);
400
+ const outcomes = await runGateStepsInProcess(input.workspace, planL1JobSteps(selected), input.environment, input.logsDirectory, input.commit);
401
+ const succeeded = outcomes.length > 0
402
+ && outcomes.every((outcome) => outcome.exitCode === 0 && outcome.signal === null && !outcome.timedOut);
403
+ const artifact = await recordGateArtifactFromStepOutcomes(input.store, identity, input.gate.plan, outcomes, succeeded, input.now);
404
+ return {
405
+ artifact,
406
+ reused: false,
407
+ checks: checkResultsFromGateArtifact(artifact, false)
408
+ };
409
+ }
410
+ /** Stable digest of a log file, for ad-hoc evidence binding. */
411
+ export function digestLogContent(content) {
412
+ return createHash("sha256").update(content).digest("hex");
413
+ }
414
+ export { gateArtifactRef, parseGateArtifactRef };