@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.
- package/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- 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 };
|