@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,31 +1,30 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
2
|
import { lstat, mkdir, open, rm, stat } from "node:fs/promises";
|
|
5
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
4
|
import { promisify } from "node:util";
|
|
7
5
|
import { selectEnvironment } from "../agent/launchEnvironment.js";
|
|
8
6
|
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
9
7
|
import { durableJobIdempotencyKey } from "../job/durableJob.js";
|
|
10
|
-
import {
|
|
11
|
-
import { planBootstrapJobSteps, planL2JobSteps } from "../verification/verificationPlan.js";
|
|
12
|
-
import { assertNoAdHocFullSuiteChecks, checkResultsFromGateArtifact, checkResultsFromGateJob, gateIdentityForCandidate, lookupReusableGateArtifact, recordGateArtifactFromJob, recordGateArtifactFromStepOutcomes, resolveVerificationGate, runGateStepsInProcess } from "../verification/verificationGateService.js";
|
|
13
|
-
import { recordGateArtifactPotentialReuse, recordGateArtifactReuse } from "../verification/gateArtifact.js";
|
|
14
|
-
import { touchGateArtifact } from "../verification/gateArtifactStore.js";
|
|
8
|
+
import { readRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
15
9
|
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
16
|
-
import { integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
|
|
17
10
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
18
11
|
import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
|
|
12
|
+
import { integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
|
|
13
|
+
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
19
14
|
import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
|
|
20
|
-
import { managedIntegrationRuntimeRoot } from "../storage/homeLayout.js";
|
|
15
|
+
import { integrationTmuxSocketRoot, managedIntegrationRuntimeRoot } from "../storage/homeLayout.js";
|
|
21
16
|
import { advanceTaskProjectCommit } from "../task/task.js";
|
|
22
17
|
import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
23
|
-
import {
|
|
18
|
+
import { recordGateArtifactReuse } from "../verification/gateArtifact.js";
|
|
19
|
+
import { touchGateArtifact } from "../verification/gateArtifactStore.js";
|
|
20
|
+
import { beginGateVerification, checkResultsFromGateArtifact, checkResultsFromGateJob, gateIdentityForCandidate, lookupReusableGateArtifact, recordGateArtifactFromJob, recordGateArtifactFromStepOutcomes, resolveVerificationGate, runGateStepsInProcess } from "../verification/verificationGateService.js";
|
|
21
|
+
import { planBootstrapJobSteps, planL2JobSteps } from "../verification/verificationPlan.js";
|
|
24
22
|
import { createManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { findReusableIntegrationCheckEvidence, INTEGRATION_RUNTIME_RELEASE_ENV } from "./integrationCheckEvidenceReuse.js";
|
|
23
|
+
import { recordIntegrationCheckJob, updateIntegrationAttempt } from "./integrationAttempt.js";
|
|
24
|
+
import { applyIntegrationSource, assertIntegrationCandidate, assertRecordedSourceCandidate } from "./integrationSourceApplication.js";
|
|
28
25
|
const executeFile = promisify(execFile);
|
|
26
|
+
/** Preserve the exact execution-environment fence in durable Job input digests. */
|
|
27
|
+
const INTEGRATION_RUNTIME_RELEASE_ENV = "YUI_INTEGRATION_RUNTIME_RELEASE_ID";
|
|
29
28
|
const INTEGRATION_OPERATIONAL_ENVIRONMENT_NAMES = [
|
|
30
29
|
"PATH",
|
|
31
30
|
"USER",
|
|
@@ -76,7 +75,7 @@ export class GitIntegrationService {
|
|
|
76
75
|
#resourceRegistrar() {
|
|
77
76
|
return this.#resourceRegistrarValue ??= new ResourceRegistrar(this.home, this.now);
|
|
78
77
|
}
|
|
79
|
-
async integrate(taskId, integrationId) {
|
|
78
|
+
async integrate(taskId, integrationId, signal) {
|
|
80
79
|
let initial = requireIntegration(this.store, taskId, integrationId);
|
|
81
80
|
if (!["running", "conflicted", "blocked", "validating"].includes(initial.status)) {
|
|
82
81
|
throw new Error(`Integration cannot continue from ${initial.status}.`);
|
|
@@ -86,7 +85,7 @@ export class GitIntegrationService {
|
|
|
86
85
|
// CAS. A concurrent `project migrate` must not switch the catalog path or
|
|
87
86
|
// remove the old checkout mid-Integration, so the per-Project maintenance
|
|
88
87
|
// fence is held across every Git effect and released only on exit.
|
|
89
|
-
const release = acquireProjectMaintenanceLocks(this.home, [initial.projectId]);
|
|
88
|
+
const release = await acquireProjectMaintenanceLocks(this.home, [initial.projectId], { signal });
|
|
90
89
|
try {
|
|
91
90
|
// A concurrent abort may have settled the attempt while this caller was
|
|
92
91
|
// acquiring the same Project fence. Never execute a pre-fence snapshot.
|
|
@@ -260,9 +259,9 @@ export class GitIntegrationService {
|
|
|
260
259
|
/** Abandon under the same fence as Git/CAS, not just a status whitelist.
|
|
261
260
|
* A delivered validating candidate is recorded as committed; a provably
|
|
262
261
|
* unadvanced one may be abandoned without requiring obsolete check settings. */
|
|
263
|
-
async abort(taskId, integrationId, reason, authorize) {
|
|
262
|
+
async abort(taskId, integrationId, reason, authorize, signal) {
|
|
264
263
|
const initial = requireIntegration(this.store, taskId, integrationId);
|
|
265
|
-
const release = acquireProjectMaintenanceLocks(this.home, [initial.projectId]);
|
|
264
|
+
const release = await acquireProjectMaintenanceLocks(this.home, [initial.projectId], { signal });
|
|
266
265
|
try {
|
|
267
266
|
const attempt = requireIntegration(this.store, taskId, integrationId);
|
|
268
267
|
authorize(this.store, taskId);
|
|
@@ -338,12 +337,25 @@ export class GitIntegrationService {
|
|
|
338
337
|
release();
|
|
339
338
|
}
|
|
340
339
|
}
|
|
341
|
-
async cleanup(integration) {
|
|
340
|
+
async cleanup(integration, options = {}) {
|
|
342
341
|
// The whole cleanup is one Git transaction against the Project's
|
|
343
342
|
// repository, mirroring integrate(): a concurrent `project migrate` must
|
|
344
343
|
// not switch the catalog path or remove the old checkout mid-cleanup.
|
|
345
|
-
const release = acquireProjectMaintenanceLocks(this.home, [integration.projectId]);
|
|
344
|
+
const release = await acquireProjectMaintenanceLocks(this.home, [integration.projectId], { signal: options.signal });
|
|
346
345
|
try {
|
|
346
|
+
options.authorize?.();
|
|
347
|
+
const current = requireIntegration(this.store, integration.taskId, integration.id);
|
|
348
|
+
if (current.projectId !== integration.projectId
|
|
349
|
+
|| !["committed", "superseded", "failed"].includes(current.status)) {
|
|
350
|
+
throw new Error("Integration changed before cleanup; read its current record.");
|
|
351
|
+
}
|
|
352
|
+
integration = current;
|
|
353
|
+
const activeJob = this.store.listDurableJobs(integration.taskId).find(job => job.owner.kind === "integration-attempt" && job.owner.integrationAttemptId === integration.id
|
|
354
|
+
&& (job.status === "queued" || job.status === "running"
|
|
355
|
+
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
|
|
356
|
+
if (activeJob !== undefined) {
|
|
357
|
+
throw new Error(`Integration ${integration.id} has an active DurableJob: ${activeJob.id}/${activeJob.status}.`);
|
|
358
|
+
}
|
|
347
359
|
const task = this.store.getTask(integration.taskId);
|
|
348
360
|
if (task === null || !task.projectBindings.some(({ projectId }) => projectId === integration.projectId)) {
|
|
349
361
|
throw new Error(`Integration Task Project is unavailable: ${integration.id}.`);
|
|
@@ -408,26 +420,7 @@ export class GitIntegrationService {
|
|
|
408
420
|
}
|
|
409
421
|
await assertIntegrationCandidate(path, attempt.candidateCommit, workspace.branch);
|
|
410
422
|
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit);
|
|
411
|
-
const { runtime, head, steps, environment, inputDigest
|
|
412
|
-
const ownedJobs = this.store.listDurableJobs(attempt.taskId).filter(job => job.owner.kind === "integration-attempt" && job.owner.integrationAttemptId === attempt.id);
|
|
413
|
-
if (gate === undefined && releaseId !== null && attempt.checkInputDigest === undefined
|
|
414
|
-
&& ownedJobs.length === 0 && attempt.jobId === undefined) {
|
|
415
|
-
const reusable = findReusableIntegrationCheckEvidence({
|
|
416
|
-
taskId: attempt.taskId,
|
|
417
|
-
projectId: attempt.projectId,
|
|
418
|
-
currentAttemptId: attempt.id,
|
|
419
|
-
candidateCommit: head,
|
|
420
|
-
checkCommands: attempt.checkCommands,
|
|
421
|
-
runtimeReleaseId: releaseId,
|
|
422
|
-
attempts: this.store.listIntegrationAttempts(attempt.taskId),
|
|
423
|
-
jobs: this.store.listDurableJobs(attempt.taskId),
|
|
424
|
-
logExists: (homeRelativePath) => existsSync(join(this.home, homeRelativePath)),
|
|
425
|
-
logPathFor: (job, relativeLogPath) => (join(job.artifactsLocator, "logs", relativeLogPath))
|
|
426
|
-
});
|
|
427
|
-
if (reusable !== null) {
|
|
428
|
-
return this.#finalizeGateSuccess(updateIntegrationAttempt(attempt, { checkInputDigest: inputDigest }, this.now()), workspace, repositoryPath, head, [...reusable.checks]);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
423
|
+
const { runtime, head, steps, environment, inputDigest } = await this.#checkSpecification(attempt, managedWorkspace, gate);
|
|
431
424
|
if (attempt.checkInputDigest !== undefined && attempt.checkInputDigest !== inputDigest) {
|
|
432
425
|
throw new Error("Integration check conditions changed since admission; inspect the original Job, then abort or start a new attempt.");
|
|
433
426
|
}
|
|
@@ -444,6 +437,14 @@ export class GitIntegrationService {
|
|
|
444
437
|
})
|
|
445
438
|
}, this.now());
|
|
446
439
|
this.store.saveIntegrationAttempt(attempt.taskId, persisted);
|
|
440
|
+
if (gate !== undefined) {
|
|
441
|
+
// Resumption can enter here after persisting admission but before the
|
|
442
|
+
// original call withdrew old proof or recorded its Job id.
|
|
443
|
+
beginGateVerification(this.store, gateIdentityForCandidate({
|
|
444
|
+
projectId: attempt.projectId, gate, level: "L2", commit: head,
|
|
445
|
+
targetRef: attempt.targetRef, baseHead: attempt.beforeCommit
|
|
446
|
+
}), gate.plan, this.now());
|
|
447
|
+
}
|
|
447
448
|
// All domain admission and durable candidate identity precede Job effects.
|
|
448
449
|
this.runtimeIsolation.activate(runtime);
|
|
449
450
|
await prepareIntegrationRuntimeHome(runtime, this.home);
|
|
@@ -507,7 +508,7 @@ export class GitIntegrationService {
|
|
|
507
508
|
baseHead: attempt.beforeCommit
|
|
508
509
|
});
|
|
509
510
|
try {
|
|
510
|
-
const artifact = await recordGateArtifactFromJob(this.store, this.home, identity, gate.plan, job, this.now());
|
|
511
|
+
const artifact = await this.#publishGateEvidence(workspace, job.head, () => recordGateArtifactFromJob(this.store, this.home, identity, gate.plan, job, this.now()));
|
|
511
512
|
if (checks.every((check) => check.outcome !== "failed")) {
|
|
512
513
|
checks.push(...checkResultsFromGateArtifact(artifact));
|
|
513
514
|
}
|
|
@@ -549,11 +550,10 @@ export class GitIntegrationService {
|
|
|
549
550
|
/**
|
|
550
551
|
* Issue 08: the VerificationPlan gate for a configured Project.
|
|
551
552
|
*
|
|
552
|
-
*
|
|
553
|
-
* returns an existing successful artifact for the same identity tuple
|
|
553
|
+
* Returns an existing successful artifact for the same identity tuple
|
|
554
554
|
* (project + exact commit + plan digest + toolchain digest + target
|
|
555
|
-
* boundary)
|
|
556
|
-
*
|
|
555
|
+
* boundary), unless this attempt explicitly requests fresh checks.
|
|
556
|
+
* The gate itself runs as bootstrap + L2 DurableJob steps (or
|
|
557
557
|
* in-process when no Controller job port is available) and records a
|
|
558
558
|
* self-contained GateArtifact. The final CAS in
|
|
559
559
|
* {@link #finalizeGateSuccess} still fences a target that moves during the
|
|
@@ -561,19 +561,26 @@ export class GitIntegrationService {
|
|
|
561
561
|
*/
|
|
562
562
|
async #runVerificationGate(attempt, workspace, path, managedWorkspace, repositoryPath, gate) {
|
|
563
563
|
const spec = await this.#checkSpecification(attempt, managedWorkspace, gate);
|
|
564
|
+
const unsettled = this.store.listIntegrationAttempts(attempt.taskId).find(other => {
|
|
565
|
+
if (other.id === attempt.id || other.projectId !== attempt.projectId
|
|
566
|
+
|| other.targetRef !== attempt.targetRef || other.beforeCommit !== attempt.beforeCommit
|
|
567
|
+
|| other.candidateCommit !== attempt.candidateCommit
|
|
568
|
+
|| other.gatePlanDigest !== gate.planDigest || other.gateToolchainDigest !== gate.toolchainDigest)
|
|
569
|
+
return false;
|
|
570
|
+
if (other.status === "running" || other.status === "validating")
|
|
571
|
+
return true;
|
|
572
|
+
const job = other.jobId === undefined ? null : this.store.getDurableJob(other.taskId, other.jobId);
|
|
573
|
+
return job !== null && (job.status === "queued" || job.status === "running"
|
|
574
|
+
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined));
|
|
575
|
+
});
|
|
576
|
+
if (unsettled !== undefined) {
|
|
577
|
+
throw new Error(`Exact verification is unsettled in ${unsettled.taskId}/${unsettled.id}; continue or settle its original Job before requesting another check.`);
|
|
578
|
+
}
|
|
564
579
|
attempt = updateIntegrationAttempt(attempt, {
|
|
565
580
|
checkInputDigest: spec.inputDigest,
|
|
566
581
|
gatePlanDigest: gate.planDigest, gateToolchainDigest: gate.toolchainDigest
|
|
567
582
|
}, this.now());
|
|
568
583
|
this.store.saveIntegrationAttempt(attempt.taskId, attempt);
|
|
569
|
-
if (gate.mode === "enforce") {
|
|
570
|
-
try {
|
|
571
|
-
assertNoAdHocFullSuiteChecks(gate.plan, attempt.checkCommands);
|
|
572
|
-
}
|
|
573
|
-
catch (error) {
|
|
574
|
-
return this.#fail(attempt, error instanceof Error ? error : new Error(String(error)), "verification-plan", workspace);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
584
|
const candidateCommit = await gitLine(["-C", path, "rev-parse", "HEAD^{commit}"]);
|
|
578
585
|
const identity = gateIdentityForCandidate({
|
|
579
586
|
projectId: attempt.projectId,
|
|
@@ -583,7 +590,7 @@ export class GitIntegrationService {
|
|
|
583
590
|
targetRef: attempt.targetRef,
|
|
584
591
|
baseHead: attempt.beforeCommit
|
|
585
592
|
});
|
|
586
|
-
if (
|
|
593
|
+
if (!attempt.rerunChecks && attempt.checkCommands.length === 0) {
|
|
587
594
|
const existing = await lookupReusableGateArtifact(this.store, identity);
|
|
588
595
|
if (existing !== null) {
|
|
589
596
|
touchGateArtifact(this.store, recordGateArtifactReuse(existing, this.now()));
|
|
@@ -591,31 +598,22 @@ export class GitIntegrationService {
|
|
|
591
598
|
return this.#finalizeGateSuccess(attempt, workspace, repositoryPath, candidateCommit, checks);
|
|
592
599
|
}
|
|
593
600
|
}
|
|
594
|
-
else {
|
|
595
|
-
// Record mode: observe the potential reuse without skipping the gate.
|
|
596
|
-
const existing = await lookupReusableGateArtifact(this.store, identity);
|
|
597
|
-
if (existing !== null) {
|
|
598
|
-
touchGateArtifact(this.store, recordGateArtifactPotentialReuse(existing, this.now()));
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
601
|
if (this.jobPort !== undefined) {
|
|
602
602
|
return this.#startCheckJob(attempt, workspace, path, managedWorkspace, repositoryPath, gate);
|
|
603
603
|
}
|
|
604
|
-
|
|
604
|
+
beginGateVerification(this.store, identity, gate.plan, this.now());
|
|
605
|
+
// An explicit local caller without a Controller Job port runs the
|
|
605
606
|
// plan gate in-process and record the artifact directly.
|
|
606
607
|
const runtime = this.#runtimePreparation(attempt, managedWorkspace);
|
|
607
608
|
this.runtimeIsolation.activate(runtime);
|
|
608
609
|
let cleanupReason = "failure";
|
|
609
610
|
try {
|
|
610
611
|
await prepareIntegrationRuntimeHome(runtime, this.home);
|
|
611
|
-
const steps =
|
|
612
|
-
...planBootstrapJobSteps(gate.plan),
|
|
613
|
-
...planL2JobSteps(gate.plan)
|
|
614
|
-
];
|
|
612
|
+
const steps = spec.steps;
|
|
615
613
|
const outcomes = await runGateStepsInProcess(path, steps, spec.environment, integrationCheckDirectory(this.home, attempt.taskId, attempt.id), candidateCommit);
|
|
616
614
|
const succeeded = outcomes.length === steps.length
|
|
617
615
|
&& outcomes.every((outcome) => outcome.exitCode === 0 && outcome.signal === null && !outcome.timedOut);
|
|
618
|
-
const artifact = await recordGateArtifactFromStepOutcomes(this.store, identity, gate.plan, outcomes, succeeded, this.now());
|
|
616
|
+
const artifact = await this.#publishGateEvidence(workspace, candidateCommit, () => recordGateArtifactFromStepOutcomes(this.store, identity, gate.plan, outcomes, succeeded, this.now()));
|
|
619
617
|
const checks = checkResultsFromGateArtifact(artifact);
|
|
620
618
|
cleanupReason = succeeded ? "completion" : "failure";
|
|
621
619
|
if (!succeeded) {
|
|
@@ -629,6 +627,13 @@ export class GitIntegrationService {
|
|
|
629
627
|
this.runtimeIsolation.cleanup(runtime, cleanupReason);
|
|
630
628
|
}
|
|
631
629
|
}
|
|
630
|
+
/** Both executors publish proof only for the exact clean candidate they
|
|
631
|
+
* checked. A later delivery/CAS failure is separate from this evidence, but
|
|
632
|
+
* a mutated candidate must never enter the reusable-success cache. */
|
|
633
|
+
async #publishGateEvidence(workspace, candidateCommit, publish) {
|
|
634
|
+
await assertIntegrationCandidate(workspace.path, candidateCommit, workspace.branch);
|
|
635
|
+
return publish();
|
|
636
|
+
}
|
|
632
637
|
async #finalizeGateSuccess(attempt, workspace, repositoryPath, candidateCommit, checks) {
|
|
633
638
|
await assertIntegrationCandidate(workspace.path, candidateCommit, workspace.branch);
|
|
634
639
|
const validating = updateIntegrationAttempt(attempt, {
|
|
@@ -641,7 +646,7 @@ export class GitIntegrationService {
|
|
|
641
646
|
const committed = this.#recordCommitted(validating);
|
|
642
647
|
return this.#terminalResult("committed", committed, workspace);
|
|
643
648
|
}
|
|
644
|
-
#runtimePreparation(
|
|
649
|
+
#runtimePreparation(_attempt, workspace) {
|
|
645
650
|
return this.runtimeIsolation.preflight({
|
|
646
651
|
workspace,
|
|
647
652
|
allowExactActive: true
|
|
@@ -655,11 +660,12 @@ export class GitIntegrationService {
|
|
|
655
660
|
const head = attempt.candidateCommit;
|
|
656
661
|
if (head === undefined)
|
|
657
662
|
throw new Error("Integration check specification requires a candidate.");
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
: [...planBootstrapJobSteps(gate.plan
|
|
663
|
+
const explicitChecks = attempt.checkCommands.map((command, index) => ({
|
|
664
|
+
name: `check-${index + 1}`, command, timeoutMs: 30 * 60_000
|
|
665
|
+
}));
|
|
666
|
+
const steps = gate === undefined ? explicitChecks
|
|
667
|
+
: [...planBootstrapJobSteps(gate.plan, managedWorkspace.root),
|
|
668
|
+
...planL2JobSteps(gate.plan, managedWorkspace.root), ...explicitChecks]
|
|
663
669
|
.map(step => ({ ...step, timeoutMs: 30 * 60_000 }));
|
|
664
670
|
const releaseId = integrationRuntimeReleaseIdentity(this.home);
|
|
665
671
|
const environment = Object.freeze({
|
|
@@ -671,7 +677,7 @@ export class GitIntegrationService {
|
|
|
671
677
|
projectId: attempt.projectId, head, workspace: managedWorkspace.root,
|
|
672
678
|
env: environment, steps
|
|
673
679
|
});
|
|
674
|
-
return { runtime, head, steps, environment, inputDigest
|
|
680
|
+
return { runtime, head, steps, environment, inputDigest };
|
|
675
681
|
}
|
|
676
682
|
async #recoverValidating(attempt, workspace, managedWorkspace, repositoryPath) {
|
|
677
683
|
if (attempt.candidateCommit === undefined || attempt.checks === undefined) {
|
|
@@ -792,11 +798,6 @@ function integrationSummary(attempt) {
|
|
|
792
798
|
? `Upstream ${attempt.source.remoteCommit} was already represented; Task head was unchanged.`
|
|
793
799
|
: `Rebased Task changes onto upstream ${attempt.source.remoteCommit}.`;
|
|
794
800
|
}
|
|
795
|
-
if (attempt.source.kind === "historical-change-sets") {
|
|
796
|
-
return unchanged
|
|
797
|
-
? "Historical Integration was already represented; Task head was unchanged."
|
|
798
|
-
: "Historical Integration advanced the Task head.";
|
|
799
|
-
}
|
|
800
801
|
if (attempt.source.strategy === "manual") {
|
|
801
802
|
const decision = attempt.resolution;
|
|
802
803
|
if (decision?.action !== "manual-resolution") {
|
|
@@ -1023,18 +1024,6 @@ function defaultIntegrationRuntimeIsolation(home, homeId) {
|
|
|
1023
1024
|
}
|
|
1024
1025
|
});
|
|
1025
1026
|
}
|
|
1026
|
-
/**
|
|
1027
|
-
* Short `/tmp` directory that holds ONLY the integration check's tmux socket
|
|
1028
|
-
* endpoint. tmux uses a `sockaddr_un` path whose length budget cannot absorb a
|
|
1029
|
-
* deep Home path, so this single IPC endpoint is the one approved exception to
|
|
1030
|
-
* the unified-Home contract. All other integration runtime state (data, cache,
|
|
1031
|
-
* ordinary temp) lives in the Home partition from `managedIntegrationRuntimeRoot`.
|
|
1032
|
-
*/
|
|
1033
|
-
function integrationTmuxSocketRoot(home) {
|
|
1034
|
-
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
1035
|
-
const homeDigest = createHash("sha256").update(resolve(home)).digest("hex").slice(0, 16);
|
|
1036
|
-
return join("/tmp", `yi-${uid.toString(36)}-${homeDigest}`);
|
|
1037
|
-
}
|
|
1038
1027
|
/**
|
|
1039
1028
|
* Reuse is enabled only for an immutable installed release. Development
|
|
1040
1029
|
* checkouts have no content-addressed release identity, so they return.
|
|
@@ -12,6 +12,7 @@ export function createIntegrationAttempt(input, now) {
|
|
|
12
12
|
source: input.source,
|
|
13
13
|
beforeCommit: input.beforeCommit,
|
|
14
14
|
checkCommands: normalizedUniqueText(input.checkCommands ?? [], "Integration check command"),
|
|
15
|
+
rerunChecks: input.rerunChecks ?? false,
|
|
15
16
|
status: "running",
|
|
16
17
|
createdAt: timestamp,
|
|
17
18
|
updatedAt: timestamp
|
|
@@ -143,6 +144,8 @@ export function validateIntegrationAttempt(attempt) {
|
|
|
143
144
|
requireCommit(attempt.beforeCommit, "Integration before commit");
|
|
144
145
|
validateIntegrationSource(attempt.taskId, attempt.source);
|
|
145
146
|
normalizedUniqueText(attempt.checkCommands, "Integration check command");
|
|
147
|
+
if (typeof attempt.rerunChecks !== "boolean")
|
|
148
|
+
throw new Error("Integration rerunChecks must be explicit.");
|
|
146
149
|
if (attempt.candidateCommit !== undefined) {
|
|
147
150
|
requireCommit(attempt.candidateCommit, "Integration candidate commit");
|
|
148
151
|
}
|
|
@@ -239,20 +242,6 @@ function validateIntegrationSource(taskId, source) {
|
|
|
239
242
|
}
|
|
240
243
|
return;
|
|
241
244
|
}
|
|
242
|
-
if (source.kind === "historical-change-sets") {
|
|
243
|
-
if (!Array.isArray(source.changeSetIds) || source.changeSetIds.length === 0) {
|
|
244
|
-
throw new Error("Historical Integration source requires ChangeSet ids.");
|
|
245
|
-
}
|
|
246
|
-
const seen = new Set();
|
|
247
|
-
for (const changeSetId of source.changeSetIds) {
|
|
248
|
-
validateTaskRecordReference({ taskId, localId: changeSetId }, "changeSet");
|
|
249
|
-
if (seen.has(changeSetId)) {
|
|
250
|
-
throw new Error(`Historical Integration ChangeSet is duplicated: ${changeSetId}.`);
|
|
251
|
-
}
|
|
252
|
-
seen.add(changeSetId);
|
|
253
|
-
}
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
245
|
throw new Error("Integration source is invalid.");
|
|
257
246
|
}
|
|
258
247
|
function normalizeConflictReport(report) {
|
|
@@ -33,9 +33,6 @@ export async function applyIntegrationSource(input) {
|
|
|
33
33
|
});
|
|
34
34
|
return pending;
|
|
35
35
|
};
|
|
36
|
-
if (source.kind === "historical-change-sets") {
|
|
37
|
-
throw new Error("Historical Integration source has no current application proof; preserve evidence and abort explicitly.");
|
|
38
|
-
}
|
|
39
36
|
if (source.kind === "work-item" && source.strategy === "manual") {
|
|
40
37
|
if (current.candidateCommit !== undefined) {
|
|
41
38
|
await assertIntegrationCandidate(path, current.candidateCommit);
|
|
@@ -61,15 +58,11 @@ export async function applyIntegrationSource(input) {
|
|
|
61
58
|
const branch = workspace.branch.startsWith("refs/") ? workspace.branch : `refs/heads/${workspace.branch}`;
|
|
62
59
|
const active = await activeGitOperation(path);
|
|
63
60
|
if (current.sourceProgress === undefined) {
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// without a receipt are deliberately not inferred from a clean tree.
|
|
61
|
+
// Only a pristine candidate can establish this attempt's first receipt.
|
|
62
|
+
// An unfinished operation without that receipt belongs to explicit recovery.
|
|
67
63
|
const head = await line(path, "rev-parse", "HEAD");
|
|
68
64
|
if (active !== undefined) {
|
|
69
|
-
|
|
70
|
-
throw new Error("Historical cherry-pick has no durable completed-step cursor; preserve it and choose explicit recovery.");
|
|
71
|
-
}
|
|
72
|
-
await assertOperation(path, kind, commits[0], current.beforeCommit, branch, source);
|
|
65
|
+
throw new Error("Active Git operation has no attempt-owned progress receipt; preserve it and choose explicit recovery.");
|
|
73
66
|
}
|
|
74
67
|
else if (head !== current.beforeCommit || current.status === "conflicted") {
|
|
75
68
|
throw new Error("Integration source completion is unproven: no attempt-owned Git receipt. Preserve the candidate; do not replay.");
|
|
@@ -79,8 +72,7 @@ export async function applyIntegrationSource(input) {
|
|
|
79
72
|
}
|
|
80
73
|
save({ sourceProgress: {
|
|
81
74
|
workspace: resolve(path), branch, sourceDigest, completedSteps: 0,
|
|
82
|
-
head: current.beforeCommit
|
|
83
|
-
...(active === undefined ? {} : { activeAction: actionId() })
|
|
75
|
+
head: current.beforeCommit
|
|
84
76
|
} });
|
|
85
77
|
}
|
|
86
78
|
let progress = current.sourceProgress;
|
|
@@ -77,7 +77,7 @@ function isCommandPath(lowerPath, segments, lowerFile) {
|
|
|
77
77
|
|| lowerFile.startsWith("cli")
|
|
78
78
|
|| lowerPath.includes("/bin/");
|
|
79
79
|
}
|
|
80
|
-
function isContractPath(lowerPath,
|
|
80
|
+
function isContractPath(lowerPath, _segments, lowerFile) {
|
|
81
81
|
return lowerFile === "index.ts"
|
|
82
82
|
|| lowerFile === "index.js"
|
|
83
83
|
|| lowerFile === "index.mjs"
|
|
@@ -87,7 +87,7 @@ function isContractPath(lowerPath, segments, lowerFile) {
|
|
|
87
87
|
|| lowerPath.includes("/contract/")
|
|
88
88
|
|| lowerPath.includes("/contracts/");
|
|
89
89
|
}
|
|
90
|
-
function isTestPath(
|
|
90
|
+
function isTestPath(_lowerPath, segments, lowerFile) {
|
|
91
91
|
if (segments.includes("test") || segments.includes("tests") || segments.includes("__tests__")) {
|
|
92
92
|
return true;
|
|
93
93
|
}
|
package/dist/job/durableJob.js
CHANGED
|
@@ -16,27 +16,9 @@ export function isDurableJobTerminal(status) {
|
|
|
16
16
|
}
|
|
17
17
|
export function createDurableJob(input, now) {
|
|
18
18
|
const timestamp = now.toISOString();
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
projectId: input.projectId,
|
|
23
|
-
head: input.head,
|
|
24
|
-
steps: input.steps,
|
|
25
|
-
workspace: input.workspace,
|
|
26
|
-
env: input.env
|
|
27
|
-
})
|
|
28
|
-
: retryDurableJobIdempotencyKey(durableJobIdempotencyKey({
|
|
29
|
-
owner: input.owner,
|
|
30
|
-
projectId: input.projectId,
|
|
31
|
-
head: input.head,
|
|
32
|
-
steps: input.steps,
|
|
33
|
-
workspace: input.workspace,
|
|
34
|
-
env: input.env
|
|
35
|
-
}), input.retryOf);
|
|
36
|
-
const idempotencyKey = input.operation === undefined ? contentKey
|
|
37
|
-
: createHash("sha256").update(JSON.stringify([
|
|
38
|
-
input.operation.actorId, input.operation.requestId
|
|
39
|
-
])).digest("hex");
|
|
19
|
+
const idempotencyKey = createHash("sha256").update(JSON.stringify([
|
|
20
|
+
input.operation.actorId, input.operation.requestId
|
|
21
|
+
])).digest("hex");
|
|
40
22
|
return validateDurableJob({
|
|
41
23
|
schemaVersion: CURRENT_DURABLE_JOB_SCHEMA_VERSION,
|
|
42
24
|
id: input.id,
|
|
@@ -49,10 +31,7 @@ export function createDurableJob(input, now) {
|
|
|
49
31
|
steps: input.steps.map((step) => ({ ...step })),
|
|
50
32
|
idempotencyKey,
|
|
51
33
|
operation: {
|
|
52
|
-
|
|
53
|
-
inputDigest: input.operation?.inputDigest ?? idempotencyKey,
|
|
54
|
-
actorId: input.operation?.actorId ?? "internal:job",
|
|
55
|
-
authorityRef: input.operation?.authorityRef ?? "internal:job",
|
|
34
|
+
...input.operation,
|
|
56
35
|
targetId: input.workspace,
|
|
57
36
|
capability: "job.start",
|
|
58
37
|
implementation: JOB_RUNNER_IMPLEMENTATION,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
export class JobAssignmentScopeError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/** Authentication identifies the current Session; it does not grant another
|
|
5
|
+
* Assignment's workspace. Share this check across admission, management and
|
|
6
|
+
* the last pre-spawn boundary. Existing running Jobs settle independently. */
|
|
7
|
+
export function assertJobAssignmentScope(store, target, caller) {
|
|
8
|
+
if (caller.scope !== "task" || caller.role === "leader")
|
|
9
|
+
return;
|
|
10
|
+
const run = caller.role === undefined ? null : store.getActiveRun(target.taskId, caller.role);
|
|
11
|
+
const workspace = run?.workspace ?? run?.effective.workspace;
|
|
12
|
+
const item = run?.workItemId === undefined ? null : store.getWorkItem(target.taskId, run.workItemId);
|
|
13
|
+
if (run?.status !== "active" || run.reviewRoundId !== undefined
|
|
14
|
+
|| target.owner.kind !== "work-item" || target.owner.workItemId !== run.workItemId
|
|
15
|
+
|| item?.status !== "open" || item.assignee !== caller.role
|
|
16
|
+
|| !item.writeProjectIds.includes(target.projectId)
|
|
17
|
+
|| !run.effective.writeProjectIds.includes(target.projectId)
|
|
18
|
+
|| workspace === undefined || resolve(workspace.root) !== resolve(target.workspace)
|
|
19
|
+
|| !workspace.entries.some(entry => entry.projectId === target.projectId && entry.access === "write")) {
|
|
20
|
+
throw new JobAssignmentScopeError("Job is outside the caller's current Assignment. Use the exact assigned WorkItem workspace and writable Project; this Job owner cannot represent a Review or replica Lane.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A read model over the one Job, never separately persisted.
|
|
2
|
+
* confirmed means the selected runner was observed, not that every action
|
|
3
|
+
* performed by an arbitrary command succeeded. Inspect original step receipts.
|
|
4
|
+
*/
|
|
5
|
+
export function inspectJobOperation(job) {
|
|
6
|
+
return {
|
|
7
|
+
operationRef: { taskId: job.taskId, jobId: job.id },
|
|
8
|
+
...job.operation,
|
|
9
|
+
state: job.status === "queued" ? "pending"
|
|
10
|
+
: job.status === "running" ? "running"
|
|
11
|
+
: job.status === "unknown-needs-attention" ? "unknown" : "finished",
|
|
12
|
+
outcome: job.result?.outcome,
|
|
13
|
+
result: job.result,
|
|
14
|
+
checkpoint: job.checkpoint
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/job/jobRunner.js
CHANGED
|
@@ -11,6 +11,7 @@ import { spawn } from "node:child_process";
|
|
|
11
11
|
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, utimesSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { join, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { jobStepDirectory } from "./stepDirectory.js";
|
|
14
15
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
15
16
|
import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
|
|
16
17
|
const DEFAULT_STEP_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -129,7 +130,7 @@ export async function runDurableJobRunner(specPath) {
|
|
|
129
130
|
// Issue 08: a structured argv step executes without a shell so its
|
|
130
131
|
// tokens can never be reinterpreted; a legacy command step keeps the
|
|
131
132
|
// shell form. A per-step cwd/env override applies to either form.
|
|
132
|
-
const stepCwd = step.cwd
|
|
133
|
+
const stepCwd = jobStepDirectory(spec.workspace, step.cwd);
|
|
133
134
|
const stepEnv = step.env === undefined
|
|
134
135
|
? spec.env
|
|
135
136
|
: { ...spec.env, ...step.env };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
/** Recheck at execution, including directories created by a bootstrap step. */
|
|
4
|
+
export function jobStepDirectory(workspace, override) {
|
|
5
|
+
const root = realpathSync(workspace);
|
|
6
|
+
const cwd = override ?? workspace;
|
|
7
|
+
if (!isAbsolute(cwd))
|
|
8
|
+
throw new Error("Job step cwd must be absolute.");
|
|
9
|
+
const actual = realpathSync(cwd);
|
|
10
|
+
const nested = relative(root, actual);
|
|
11
|
+
if (nested === ".." || nested.startsWith(`..${sep}`) || isAbsolute(nested)) {
|
|
12
|
+
throw new Error(`Job step cwd is outside its managed workspace: ${resolve(cwd)}.`);
|
|
13
|
+
}
|
|
14
|
+
return actual;
|
|
15
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { listArtifactsCapability, readArtifactCapability, saveArtifactCapability } from "../artifacts/artifactCapability.js";
|
|
2
2
|
import { runConfigCommand } from "../commands/configCommands.js";
|
|
3
|
-
import {
|
|
3
|
+
import { sendTaskMessageCommand, updateTaskMetadataCommand } from "../commands/taskCommands.js";
|
|
4
4
|
import { CONFIG_DOMAINS } from "../config/configCatalog.js";
|
|
5
|
+
import { inspectTaskContext, readTaskContext, readTaskContextDelta, withContextObservations } from "../context/taskContext.js";
|
|
5
6
|
import { createJobCallAuthority, parseDurableJobStartParams } from "../controller/jobControl.js";
|
|
6
|
-
import { inspectJobOperation } from "
|
|
7
|
-
import {
|
|
8
|
-
import { createProjectResources } from "../resources/projectResourceService.js";
|
|
9
|
-
import { saveArtifactCapability, readArtifactCapability, listArtifactsCapability } from "../artifacts/artifactCapability.js";
|
|
7
|
+
import { inspectJobOperation } from "../job/jobOperation.js";
|
|
8
|
+
import { assertJobAssignmentScope } from "../job/jobAssignmentScope.js";
|
|
10
9
|
import { createPluginService } from "../plugins/pluginService.js";
|
|
10
|
+
import { createProjectResources } from "../resources/projectResourceService.js";
|
|
11
|
+
import { CapabilityRegistry, } from "./capabilityRegistry.js";
|
|
11
12
|
const text = { type: "string", minLength: 1 };
|
|
12
13
|
const strings = { type: "object", additionalProperties: { type: "string" } };
|
|
13
14
|
const object = (properties, required = Object.keys(properties)) => ({
|
|
@@ -26,7 +27,6 @@ const definitions = [
|
|
|
26
27
|
effect: "local-mutation", requiredPermissions: ["task:read"], source: "sendTaskMessageCommand",
|
|
27
28
|
inputSchema: object({ taskId: text, body: text,
|
|
28
29
|
intent: { enum: ["record", "discuss", "develop"] },
|
|
29
|
-
wakePolicy: { enum: ["leader", "none"] },
|
|
30
30
|
recipient: object({ roleName: text, workItemId: text, reviewRoundId: text }, ["roleName"])
|
|
31
31
|
}, ["taskId", "body"]), outputSchema: recordOutput
|
|
32
32
|
},
|
|
@@ -170,7 +170,8 @@ const definitions = [
|
|
|
170
170
|
] },
|
|
171
171
|
env: strings,
|
|
172
172
|
steps: { type: "array", minItems: 1, items: object({
|
|
173
|
-
name: text, command: text, timeoutMs: { type: "integer" }
|
|
173
|
+
name: text, command: text, timeoutMs: { type: "integer" },
|
|
174
|
+
argv: { type: "array", minItems: 1, items: text }, cwd: text, env: strings
|
|
174
175
|
}, ["name", "command"]) },
|
|
175
176
|
retryOf: text
|
|
176
177
|
}, ["taskId", "projectId", "head", "workspace", "owner", "env", "steps"]),
|
|
@@ -247,7 +248,7 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
247
248
|
// gains develop from an intent argument; only a global Operator Session
|
|
248
249
|
// (user-authority) may carry one. sendTaskMessageCommand enforces this,
|
|
249
250
|
// so the capability just forwards the optional intent unchanged.
|
|
250
|
-
const result = sendTaskMessageCommand(store, taskId, params.body,
|
|
251
|
+
const result = sendTaskMessageCommand(store, taskId, params.body, { environment: callerEnvironment(caller) }, params.recipient, params.intent);
|
|
251
252
|
signal(taskId);
|
|
252
253
|
return result.message;
|
|
253
254
|
}
|
|
@@ -338,11 +339,11 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
338
339
|
...(patch.tags?.length === 0 ? { tags: null } : {})
|
|
339
340
|
}, {
|
|
340
341
|
environment: callerEnvironment(caller),
|
|
341
|
-
runtime: {
|
|
342
|
+
runtime: { notifyMailboxChanged: () => signal(taskId), reconcileTask: signal }
|
|
342
343
|
});
|
|
343
344
|
}
|
|
344
345
|
if (name === "job.get") {
|
|
345
|
-
const job = jobs.getJob(taskId, params.jobId);
|
|
346
|
+
const job = jobs.getJob(taskId, params.jobId, caller);
|
|
346
347
|
if (!job)
|
|
347
348
|
throw new Error(`Job not found: ${taskId}/${params.jobId}.`);
|
|
348
349
|
const operation = inspectJobOperation(job);
|
|
@@ -379,6 +380,9 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
379
380
|
if (sessions?.sessions[sessions.activeAgentId]?.effective.executionAuthority !== "delivery") {
|
|
380
381
|
throw new Error("Planning Sessions cannot start delivery Jobs.");
|
|
381
382
|
}
|
|
383
|
+
if (descriptor?.name === "job.start" && input !== undefined) {
|
|
384
|
+
assertJobAssignmentScope(store, input, caller);
|
|
385
|
+
}
|
|
382
386
|
}
|
|
383
387
|
if (permission === "task:read" || permission === "job:start")
|
|
384
388
|
continue;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createDurableJobControl } from "../controller/jobControl.js";
|
|
2
2
|
import { JOB_RUNNER_IMPLEMENTATION } from "../job/durableJob.js";
|
|
3
|
-
import { InstanceHost } from "./instanceHost.js";
|
|
4
3
|
import { createBuiltinCapabilities } from "./builtinCapabilities.js";
|
|
4
|
+
import { InstanceHost } from "./instanceHost.js";
|
|
5
5
|
/** Called once by the existing Controller root. Does not open a Store, start
|
|
6
6
|
* another Controller, or provide arbitrary persistence to plugin code.
|
|
7
7
|
* T02 registers contributions on this Host and wraps this same Job control.
|
|
@@ -29,19 +29,3 @@ export function createKernelPorts(store, runner, signal = () => undefined, conte
|
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
-
/** A read model over the one Job, never separately persisted.
|
|
33
|
-
* confirmed means the selected runner was observed, not that every action
|
|
34
|
-
* performed by an arbitrary command succeeded. Inspect original step receipts.
|
|
35
|
-
*/
|
|
36
|
-
export function inspectJobOperation(job) {
|
|
37
|
-
return {
|
|
38
|
-
operationRef: { taskId: job.taskId, jobId: job.id },
|
|
39
|
-
...job.operation,
|
|
40
|
-
state: job.status === "queued" ? "pending"
|
|
41
|
-
: job.status === "running" ? "running"
|
|
42
|
-
: job.status === "unknown-needs-attention" ? "unknown" : "finished",
|
|
43
|
-
outcome: job.result?.outcome,
|
|
44
|
-
result: job.result,
|
|
45
|
-
checkpoint: job.checkpoint
|
|
46
|
-
};
|
|
47
|
-
}
|