@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.
- package/ARCHITECTURE.md +11 -6
- package/ARCHITECTURE.zh-CN.md +8 -4
- package/README.md +13 -5
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/artifacts/artifactCommitLock.js +16 -55
- package/dist/artifacts/taskArtifactRepository.js +3 -3
- package/dist/cli/agentConfigurationPicker.js +1 -1
- package/dist/cli/commandCatalog.js +92 -48
- package/dist/cli/completion.js +18 -18
- package/dist/cli/completionWizard.js +1 -1
- package/dist/cli/dynamicCompletion.js +1 -1
- package/dist/cli/interactionPolicy.js +11 -3
- package/dist/cli/invocationAuthority.js +64 -0
- package/dist/cli/managedDiagnostics.js +2 -2
- package/dist/cli/parseRepeatable.js +35 -0
- package/dist/cli/updatePorts.js +26 -5
- package/dist/cli.js +1303 -1235
- package/dist/commands/agentCommands.js +4 -4
- package/dist/commands/capabilityCommands.js +1 -1
- package/dist/commands/configCommands.js +6 -57
- package/dist/commands/configOverview.js +2 -2
- package/dist/commands/durableJobCommands.js +12 -6
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +78 -29
- package/dist/commands/grantCommands.js +0 -3
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/operatorCommands.js +3 -3
- package/dist/commands/projectCommands.js +45 -25
- package/dist/commands/resourcesCommands.js +10 -40
- package/dist/commands/roleConfiguration.js +2 -6
- package/dist/commands/taskActivationCommands.js +4 -8
- package/dist/commands/taskAgentCapabilities.js +9 -0
- package/dist/commands/taskCommandSupport.js +155 -0
- package/dist/commands/taskCommandTypes.js +1 -0
- package/dist/commands/taskCommands.js +355 -732
- package/dist/commands/taskCompletionGate.js +1 -1
- package/dist/commands/taskExecutionCommands.js +1 -1
- package/dist/commands/taskFactCommands.js +325 -0
- package/dist/commands/taskInputCommands.js +12 -42
- package/dist/commands/taskIntegrationCommands.js +20 -58
- package/dist/commands/taskNextActionCommand.js +7 -6
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +12 -3
- package/dist/commands/taskPublicationVerifyCommand.js +24 -40
- package/dist/commands/taskRemoteDeliveryCommand.js +5 -86
- package/dist/commands/taskUpstreamCommands.js +19 -8
- package/dist/commands/workflowCommands.js +6 -1
- package/dist/completion/completionInstaller.js +26 -26
- package/dist/completion/completionState.js +26 -26
- package/dist/completion/fileCompletionManager.js +6 -11
- package/dist/config/configCatalog.js +0 -2
- package/dist/config/timeZone.js +14 -0
- package/dist/config/yuiConfig.js +0 -29
- package/dist/context/runContextPack.js +5 -3
- package/dist/context/taskCatalog.js +185 -0
- package/dist/context/taskContext.js +152 -151
- package/dist/context/wakeRunReferences.js +11 -0
- package/dist/controller/agentCapabilities.js +58 -0
- package/dist/controller/agentRuntimeObserver.js +6 -11
- package/dist/controller/clientRuntime.js +22 -14
- package/dist/controller/controller.js +20 -24
- package/dist/controller/fileSchedulerStoreAdapter.js +191 -225
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/jobClient.js +4 -4
- package/dist/controller/jobControl.js +60 -25
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +85 -43
- package/dist/controller/runtimeEventProcessor.js +0 -5
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/controller/taskAgentError.js +35 -0
- package/dist/coordination/workMailboxQueue.js +2 -24
- package/dist/core/controllerClient.js +2 -2
- package/dist/core/fileLockOwner.js +74 -0
- package/dist/decision/decision.js +17 -0
- package/dist/doctor/doctor.js +18 -9
- package/dist/event/taskEvent.js +12 -0
- package/dist/execution/workItemExecutionProjection.js +2 -2
- package/dist/executor/agentCapabilityConfig.js +43 -0
- package/dist/executor/agentConfigurationCatalog.js +49 -11
- package/dist/executor/agentExecutor.js +5 -8
- package/dist/executor/fileRoleLaunchPlanner.js +11 -53
- package/dist/executor/taskAgentCapabilities.js +64 -0
- package/dist/integration/deliveryObligation.js +1 -69
- package/dist/integration/gitIntegrationService.js +79 -90
- package/dist/integration/integrationAttempt.js +3 -14
- package/dist/integration/integrationSourceApplication.js +4 -12
- package/dist/integration/manifestTags.js +2 -2
- package/dist/job/durableJob.js +4 -25
- package/dist/job/jobAssignmentScope.js +22 -0
- package/dist/job/jobOperation.js +16 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/stepDirectory.js +15 -0
- package/dist/kernel/builtinCapabilities.js +15 -11
- package/dist/kernel/kernelPorts.js +1 -17
- package/dist/lifecycle/exactRunTerminalization.js +5 -10
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/message/inputControlResolution.js +3 -4
- package/dist/message/message.js +17 -20
- package/dist/message/messageContinuation.js +1 -1
- package/dist/milestone/milestone.js +11 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/observability/orchestrationMetrics.js +24 -1
- package/dist/observability/runtimeIdentity.js +1 -17
- package/dist/output/agentConfigurationPresentation.js +2 -0
- package/dist/output/timePresentation.js +1 -14
- package/dist/plugins/pluginService.js +12 -4
- package/dist/release/releaseWorkflowPorts.js +45 -96
- package/dist/release/runtimeRelease.js +9 -15
- package/dist/repository/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +98 -82
- package/dist/repository/taskBaseFreshness.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +82 -144
- package/dist/repository/taskWorkspacePreparer.js +113 -47
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/autoResourceGc.js +4 -77
- package/dist/resources/liveReferences.js +7 -54
- package/dist/resources/resourceDiscovery.js +5 -118
- package/dist/resources/resourceGc.js +233 -181
- package/dist/resources/resourceRegistrar.js +5 -4
- package/dist/resources/resourceRegistry.js +3 -37
- package/dist/resources/resourceRegistryStore.js +13 -36
- package/dist/resources/sqliteResourceRegistry.js +43 -26
- package/dist/review/deltaRecheck.js +1 -1
- package/dist/review/reviewAcceptance.js +1 -1
- package/dist/review/reviewDecision.js +1 -1
- package/dist/review/reviewRound.js +8 -7
- package/dist/review/taskFinalReviewContractResolution.js +1 -1
- package/dist/runtime/acpProtocol.js +4 -51
- package/dist/runtime/acpSession.js +10 -75
- package/dist/runtime/acpSessionConfiguration.js +1 -8
- package/dist/runtime/agentEndpoint.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentFailureContext.js +43 -0
- package/dist/runtime/agentHost.js +42 -25
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/codexInteractiveHost.js +2 -2
- package/dist/runtime/index.js +0 -1
- package/dist/runtime/managedCaller.js +21 -1
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerErrors.js +38 -0
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/runtimeObservation.js +0 -5
- package/dist/runtime/runtimeSessionCandidate.js +2 -3
- package/dist/runtime/sessionOwnerIdentity.js +1 -1
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +10 -43
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +6 -8
- package/dist/scheduler/activeRoleRunDelivery.js +19 -7
- package/dist/scheduler/leaderWakeupProcessor.js +24 -6
- package/dist/scheduler/operatorEvent.js +12 -20
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +5 -8
- package/dist/scheduler/roleRunLiveness.js +4 -4
- package/dist/scheduler/roleRunStall.js +16 -23
- package/dist/scheduler/taskExecutionProjection.js +49 -36
- package/dist/scheduler/taskObservabilityProjection.js +6 -45
- package/dist/scheduler/taskWake.js +5 -10
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/scheduler/wakeupQueue.js +2 -4
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/contextRecords.js +106 -0
- package/dist/storage/currentTaskStore.js +1 -1
- package/dist/storage/homeLayout.js +13 -17
- package/dist/storage/migrations/agentFailureContext.js +22 -0
- package/dist/storage/migrations/currentInputContract.js +86 -0
- package/dist/storage/migrations/currentRuntimeContract.js +228 -0
- package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
- package/dist/storage/migrations/integrationContinuation.js +5 -4
- package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
- package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
- package/dist/storage/migrations/verificationPlanV1.js +162 -0
- package/dist/storage/migrations/verificationPolicy.js +74 -0
- package/dist/storage/migrations/workItemHistory.js +46 -0
- package/dist/storage/persistenceWorker.js +12 -4
- package/dist/storage/recordValidation.js +87 -0
- package/dist/storage/sqliteSchema.js +173 -1
- package/dist/storage/sqliteStore.js +172 -161
- package/dist/storage/storageSchema.js +2 -14
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +16 -7
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +5 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/completionReadiness.js +5 -24
- package/dist/task/draftPlan.js +0 -53
- package/dist/task/nextAction.js +7 -13
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/task/remoteDeliveryService.js +89 -0
- package/dist/task/taskActivation.js +2 -25
- package/dist/task/taskActivationService.js +0 -2
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/task/taskSubmission.js +3 -14
- package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
- package/dist/telemetry/sqliteTelemetryStore.js +49 -49
- package/dist/telemetry/telemetryWiring.js +4 -3
- package/dist/tmux/tmuxManager.js +29 -20
- package/dist/verification/gateArtifact.js +15 -24
- package/dist/verification/gateArtifactStore.js +14 -7
- package/dist/verification/verificationGateService.js +29 -88
- package/dist/verification/verificationPlan.js +27 -72
- package/dist/web/assets/client/app.js +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +6 -5
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +12 -3
- package/dist/web/webSnapshot.js +14 -85
- package/dist/web/webTaskSurface.js +17 -46
- package/dist/workItem/workItem.js +1 -12
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +42 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +33 -3
- package/docs/managed-turn-and-session-runtime.md +121 -1
- package/docs/managed-turn-and-session-runtime.zh-CN.md +96 -2
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/plugin-sdk.md +4 -2
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/provider-runtime.md +3 -1
- package/docs/provider-runtime.zh-CN.md +4 -2
- package/docs/release-workflow.md +190 -7
- package/docs/release-workflow.zh-CN.md +139 -5
- package/docs/roles-and-configuration.md +29 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/sqlite-control-plane-design.md +16 -3
- package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
- package/docs/task-dag-semantics.md +3 -3
- package/docs/task-dag-semantics.zh-CN.md +2 -2
- package/docs/task-delivery.md +223 -17
- package/docs/task-delivery.zh-CN.md +171 -14
- package/docs/task-discovery.md +101 -0
- package/docs/task-discovery.zh-CN.md +85 -0
- package/docs/testing/verification-levels.md +161 -9
- package/docs/testing/verification-levels.zh-CN.md +116 -9
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -0
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-leader/references/integration.md +9 -5
- package/skills/yui-leader/references/planning.md +9 -3
- package/skills/yui-operator/SKILL.md +24 -8
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +71 -0
- package/dist/agent/agentRegistry.js +0 -10
- package/dist/agentRun/runIdentity.js +0 -22
- package/dist/commands/deliveryGuardPreflight.js +0 -30
- package/dist/commands/taskIntegrationQueueCommands.js +0 -226
- package/dist/commands/taskOverviewCommand.js +0 -376
- package/dist/completion/completionWizard.js +0 -125
- package/dist/context/dispatchContext.js +0 -110
- package/dist/context/wakeNotification.js +0 -144
- package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
- package/dist/integration/integrationQueueEntry.js +0 -191
- package/dist/integration/integrationQueueService.js +0 -810
- package/dist/release/fakeReleasePorts.js +0 -55
- package/dist/runtime/sessionOwnerRegistry.js +0 -137
- package/dist/task/deliveryGuard.js +0 -226
- /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
|
|
12
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
});
|
package/dist/tmux/tmuxManager.js
CHANGED
|
@@ -509,7 +509,9 @@ export class TmuxManager {
|
|
|
509
509
|
let output;
|
|
510
510
|
try {
|
|
511
511
|
output = this.run([
|
|
512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
//
|
|
1173
|
-
//
|
|
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
|
|
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.
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
|
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 !== "
|
|
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
|
-
|
|
100
|
+
boundary: artifact.boundary
|
|
110
101
|
})) {
|
|
111
102
|
throw new Error("GateArtifact key does not match its identity tuple.");
|
|
112
103
|
}
|
|
113
|
-
if (artifact.
|
|
104
|
+
if (artifact.boundary === undefined) {
|
|
114
105
|
throw new Error("An L2 GateArtifact requires a target boundary.");
|
|
115
106
|
}
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
37
|
-
|
|
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
|
-
|
|
46
|
+
return null;
|
|
40
47
|
const logs = store.getGateArtifactLogs(artifact.key);
|
|
41
48
|
const verification = verifyGateArtifactLogs(artifact, logs);
|
|
42
|
-
if (verification.ok)
|
|
43
|
-
return
|
|
49
|
+
if (!verification.ok)
|
|
50
|
+
return null;
|
|
44
51
|
}
|
|
45
|
-
return
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
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,
|
|
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");
|