@zq-silk/yui 0.15.9 → 0.15.12
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 +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +64 -16
- package/dist/cli/interactionPolicy.js +7 -3
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli/updateOrchestrator.js +24 -1
- package/dist/cli/updatePorts.js +7 -3
- package/dist/cli/upgradeCommand.js +42 -2
- package/dist/cli.js +381 -107
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +339 -4
- package/dist/commands/projectCommands.js +50 -22
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +586 -96
- package/dist/commands/taskIntegrationCommands.js +19 -39
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +55 -6
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +14 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
- package/dist/controller/globalInputDelivery.js +132 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +117 -7
- package/dist/controller/runtimeEventInbox.js +32 -3
- package/dist/controller/runtimeEventProcessor.js +26 -6
- package/dist/controller/runtimeHookRunFence.js +75 -19
- package/dist/controller/structuredProviderObservation.js +133 -70
- package/dist/coordination/workMailboxQueue.js +5 -0
- package/dist/execution/workItemExecutionProjection.js +1 -1
- package/dist/executor/agentExecutor.js +64 -4
- package/dist/executor/executorRegistry.js +3 -0
- package/dist/executor/fileRoleLaunchPlanner.js +78 -118
- package/dist/integration/deliveryObligation.js +2 -1
- package/dist/integration/gitIntegrationService.js +329 -386
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +367 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +430 -107
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +182 -101
- package/dist/repository/taskWorkspacePreparer.js +205 -72
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +179 -82
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +38 -6
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +26 -8
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +167 -4
- package/dist/storage/sqliteStore.js +57 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +129 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +147 -17
- package/dist/web/assets/client/components.js +56 -13
- package/dist/web/assets/client/i18n.js +78 -4
- package/dist/web/assets/client/taskSurface.js +108 -1
- package/dist/web/assets/client/view.js +39 -8
- package/dist/web/assets/shell.js +29 -0
- package/dist/web/assets/styles/layout.js +8 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +131 -4
- package/dist/web/webSnapshot.js +16 -6
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +111 -35
- 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 +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- 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/release-workflow.md +39 -0
- package/docs/release-workflow.zh-CN.md +29 -0
- package/docs/sqlite-control-plane-design.md +223 -1
- package/docs/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +40 -0
- package/docs/testing/verification-levels.zh-CN.md +23 -0
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +154 -51
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +19 -3
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +42 -0
- package/skills/yui-runtime/references/publication.md +42 -0
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -6,18 +6,21 @@ import { dirname, join, relative, resolve, sep } from "node:path";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { selectEnvironment } from "../agent/launchEnvironment.js";
|
|
8
8
|
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
9
|
+
import { durableJobIdempotencyKey } from "../job/durableJob.js";
|
|
10
|
+
import { applyIntegrationSource, assertIntegrationCandidate, assertRecordedSourceCandidate } from "./integrationSourceApplication.js";
|
|
9
11
|
import { planBootstrapJobSteps, planL2JobSteps } from "../verification/verificationPlan.js";
|
|
10
12
|
import { assertNoAdHocFullSuiteChecks, checkResultsFromGateArtifact, checkResultsFromGateJob, gateIdentityForCandidate, lookupReusableGateArtifact, recordGateArtifactFromJob, recordGateArtifactFromStepOutcomes, resolveVerificationGate, runGateStepsInProcess } from "../verification/verificationGateService.js";
|
|
11
13
|
import { recordGateArtifactPotentialReuse, recordGateArtifactReuse } from "../verification/gateArtifact.js";
|
|
12
14
|
import { touchGateArtifact } from "../verification/gateArtifactStore.js";
|
|
13
|
-
import { NodeGitWorkspace
|
|
14
|
-
import {
|
|
15
|
+
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
16
|
+
import { integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
|
|
15
17
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
16
18
|
import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
|
|
17
19
|
import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
|
|
20
|
+
import { managedIntegrationRuntimeRoot } from "../storage/homeLayout.js";
|
|
18
21
|
import { advanceTaskProjectCommit } from "../task/task.js";
|
|
19
22
|
import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
20
|
-
import { recordIntegrationCheckJob,
|
|
23
|
+
import { recordIntegrationCheckJob, updateIntegrationAttempt } from "./integrationAttempt.js";
|
|
21
24
|
import { createManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
22
25
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
23
26
|
import { readRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
@@ -52,16 +55,12 @@ const INTEGRATION_OPERATIONAL_ENVIRONMENT_NAMES = [
|
|
|
52
55
|
"CURL_CA_BUNDLE",
|
|
53
56
|
"REQUESTS_CA_BUNDLE"
|
|
54
57
|
];
|
|
55
|
-
export const REMOTE_BASELINE_CONFLICT_PREFIX = "Upstream rebase conflicts";
|
|
56
|
-
const WORK_ITEM_MERGE_CONFLICT_PREFIX = "WorkItem merge conflicts";
|
|
57
|
-
const MANUAL_INTEGRATION_PREFIX = "Manual WorkItem integration";
|
|
58
58
|
export class GitIntegrationService {
|
|
59
59
|
store;
|
|
60
60
|
git;
|
|
61
61
|
now;
|
|
62
62
|
jobPort;
|
|
63
63
|
home;
|
|
64
|
-
worktreeRoot;
|
|
65
64
|
environment;
|
|
66
65
|
runtimeIsolation;
|
|
67
66
|
#resourceRegistrarValue;
|
|
@@ -71,50 +70,64 @@ export class GitIntegrationService {
|
|
|
71
70
|
this.now = now;
|
|
72
71
|
this.jobPort = jobPort;
|
|
73
72
|
this.home = resolve(home);
|
|
74
|
-
this.worktreeRoot = resolveWorktreeRoot(home, store.getConfig().defaultWorkspace);
|
|
75
73
|
this.environment = { ...environment };
|
|
76
74
|
this.runtimeIsolation = runtimeIsolation;
|
|
77
75
|
}
|
|
78
76
|
#resourceRegistrar() {
|
|
79
77
|
return this.#resourceRegistrarValue ??= new ResourceRegistrar(this.home, this.now);
|
|
80
78
|
}
|
|
81
|
-
async integrate(taskId, integrationId) {
|
|
82
|
-
|
|
79
|
+
async integrate(taskId, integrationId, signal) {
|
|
80
|
+
let initial = requireIntegration(this.store, taskId, integrationId);
|
|
81
|
+
if (!["running", "conflicted", "blocked", "validating"].includes(initial.status)) {
|
|
82
|
+
throw new Error(`Integration cannot continue from ${initial.status}.`);
|
|
83
|
+
}
|
|
83
84
|
// The whole Integration is one Git transaction against the Project's
|
|
84
85
|
// repository: worktree creation, cherry-picks, checks, and the target ref
|
|
85
86
|
// CAS. A concurrent `project migrate` must not switch the catalog path or
|
|
86
87
|
// remove the old checkout mid-Integration, so the per-Project maintenance
|
|
87
88
|
// fence is held across every Git effect and released only on exit.
|
|
88
|
-
const release = acquireProjectMaintenanceLocks(this.home, [initial.projectId]);
|
|
89
|
+
const release = await acquireProjectMaintenanceLocks(this.home, [initial.projectId], { signal });
|
|
89
90
|
try {
|
|
91
|
+
// A concurrent abort may have settled the attempt while this caller was
|
|
92
|
+
// acquiring the same Project fence. Never execute a pre-fence snapshot.
|
|
93
|
+
const currentUnderFence = requireIntegration(this.store, taskId, integrationId);
|
|
94
|
+
if (currentUnderFence.projectId !== initial.projectId
|
|
95
|
+
|| !["running", "conflicted", "blocked", "validating"].includes(currentUnderFence.status)) {
|
|
96
|
+
throw new Error("Integration changed before execution admission; read its current record.");
|
|
97
|
+
}
|
|
98
|
+
initial = currentUnderFence;
|
|
90
99
|
const task = this.store.getTask(initial.taskId);
|
|
91
100
|
if (task === null || !task.projectBindings.some(({ projectId }) => projectId === initial.projectId)) {
|
|
92
101
|
throw new Error(`Integration Task Project is unavailable: ${initial.taskId}.`);
|
|
93
102
|
}
|
|
94
|
-
if (task.status !== "active") {
|
|
103
|
+
if (task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
95
104
|
throw new Error(`Integration Task is not active: ${task.id}/${task.status}.`);
|
|
96
105
|
}
|
|
97
106
|
const project = this.store.getProject(initial.projectId);
|
|
98
107
|
if (project === null)
|
|
99
108
|
throw new Error(`Project not found: ${initial.projectId}.`);
|
|
109
|
+
if (project.status !== "active")
|
|
110
|
+
throw new Error(`Integration Project is not active: ${project.id}.`);
|
|
100
111
|
const taskWorkspace = this.store.getTaskWorkspace(task.id);
|
|
101
|
-
const
|
|
112
|
+
const taskEntry = taskWorkspace?.entries.find(({ projectId }) => projectId === project.id);
|
|
113
|
+
const taskRepository = taskEntry?.path;
|
|
102
114
|
if (taskWorkspace === null
|
|
103
115
|
|| taskWorkspace.owner.type !== "task"
|
|
116
|
+
|| taskEntry === undefined
|
|
104
117
|
|| taskRepository === undefined) {
|
|
105
118
|
throw new Error(`Integration Task clone is unavailable: ${task.id}/${project.id}.`);
|
|
106
119
|
}
|
|
107
120
|
// Issue 08: a Project with a VerificationPlan gates through its plan
|
|
108
121
|
// (bootstrap + L2) and reuses exact-SHA artifacts; an unconfigured
|
|
109
122
|
// Project keeps the existing explicit check path unchanged.
|
|
110
|
-
const gate = resolveVerificationGate(project, this.environment);
|
|
111
123
|
let prepared;
|
|
112
124
|
let workspace;
|
|
113
125
|
let managedWorkspace;
|
|
114
126
|
try {
|
|
115
127
|
prepared = await this.git.ensureIntegrationWorktree({
|
|
116
128
|
repositoryPath: taskRepository,
|
|
117
|
-
container:
|
|
129
|
+
container: integrationWorkspaceRoot(this.home, task.id, initial.id),
|
|
130
|
+
directory: taskEntry.directory,
|
|
118
131
|
taskSegment: taskWorkspaceRefSegment(task),
|
|
119
132
|
integrationId: initial.id,
|
|
120
133
|
baseRef: initial.beforeCommit
|
|
@@ -126,6 +139,12 @@ export class GitIntegrationService {
|
|
|
126
139
|
baseCommit: prepared.baseCommit
|
|
127
140
|
};
|
|
128
141
|
const existingWorkspace = this.store.getIntegrationWorkspace(task.id, initial.id);
|
|
142
|
+
if (existingWorkspace !== null && (resolve(existingWorkspace.root) !== resolve(prepared.path)
|
|
143
|
+
|| existingWorkspace.entries.length !== 1
|
|
144
|
+
|| existingWorkspace.entries[0]?.projectId !== project.id
|
|
145
|
+
|| existingWorkspace.entries[0]?.access !== "write"
|
|
146
|
+
|| existingWorkspace.entries[0]?.branch !== prepared.branch))
|
|
147
|
+
throw new Error("Integration managed workspace identity changed.");
|
|
129
148
|
managedWorkspace = existingWorkspace ?? createManagedWorkspace({
|
|
130
149
|
owner: {
|
|
131
150
|
type: "integration-attempt",
|
|
@@ -135,7 +154,7 @@ export class GitIntegrationService {
|
|
|
135
154
|
root: prepared.path,
|
|
136
155
|
entries: [{
|
|
137
156
|
projectId: project.id,
|
|
138
|
-
directory:
|
|
157
|
+
directory: taskEntry.directory,
|
|
139
158
|
access: "write",
|
|
140
159
|
path: prepared.path,
|
|
141
160
|
branch: prepared.branch,
|
|
@@ -150,19 +169,26 @@ export class GitIntegrationService {
|
|
|
150
169
|
return this.#fail(initial, error, "integration-preparation");
|
|
151
170
|
}
|
|
152
171
|
if (initial.status === "validating") {
|
|
153
|
-
return this.#recoverValidating(initial, workspace, taskRepository);
|
|
172
|
+
return await this.#recoverValidating(initial, workspace, managedWorkspace, taskRepository);
|
|
154
173
|
}
|
|
174
|
+
const gate = resolveVerificationGate(project, this.environment);
|
|
155
175
|
let current = initial;
|
|
156
176
|
// A check DurableJob is the source of truth for a running attempt that
|
|
157
177
|
// already bound one: never re-apply commits or spawn a second job. The
|
|
158
178
|
// job's terminal wakeup drives the resume through `integration continue`.
|
|
159
|
-
if (current.status === "running" && current.jobId !== undefined) {
|
|
160
|
-
return this.#resumeCheckJob(current, workspace, prepared.path, taskRepository, gate);
|
|
161
|
-
}
|
|
162
179
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
180
|
+
current = await applyIntegrationSource({
|
|
181
|
+
attempt: current, workspace, remoteUrl: project.remoteUrl,
|
|
182
|
+
git: this.git, store: this.store, now: this.now
|
|
183
|
+
});
|
|
184
|
+
if (current.status === "conflicted" || current.status === "blocked") {
|
|
185
|
+
return { status: current.status, attempt: current, workspace };
|
|
186
|
+
}
|
|
187
|
+
await assertIntegrationCandidate(prepared.path, current.candidateCommit, workspace.branch);
|
|
188
|
+
if (current.jobId !== undefined || current.checkInputDigest !== undefined) {
|
|
189
|
+
if (this.jobPort === undefined)
|
|
190
|
+
throw new Error("Integration requires its Controller Job port to resume.");
|
|
191
|
+
return await this.#startCheckJob(current, workspace, prepared.path, managedWorkspace, taskRepository, gate);
|
|
166
192
|
}
|
|
167
193
|
// Static preflight: fail before any expensive check when the target
|
|
168
194
|
// moved or its worktree is dirty, so the check commands never run on a
|
|
@@ -170,17 +196,17 @@ export class GitIntegrationService {
|
|
|
170
196
|
// after the checks, so a move during the gate is still fenced at CAS.
|
|
171
197
|
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.beforeCommit);
|
|
172
198
|
if (gate !== undefined) {
|
|
173
|
-
return this.#runVerificationGate(current, workspace, prepared.path, managedWorkspace, taskRepository, gate);
|
|
199
|
+
return await this.#runVerificationGate(current, workspace, prepared.path, managedWorkspace, taskRepository, gate);
|
|
174
200
|
}
|
|
175
201
|
if (this.jobPort !== undefined && current.checkCommands.length > 0) {
|
|
176
|
-
return this.#startCheckJob(current, workspace, prepared.path, managedWorkspace, taskRepository);
|
|
202
|
+
return await this.#startCheckJob(current, workspace, prepared.path, managedWorkspace, taskRepository);
|
|
177
203
|
}
|
|
204
|
+
const spec = await this.#checkSpecification(current, managedWorkspace, gate);
|
|
205
|
+
current = updateIntegrationAttempt(current, { checkInputDigest: spec.inputDigest }, this.now());
|
|
206
|
+
this.store.saveIntegrationAttempt(task.id, current);
|
|
178
207
|
const checkedHead = await gitLine(["-C", prepared.path, "rev-parse", "HEAD^{commit}"]);
|
|
179
|
-
const checkResults = await this.#runChecks(current, managedWorkspace, prepared.path);
|
|
180
|
-
|
|
181
|
-
if (afterHead !== checkedHead) {
|
|
182
|
-
return this.#fail(current, new Error(`Integration workspace moved during the checks: ${afterHead} != checked ${checkedHead}.`), "integration", workspace);
|
|
183
|
-
}
|
|
208
|
+
const checkResults = await this.#runChecks(current, managedWorkspace, prepared.path, spec.environment);
|
|
209
|
+
await assertIntegrationCandidate(prepared.path, checkedHead, workspace.branch);
|
|
184
210
|
if (checkResults.some((check) => check.outcome === "failed")) {
|
|
185
211
|
current = updateIntegrationAttempt(current, {
|
|
186
212
|
status: "failed",
|
|
@@ -201,18 +227,14 @@ export class GitIntegrationService {
|
|
|
201
227
|
return this.#terminalResult("committed", committed, workspace);
|
|
202
228
|
}
|
|
203
229
|
catch (error) {
|
|
204
|
-
if
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
summary: error.message
|
|
208
|
-
}, this.now());
|
|
209
|
-
this.store.saveIntegrationAttempt(task.id, pending);
|
|
210
|
-
return { status: "blocked", attempt: pending, workspace };
|
|
211
|
-
}
|
|
230
|
+
// Git/Job effects may already exist even if the following save failed.
|
|
231
|
+
// Keep the last durable cursor/candidate for exact Agent-directed retry.
|
|
232
|
+
current = requireIntegration(this.store, taskId, integrationId);
|
|
212
233
|
if (current.status === "validating" && current.candidateCommit !== undefined) {
|
|
213
234
|
const target = await resolveRef(taskRepository, current.targetRef);
|
|
214
235
|
if (target === current.candidateCommit) {
|
|
215
236
|
try {
|
|
237
|
+
await assertIntegrationCandidate(workspace.path, current.candidateCommit, workspace.branch);
|
|
216
238
|
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.candidateCommit);
|
|
217
239
|
const committed = this.#recordCommitted(current);
|
|
218
240
|
return this.#terminalResult("committed", committed, workspace);
|
|
@@ -224,19 +246,117 @@ export class GitIntegrationService {
|
|
|
224
246
|
}
|
|
225
247
|
}
|
|
226
248
|
}
|
|
227
|
-
|
|
249
|
+
const diagnosed = updateIntegrationAttempt(current, {
|
|
250
|
+
summary: error instanceof Error ? error.message : String(error)
|
|
251
|
+
}, this.now());
|
|
252
|
+
this.store.saveIntegrationAttempt(taskId, diagnosed);
|
|
253
|
+
return { status: diagnosed.status === "conflicted" ? "conflicted" : "blocked", attempt: diagnosed, workspace };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
release();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Abandon under the same fence as Git/CAS, not just a status whitelist.
|
|
261
|
+
* A delivered validating candidate is recorded as committed; a provably
|
|
262
|
+
* unadvanced one may be abandoned without requiring obsolete check settings. */
|
|
263
|
+
async abort(taskId, integrationId, reason, authorize, signal) {
|
|
264
|
+
const initial = requireIntegration(this.store, taskId, integrationId);
|
|
265
|
+
const release = await acquireProjectMaintenanceLocks(this.home, [initial.projectId], { signal });
|
|
266
|
+
try {
|
|
267
|
+
const attempt = requireIntegration(this.store, taskId, integrationId);
|
|
268
|
+
authorize(this.store, taskId);
|
|
269
|
+
const task = this.store.getTask(taskId);
|
|
270
|
+
if (task?.status !== "active" || attempt.projectId !== initial.projectId
|
|
271
|
+
|| !task.projectBindings.some(binding => binding.projectId === attempt.projectId)) {
|
|
272
|
+
throw new Error("Integration Task/Project is not available for abort.");
|
|
273
|
+
}
|
|
274
|
+
if (!["running", "blocked", "conflicted", "validating"].includes(attempt.status)) {
|
|
275
|
+
throw new Error(`Integration cannot be aborted from ${attempt.status}: ${attempt.id}.`);
|
|
276
|
+
}
|
|
277
|
+
if (reason.trim() === "")
|
|
278
|
+
throw new Error("Integration abort reason is required.");
|
|
279
|
+
const jobs = this.store.listDurableJobs(taskId).filter(job => job.owner.kind === "integration-attempt" && job.owner.integrationAttemptId === integrationId);
|
|
280
|
+
const workspace = this.store.getIntegrationWorkspace(taskId, integrationId);
|
|
281
|
+
if (jobs.some(job => job.taskId !== taskId || job.projectId !== attempt.projectId
|
|
282
|
+
|| job.workspace !== workspace?.root)) {
|
|
283
|
+
throw new Error("Integration Job Project/workspace identity changed; inspect before abort.");
|
|
284
|
+
}
|
|
285
|
+
if (attempt.jobId !== undefined && !jobs.some(job => job.id === attempt.jobId)) {
|
|
286
|
+
throw new Error("Integration Job binding does not match its owner; inspect before abort.");
|
|
287
|
+
}
|
|
288
|
+
if (attempt.status === "validating") {
|
|
289
|
+
if (attempt.candidateCommit === undefined || attempt.checks === undefined) {
|
|
290
|
+
throw new Error("Validating Integration has no candidate/check evidence; inspect before settlement.");
|
|
291
|
+
}
|
|
292
|
+
const main = this.store.getTaskWorkspace(taskId);
|
|
293
|
+
const mainEntry = main?.entries.find(entry => entry.projectId === attempt.projectId && entry.access === "write");
|
|
294
|
+
const workspaceEntry = workspace?.entries.find(entry => entry.projectId === attempt.projectId && entry.access === "write");
|
|
295
|
+
const project = this.store.getProject(attempt.projectId);
|
|
296
|
+
if (main?.owner.type !== "task" || mainEntry === undefined || workspace === null
|
|
297
|
+
|| project === null || project.status !== "active"
|
|
298
|
+
|| workspace.owner.type !== "integration-attempt" || workspace.owner.taskId !== taskId
|
|
299
|
+
|| workspace.owner.integrationAttemptId !== integrationId
|
|
300
|
+
|| workspaceEntry?.path !== workspace.root) {
|
|
301
|
+
throw new Error("Integration target/workspace ownership is unavailable for settlement.");
|
|
302
|
+
}
|
|
303
|
+
if (jobs.some(job => ["queued", "running", "unknown-needs-attention"].includes(job.status))) {
|
|
304
|
+
throw new Error("Validating Integration still has an active or unproven Job; settle that exact Job first.");
|
|
305
|
+
}
|
|
306
|
+
if (attempt.jobId !== undefined) {
|
|
307
|
+
assertCheckJobIdentity(attempt, jobs.find(job => job.id === attempt.jobId), workspace.root);
|
|
308
|
+
}
|
|
309
|
+
const target = await resolveRef(mainEntry.path, attempt.targetRef);
|
|
310
|
+
if (target === attempt.candidateCommit && target !== attempt.beforeCommit) {
|
|
311
|
+
await assertTargetReadyForChecks(mainEntry.path, attempt.targetRef, target);
|
|
312
|
+
return this.#recordCommitted(attempt, authorize);
|
|
313
|
+
}
|
|
314
|
+
if (target !== attempt.beforeCommit) {
|
|
315
|
+
throw new Error(`Target moved to ${target}; cannot prove this Integration is unadvanced. Inspect before settlement.`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const job of jobs) {
|
|
319
|
+
if (this.jobPort !== undefined && (job.status === "queued" || job.status === "running")) {
|
|
320
|
+
await this.jobPort.cancelJob(taskId, job.id);
|
|
321
|
+
}
|
|
228
322
|
}
|
|
323
|
+
return this.store.transaction(tx => {
|
|
324
|
+
authorize(tx, taskId);
|
|
325
|
+
const current = requireIntegration(tx, taskId, integrationId);
|
|
326
|
+
if (JSON.stringify(current) !== JSON.stringify(attempt)) {
|
|
327
|
+
throw new Error("Integration changed during abort; inspect its current record.");
|
|
328
|
+
}
|
|
329
|
+
const aborted = updateIntegrationAttempt(current, {
|
|
330
|
+
status: "failed",
|
|
331
|
+
checks: [...(current.checks ?? []), { name: "aborted", outcome: "failed", details: reason }]
|
|
332
|
+
}, this.now());
|
|
333
|
+
tx.saveIntegrationAttempt(taskId, aborted);
|
|
334
|
+
return aborted;
|
|
335
|
+
});
|
|
229
336
|
}
|
|
230
337
|
finally {
|
|
231
338
|
release();
|
|
232
339
|
}
|
|
233
340
|
}
|
|
234
|
-
async cleanup(integration) {
|
|
341
|
+
async cleanup(integration, options = {}) {
|
|
235
342
|
// The whole cleanup is one Git transaction against the Project's
|
|
236
343
|
// repository, mirroring integrate(): a concurrent `project migrate` must
|
|
237
344
|
// not switch the catalog path or remove the old checkout mid-cleanup.
|
|
238
|
-
const release = acquireProjectMaintenanceLocks(this.home, [integration.projectId]);
|
|
345
|
+
const release = await acquireProjectMaintenanceLocks(this.home, [integration.projectId], { signal: options.signal });
|
|
239
346
|
try {
|
|
347
|
+
options.authorize?.();
|
|
348
|
+
const current = requireIntegration(this.store, integration.taskId, integration.id);
|
|
349
|
+
if (current.projectId !== integration.projectId
|
|
350
|
+
|| !["committed", "superseded", "failed"].includes(current.status)) {
|
|
351
|
+
throw new Error("Integration changed before cleanup; read its current record.");
|
|
352
|
+
}
|
|
353
|
+
integration = current;
|
|
354
|
+
const activeJob = this.store.listDurableJobs(integration.taskId).find(job => job.owner.kind === "integration-attempt" && job.owner.integrationAttemptId === integration.id
|
|
355
|
+
&& (job.status === "queued" || job.status === "running"
|
|
356
|
+
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
|
|
357
|
+
if (activeJob !== undefined) {
|
|
358
|
+
throw new Error(`Integration ${integration.id} has an active DurableJob: ${activeJob.id}/${activeJob.status}.`);
|
|
359
|
+
}
|
|
240
360
|
const task = this.store.getTask(integration.taskId);
|
|
241
361
|
if (task === null || !task.projectBindings.some(({ projectId }) => projectId === integration.projectId)) {
|
|
242
362
|
throw new Error(`Integration Task Project is unavailable: ${integration.id}.`);
|
|
@@ -253,9 +373,18 @@ export class GitIntegrationService {
|
|
|
253
373
|
const runtime = this.#runtimePreparation(integration, managedWorkspace);
|
|
254
374
|
this.runtimeIsolation.cleanup(runtime, integration.status === "committed" ? "completion" : "failure");
|
|
255
375
|
}
|
|
376
|
+
const recorded = managedWorkspace?.entries[0];
|
|
377
|
+
const directory = recorded?.directory
|
|
378
|
+
?? task.projectBindings.find(({ projectId }) => projectId === integration.projectId)?.directory;
|
|
379
|
+
if (directory === undefined) {
|
|
380
|
+
throw new Error(`Integration Project binding is unavailable: ${task.id}/${project.id}.`);
|
|
381
|
+
}
|
|
256
382
|
const result = await this.git.removeIntegrationWorktree({
|
|
257
383
|
repositoryPath: taskRepository,
|
|
258
|
-
container:
|
|
384
|
+
container: recorded !== undefined
|
|
385
|
+
? dirname(recorded.path)
|
|
386
|
+
: integrationWorkspaceRoot(this.home, task.id, integration.id),
|
|
387
|
+
directory,
|
|
259
388
|
taskSegment: taskWorkspaceRefSegment(task),
|
|
260
389
|
integrationId: integration.id,
|
|
261
390
|
discardChanges: integration.status === "failed"
|
|
@@ -287,26 +416,15 @@ export class GitIntegrationService {
|
|
|
287
416
|
* it, so a Leader exit mid-checks leaves no running/no-check zombie.
|
|
288
417
|
*/
|
|
289
418
|
async #startCheckJob(attempt, workspace, path, managedWorkspace, repositoryPath, gate) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
...planBootstrapJobSteps(gate.plan).map((step) => ({
|
|
300
|
-
...step,
|
|
301
|
-
timeoutMs: 30 * 60_000
|
|
302
|
-
})),
|
|
303
|
-
...planL2JobSteps(gate.plan).map((step) => ({
|
|
304
|
-
...step,
|
|
305
|
-
timeoutMs: 30 * 60_000
|
|
306
|
-
}))
|
|
307
|
-
];
|
|
308
|
-
const releaseId = integrationRuntimeReleaseIdentity(this.home);
|
|
309
|
-
if (gate === undefined && releaseId !== null) {
|
|
419
|
+
if (attempt.status !== "running" || attempt.candidateCommit === undefined) {
|
|
420
|
+
throw new Error(`Integration check admission requires a running, proven candidate: ${attempt.id}.`);
|
|
421
|
+
}
|
|
422
|
+
await assertIntegrationCandidate(path, attempt.candidateCommit, workspace.branch);
|
|
423
|
+
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit);
|
|
424
|
+
const { runtime, head, steps, environment, inputDigest, releaseId } = await this.#checkSpecification(attempt, managedWorkspace, gate);
|
|
425
|
+
const ownedJobs = this.store.listDurableJobs(attempt.taskId).filter(job => job.owner.kind === "integration-attempt" && job.owner.integrationAttemptId === attempt.id);
|
|
426
|
+
if (gate === undefined && releaseId !== null && attempt.checkInputDigest === undefined
|
|
427
|
+
&& ownedJobs.length === 0 && attempt.jobId === undefined) {
|
|
310
428
|
const reusable = findReusableIntegrationCheckEvidence({
|
|
311
429
|
taskId: attempt.taskId,
|
|
312
430
|
projectId: attempt.projectId,
|
|
@@ -320,25 +438,28 @@ export class GitIntegrationService {
|
|
|
320
438
|
logPathFor: (job, relativeLogPath) => (join(job.artifactsLocator, "logs", relativeLogPath))
|
|
321
439
|
});
|
|
322
440
|
if (reusable !== null) {
|
|
323
|
-
return this.#finalizeGateSuccess(attempt, workspace, repositoryPath, head, [...reusable.checks]);
|
|
441
|
+
return this.#finalizeGateSuccess(updateIntegrationAttempt(attempt, { checkInputDigest: inputDigest }, this.now()), workspace, repositoryPath, head, [...reusable.checks]);
|
|
324
442
|
}
|
|
325
443
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
444
|
+
if (attempt.checkInputDigest !== undefined && attempt.checkInputDigest !== inputDigest) {
|
|
445
|
+
throw new Error("Integration check conditions changed since admission; inspect the original Job, then abort or start a new attempt.");
|
|
446
|
+
}
|
|
447
|
+
if (attempt.jobId !== undefined) {
|
|
448
|
+
return this.#resumeCheckJob(attempt, workspace, path, repositoryPath, gate);
|
|
449
|
+
}
|
|
332
450
|
// Persist the gate identity before starting the job so a plan edit
|
|
333
451
|
// during the gate never misattributes the evidence on resume.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
452
|
+
const persisted = updateIntegrationAttempt(attempt, {
|
|
453
|
+
checkInputDigest: inputDigest,
|
|
454
|
+
...(gate === undefined ? {} : {
|
|
337
455
|
gatePlanDigest: gate.planDigest,
|
|
338
456
|
gateToolchainDigest: gate.toolchainDigest
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
457
|
+
})
|
|
458
|
+
}, this.now());
|
|
459
|
+
this.store.saveIntegrationAttempt(attempt.taskId, persisted);
|
|
460
|
+
// All domain admission and durable candidate identity precede Job effects.
|
|
461
|
+
this.runtimeIsolation.activate(runtime);
|
|
462
|
+
await prepareIntegrationRuntimeHome(runtime, this.home);
|
|
342
463
|
const job = await this.jobPort.startCheckJob({
|
|
343
464
|
taskId: attempt.taskId,
|
|
344
465
|
integrationId: attempt.id,
|
|
@@ -348,6 +469,7 @@ export class GitIntegrationService {
|
|
|
348
469
|
env: environment,
|
|
349
470
|
steps
|
|
350
471
|
});
|
|
472
|
+
assertCheckJobIdentity(persisted, job, path);
|
|
351
473
|
const bound = recordIntegrationCheckJob(persisted, job.id, this.now());
|
|
352
474
|
this.store.saveIntegrationAttempt(attempt.taskId, bound);
|
|
353
475
|
if (job.status !== "queued" && job.status !== "running") {
|
|
@@ -367,12 +489,17 @@ export class GitIntegrationService {
|
|
|
367
489
|
*/
|
|
368
490
|
async #resumeCheckJob(attempt, workspace, path, repositoryPath, gate) {
|
|
369
491
|
const job = await this.jobPort.getJob(attempt.taskId, attempt.jobId);
|
|
492
|
+
assertCheckJobIdentity(attempt, job, path);
|
|
493
|
+
await assertIntegrationCandidate(path, job.head, workspace.branch);
|
|
494
|
+
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit);
|
|
495
|
+
if (gate?.planDigest !== attempt.gatePlanDigest
|
|
496
|
+
|| gate?.toolchainDigest !== attempt.gateToolchainDigest) {
|
|
497
|
+
throw new Error("Integration verification plan/toolchain changed; the original Job cannot prove the current gate.");
|
|
498
|
+
}
|
|
370
499
|
if (job.status === "queued" || job.status === "running") {
|
|
371
500
|
return { status: "checks-running", attempt, workspace, job };
|
|
372
501
|
}
|
|
373
|
-
const
|
|
374
|
-
|| (job.steps?.some((step) => step.name.startsWith("bootstrap-") || step.name.startsWith("gate-")) ?? false);
|
|
375
|
-
const checks = planStyle
|
|
502
|
+
const checks = gate !== undefined
|
|
376
503
|
? checkResultsFromGateJob(job, this.home)
|
|
377
504
|
: checkResultsFromJob(attempt, job);
|
|
378
505
|
const managedWorkspace = this.store.getIntegrationWorkspace(attempt.taskId, attempt.id);
|
|
@@ -380,25 +507,13 @@ export class GitIntegrationService {
|
|
|
380
507
|
const runtime = this.#runtimePreparation(attempt, managedWorkspace);
|
|
381
508
|
this.runtimeIsolation.cleanup(runtime, checks.some((check) => check.outcome === "failed") ? "failure" : "completion");
|
|
382
509
|
}
|
|
383
|
-
//
|
|
384
|
-
// identity
|
|
385
|
-
// checked head; a plan/toolchain change since the job started yields a
|
|
386
|
-
// different key, so the artifact is never misattributed (the attempt
|
|
387
|
-
// still converges from the job's own evidence).
|
|
510
|
+
// The current plan/toolchain was matched to admission above. Record only
|
|
511
|
+
// that exact identity, never reinterpret an old Job under a changed plan.
|
|
388
512
|
if (gate !== undefined
|
|
389
513
|
&& (job.result?.outcome === "succeeded" || job.result?.outcome === "failed")) {
|
|
390
|
-
// Use the digests captured at job start so a plan edit during the
|
|
391
|
-
// gate never misattributes the evidence.
|
|
392
|
-
const recordGate = attempt.gatePlanDigest !== undefined
|
|
393
|
-
? Object.freeze({
|
|
394
|
-
...gate,
|
|
395
|
-
planDigest: attempt.gatePlanDigest,
|
|
396
|
-
toolchainDigest: attempt.gateToolchainDigest ?? gate.toolchainDigest
|
|
397
|
-
})
|
|
398
|
-
: gate;
|
|
399
514
|
const identity = gateIdentityForCandidate({
|
|
400
515
|
projectId: attempt.projectId,
|
|
401
|
-
gate
|
|
516
|
+
gate,
|
|
402
517
|
level: "L2",
|
|
403
518
|
commit: job.head,
|
|
404
519
|
targetRef: attempt.targetRef,
|
|
@@ -424,30 +539,16 @@ export class GitIntegrationService {
|
|
|
424
539
|
this.store.saveIntegrationAttempt(attempt.taskId, failed);
|
|
425
540
|
return this.#terminalResult("failed", failed, workspace);
|
|
426
541
|
}
|
|
427
|
-
|
|
428
|
-
if (candidateCommit !== job.head) {
|
|
429
|
-
// The job proved the checks at one SHA; the workspace has since moved.
|
|
430
|
-
// Advancing the target ref would publish unchecked code, so fail closed.
|
|
431
|
-
return this.#fail(attempt, new Error(`Integration workspace moved since the check ran: ${candidateCommit} != checked ${job.head}.`), "integration", workspace);
|
|
432
|
-
}
|
|
433
|
-
const validating = updateIntegrationAttempt(attempt, {
|
|
434
|
-
status: "validating",
|
|
435
|
-
candidateCommit,
|
|
436
|
-
checks
|
|
437
|
-
}, this.now());
|
|
438
|
-
this.store.saveIntegrationAttempt(attempt.taskId, validating);
|
|
439
|
-
await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.beforeCommit);
|
|
440
|
-
const committed = this.#recordCommitted(validating);
|
|
441
|
-
return this.#terminalResult("committed", committed, workspace);
|
|
542
|
+
return this.#finalizeGateSuccess(attempt, workspace, repositoryPath, job.head, checks);
|
|
442
543
|
}
|
|
443
|
-
async #runChecks(attempt, workspace, path) {
|
|
544
|
+
async #runChecks(attempt, workspace, path, environment) {
|
|
444
545
|
if (attempt.checkCommands.length === 0)
|
|
445
546
|
return [];
|
|
446
547
|
const runtime = this.#runtimePreparation(attempt, workspace);
|
|
447
548
|
this.runtimeIsolation.activate(runtime);
|
|
448
549
|
let cleanupReason = "failure";
|
|
449
550
|
try {
|
|
450
|
-
|
|
551
|
+
await prepareIntegrationRuntimeHome(runtime, this.home);
|
|
451
552
|
const checks = await runChecks(path, attempt.checkCommands, this.home, attempt.taskId, attempt.id, environment);
|
|
452
553
|
cleanupReason = checks.some(({ outcome }) => outcome === "failed")
|
|
453
554
|
? "failure"
|
|
@@ -472,6 +573,12 @@ export class GitIntegrationService {
|
|
|
472
573
|
* gate.
|
|
473
574
|
*/
|
|
474
575
|
async #runVerificationGate(attempt, workspace, path, managedWorkspace, repositoryPath, gate) {
|
|
576
|
+
const spec = await this.#checkSpecification(attempt, managedWorkspace, gate);
|
|
577
|
+
attempt = updateIntegrationAttempt(attempt, {
|
|
578
|
+
checkInputDigest: spec.inputDigest,
|
|
579
|
+
gatePlanDigest: gate.planDigest, gateToolchainDigest: gate.toolchainDigest
|
|
580
|
+
}, this.now());
|
|
581
|
+
this.store.saveIntegrationAttempt(attempt.taskId, attempt);
|
|
475
582
|
if (gate.mode === "enforce") {
|
|
476
583
|
try {
|
|
477
584
|
assertNoAdHocFullSuiteChecks(gate.plan, attempt.checkCommands);
|
|
@@ -513,12 +620,12 @@ export class GitIntegrationService {
|
|
|
513
620
|
this.runtimeIsolation.activate(runtime);
|
|
514
621
|
let cleanupReason = "failure";
|
|
515
622
|
try {
|
|
516
|
-
|
|
623
|
+
await prepareIntegrationRuntimeHome(runtime, this.home);
|
|
517
624
|
const steps = [
|
|
518
625
|
...planBootstrapJobSteps(gate.plan),
|
|
519
626
|
...planL2JobSteps(gate.plan)
|
|
520
627
|
];
|
|
521
|
-
const outcomes = await runGateStepsInProcess(path, steps, environment, integrationCheckDirectory(this.home, attempt.taskId, attempt.id), candidateCommit);
|
|
628
|
+
const outcomes = await runGateStepsInProcess(path, steps, spec.environment, integrationCheckDirectory(this.home, attempt.taskId, attempt.id), candidateCommit);
|
|
522
629
|
const succeeded = outcomes.length === steps.length
|
|
523
630
|
&& outcomes.every((outcome) => outcome.exitCode === 0 && outcome.signal === null && !outcome.timedOut);
|
|
524
631
|
const artifact = await recordGateArtifactFromStepOutcomes(this.store, identity, gate.plan, outcomes, succeeded, this.now());
|
|
@@ -536,6 +643,7 @@ export class GitIntegrationService {
|
|
|
536
643
|
}
|
|
537
644
|
}
|
|
538
645
|
async #finalizeGateSuccess(attempt, workspace, repositoryPath, candidateCommit, checks) {
|
|
646
|
+
await assertIntegrationCandidate(workspace.path, candidateCommit, workspace.branch);
|
|
539
647
|
const validating = updateIntegrationAttempt(attempt, {
|
|
540
648
|
status: "validating",
|
|
541
649
|
candidateCommit,
|
|
@@ -552,175 +660,75 @@ export class GitIntegrationService {
|
|
|
552
660
|
allowExactActive: true
|
|
553
661
|
});
|
|
554
662
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (await this.git.isAncestor(candidatePath, attempt.source.remoteCommit, attempt.beforeCommit)) {
|
|
581
|
-
return undefined;
|
|
582
|
-
}
|
|
583
|
-
try {
|
|
584
|
-
await git([
|
|
585
|
-
"-C", candidatePath,
|
|
586
|
-
"-c", "user.name=Yui",
|
|
587
|
-
"-c", "user.email=yui@local",
|
|
588
|
-
"rebase", "--onto",
|
|
589
|
-
attempt.source.remoteCommit,
|
|
590
|
-
attempt.source.taskBaseCommit,
|
|
591
|
-
workspace.branch
|
|
592
|
-
]);
|
|
593
|
-
}
|
|
594
|
-
catch (error) {
|
|
595
|
-
const affectedPaths = await conflictedPaths(candidatePath);
|
|
596
|
-
if (affectedPaths.length === 0)
|
|
597
|
-
throw error;
|
|
598
|
-
const pending = requireResolutionDecision(attempt, {
|
|
599
|
-
affectedPaths,
|
|
600
|
-
summary: `${REMOTE_BASELINE_CONFLICT_PREFIX} in ${attempt.targetRef}.`
|
|
601
|
-
}, this.now());
|
|
602
|
-
this.store.saveIntegrationAttempt(attempt.taskId, pending);
|
|
603
|
-
return { status: "blocked", attempt: pending, workspace };
|
|
604
|
-
}
|
|
605
|
-
return undefined;
|
|
606
|
-
}
|
|
607
|
-
if (attempt.source.strategy === "manual") {
|
|
608
|
-
if (attempt.resolution?.action === "manual-resolution"
|
|
609
|
-
&& attempt.conflict?.summary.startsWith(MANUAL_INTEGRATION_PREFIX)) {
|
|
610
|
-
if (!await this.git.isClean(candidatePath)) {
|
|
611
|
-
throw new Error("Manual Integration workspace must be clean and committed.");
|
|
612
|
-
}
|
|
613
|
-
return undefined;
|
|
614
|
-
}
|
|
615
|
-
const pending = requireResolutionDecision(attempt, {
|
|
616
|
-
affectedPaths: [],
|
|
617
|
-
summary: `${MANUAL_INTEGRATION_PREFIX}; apply the selected WorkItem result in the Integration workspace.`
|
|
618
|
-
}, this.now());
|
|
619
|
-
this.store.saveIntegrationAttempt(attempt.taskId, pending);
|
|
620
|
-
return { status: "blocked", attempt: pending, workspace };
|
|
621
|
-
}
|
|
622
|
-
if (attempt.source.strategy === "ff") {
|
|
623
|
-
await git([
|
|
624
|
-
"-C", candidatePath,
|
|
625
|
-
"merge", "--ff-only",
|
|
626
|
-
attempt.source.resultCommit
|
|
627
|
-
]);
|
|
628
|
-
return undefined;
|
|
629
|
-
}
|
|
630
|
-
if (attempt.source.strategy === "merge") {
|
|
631
|
-
if (attempt.resolution?.action === "manual-resolution"
|
|
632
|
-
&& attempt.conflict?.summary.startsWith(WORK_ITEM_MERGE_CONFLICT_PREFIX)) {
|
|
633
|
-
await completeMergeResolution(candidatePath);
|
|
634
|
-
return undefined;
|
|
635
|
-
}
|
|
636
|
-
try {
|
|
637
|
-
await git([
|
|
638
|
-
"-C", candidatePath,
|
|
639
|
-
"-c", "user.name=Yui",
|
|
640
|
-
"-c", "user.email=yui@local",
|
|
641
|
-
"merge", "--no-edit", "--no-ff",
|
|
642
|
-
attempt.source.resultCommit
|
|
643
|
-
]);
|
|
644
|
-
}
|
|
645
|
-
catch (error) {
|
|
646
|
-
const affectedPaths = await conflictedPaths(candidatePath);
|
|
647
|
-
if (affectedPaths.length === 0)
|
|
648
|
-
throw error;
|
|
649
|
-
const pending = requireResolutionDecision(attempt, {
|
|
650
|
-
affectedPaths,
|
|
651
|
-
summary: `${WORK_ITEM_MERGE_CONFLICT_PREFIX} in ${attempt.targetRef}.`
|
|
652
|
-
}, this.now());
|
|
653
|
-
this.store.saveIntegrationAttempt(attempt.taskId, pending);
|
|
654
|
-
return { status: "blocked", attempt: pending, workspace };
|
|
655
|
-
}
|
|
656
|
-
return undefined;
|
|
657
|
-
}
|
|
658
|
-
const commits = await commitsBetween(candidatePath, attempt.source.startCommit, attempt.source.resultCommit, attempt.source.workItemId);
|
|
659
|
-
let remaining = commits;
|
|
660
|
-
if (attempt.resolution?.action === "manual-resolution") {
|
|
661
|
-
const resolvedCommit = await completeCherryPickResolution(candidatePath);
|
|
662
|
-
const resolvedIndex = commits.findIndex(({ commit }) => commit === resolvedCommit);
|
|
663
|
-
if (resolvedIndex < 0) {
|
|
664
|
-
throw new Error(`Manual resolution commit is not part of the WorkItem result: ${resolvedCommit}.`);
|
|
665
|
-
}
|
|
666
|
-
remaining = commits.slice(resolvedIndex + 1);
|
|
667
|
-
}
|
|
668
|
-
for (const { label, commit } of remaining) {
|
|
669
|
-
// Fast-forward when the commit is a direct descendant of HEAD: this
|
|
670
|
-
// preserves the original commit SHA, which exact-SHA review evidence
|
|
671
|
-
// relies on. Fall back to cherry-pick when the target moved since the
|
|
672
|
-
// WorkItem was based (the commit is no longer a direct descendant).
|
|
673
|
-
if (await gitSucceeds(["-C", candidatePath, "merge", "--ff-only", commit])) {
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
try {
|
|
677
|
-
await git(["-C", candidatePath, "cherry-pick", commit]);
|
|
678
|
-
}
|
|
679
|
-
catch {
|
|
680
|
-
const affectedPaths = (await git([
|
|
681
|
-
"-C", candidatePath, "diff", "--name-only", "--diff-filter=U"
|
|
682
|
-
])).trim().split("\n").filter(Boolean);
|
|
683
|
-
if (affectedPaths.length === 0
|
|
684
|
-
&& await isEmptyCherryPick(candidatePath, commit)) {
|
|
685
|
-
await git(["-C", candidatePath, "cherry-pick", "--skip"]);
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
const pending = requireResolutionDecision(attempt, {
|
|
689
|
-
affectedPaths,
|
|
690
|
-
summary: `${label} conflicts with ${attempt.targetRef}.`
|
|
691
|
-
}, this.now());
|
|
692
|
-
this.store.saveIntegrationAttempt(attempt.taskId, pending);
|
|
693
|
-
return { status: "blocked", attempt: pending, workspace };
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
return undefined;
|
|
663
|
+
/** The same specification is compared before first execution, bound-Job
|
|
664
|
+
* consumption and an unadvanced validating CAS. Computing it is read-only:
|
|
665
|
+
* a changed condition must not recreate a completed runtime or start a Job. */
|
|
666
|
+
async #checkSpecification(attempt, managedWorkspace, gate) {
|
|
667
|
+
const runtime = this.#runtimePreparation(attempt, managedWorkspace);
|
|
668
|
+
const head = attempt.candidateCommit;
|
|
669
|
+
if (head === undefined)
|
|
670
|
+
throw new Error("Integration check specification requires a candidate.");
|
|
671
|
+
const steps = gate === undefined
|
|
672
|
+
? attempt.checkCommands.map((command, index) => ({
|
|
673
|
+
name: `check-${index + 1}`, command, timeoutMs: 30 * 60_000
|
|
674
|
+
}))
|
|
675
|
+
: [...planBootstrapJobSteps(gate.plan), ...planL2JobSteps(gate.plan)]
|
|
676
|
+
.map(step => ({ ...step, timeoutMs: 30 * 60_000 }));
|
|
677
|
+
const releaseId = integrationRuntimeReleaseIdentity(this.home);
|
|
678
|
+
const environment = Object.freeze({
|
|
679
|
+
...integrationCheckEnvironment(this.environment, runtime, this.home),
|
|
680
|
+
...(releaseId === null ? {} : { [INTEGRATION_RUNTIME_RELEASE_ENV]: releaseId })
|
|
681
|
+
});
|
|
682
|
+
const inputDigest = durableJobIdempotencyKey({
|
|
683
|
+
owner: { kind: "integration-attempt", integrationAttemptId: attempt.id },
|
|
684
|
+
projectId: attempt.projectId, head, workspace: managedWorkspace.root,
|
|
685
|
+
env: environment, steps
|
|
686
|
+
});
|
|
687
|
+
return { runtime, head, steps, environment, inputDigest, releaseId };
|
|
697
688
|
}
|
|
698
|
-
async #recoverValidating(attempt, workspace, repositoryPath) {
|
|
689
|
+
async #recoverValidating(attempt, workspace, managedWorkspace, repositoryPath) {
|
|
699
690
|
if (attempt.candidateCommit === undefined || attempt.checks === undefined) {
|
|
700
691
|
return this.#fail(attempt, new Error("Validating Integration is missing its candidate commit or checks."), "integration-recovery", workspace);
|
|
701
692
|
}
|
|
702
693
|
const target = await resolveRef(repositoryPath, attempt.targetRef);
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
694
|
+
// An already-applied CAS is a delivery fact, not a request to validate
|
|
695
|
+
// again under today's settings. No-op candidates still need current checks.
|
|
696
|
+
if (target !== attempt.candidateCommit || target === attempt.beforeCommit) {
|
|
697
|
+
if (target !== attempt.beforeCommit) {
|
|
698
|
+
throw new Error(`Target moved to ${target}; Integration CAS outcome needs inspection.`);
|
|
706
699
|
}
|
|
707
|
-
|
|
708
|
-
|
|
700
|
+
await assertIntegrationCandidate(workspace.path, attempt.candidateCommit, workspace.branch);
|
|
701
|
+
assertRecordedSourceCandidate(attempt, workspace);
|
|
702
|
+
const project = this.store.getProject(attempt.projectId);
|
|
703
|
+
if (project === null)
|
|
704
|
+
throw new Error("Integration Project is unavailable for check validation.");
|
|
705
|
+
const gate = resolveVerificationGate(project, this.environment);
|
|
706
|
+
const spec = await this.#checkSpecification(attempt, managedWorkspace, gate);
|
|
707
|
+
if (attempt.checkInputDigest !== spec.inputDigest
|
|
708
|
+
|| gate?.planDigest !== attempt.gatePlanDigest
|
|
709
|
+
|| gate?.toolchainDigest !== attempt.gateToolchainDigest) {
|
|
710
|
+
throw new Error("Integration check conditions changed after validation; preserve the evidence and abort or use a new attempt.");
|
|
709
711
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
712
|
+
if (attempt.jobId !== undefined) {
|
|
713
|
+
const job = this.store.getDurableJob(attempt.taskId, attempt.jobId);
|
|
714
|
+
if (job === null || job.status !== "succeeded") {
|
|
715
|
+
throw new Error("Validating Integration has no successful bound Job evidence.");
|
|
716
|
+
}
|
|
717
|
+
assertCheckJobIdentity(attempt, job, workspace.path);
|
|
718
|
+
}
|
|
719
|
+
await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.beforeCommit);
|
|
713
720
|
}
|
|
714
721
|
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.candidateCommit);
|
|
715
722
|
const committed = this.#recordCommitted(attempt);
|
|
716
723
|
return this.#terminalResult("committed", committed, workspace);
|
|
717
724
|
}
|
|
718
|
-
#recordCommitted(attempt) {
|
|
725
|
+
#recordCommitted(attempt, authorize) {
|
|
719
726
|
if (attempt.candidateCommit === undefined) {
|
|
720
727
|
throw new Error(`Committed Integration has no candidate commit: ${attempt.id}.`);
|
|
721
728
|
}
|
|
722
729
|
const candidateCommit = attempt.candidateCommit;
|
|
723
730
|
return this.store.transaction((tx) => {
|
|
731
|
+
authorize?.(tx, attempt.taskId);
|
|
724
732
|
const task = tx.getTask(attempt.taskId);
|
|
725
733
|
if (task === null)
|
|
726
734
|
throw new Error(`Task not found: ${attempt.taskId}.`);
|
|
@@ -769,6 +777,27 @@ export class GitIntegrationService {
|
|
|
769
777
|
};
|
|
770
778
|
}
|
|
771
779
|
}
|
|
780
|
+
function assertCheckJobIdentity(attempt, job, path) {
|
|
781
|
+
if (job.taskId !== attempt.taskId
|
|
782
|
+
|| job.owner.kind !== "integration-attempt" || job.owner.integrationAttemptId !== attempt.id
|
|
783
|
+
|| job.projectId !== attempt.projectId || job.workspace !== path
|
|
784
|
+
|| job.head !== attempt.candidateCommit
|
|
785
|
+
|| (attempt.jobId !== undefined && job.id !== attempt.jobId)
|
|
786
|
+
|| attempt.checkInputDigest === undefined
|
|
787
|
+
|| durableJobIdempotencyKey(job) !== attempt.checkInputDigest) {
|
|
788
|
+
throw new Error("Integration Job identity does not match its admitted Task, source candidate, Project, workspace and check specification.");
|
|
789
|
+
}
|
|
790
|
+
if (job.status === "succeeded") {
|
|
791
|
+
const results = job.result?.steps;
|
|
792
|
+
if (job.result?.outcome !== "succeeded" || results?.length !== job.steps.length
|
|
793
|
+
|| job.steps.some((step, index) => {
|
|
794
|
+
const result = results[index];
|
|
795
|
+
return result?.name !== step.name || result.head !== job.head
|
|
796
|
+
|| result.exitCode !== 0 || result.signal !== null || result.timedOut;
|
|
797
|
+
}))
|
|
798
|
+
throw new Error("Integration Job success lacks exact candidate step evidence.");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
772
801
|
function integrationSummary(attempt) {
|
|
773
802
|
const unchanged = attempt.beforeCommit === attempt.candidateCommit;
|
|
774
803
|
if (attempt.source.kind === "upstream") {
|
|
@@ -796,111 +825,6 @@ function integrationSummary(attempt) {
|
|
|
796
825
|
? `WorkItem ${attempt.source.workItemId} result was already represented; Task head was unchanged.`
|
|
797
826
|
: `Integrated WorkItem ${attempt.source.workItemId} with ${attempt.source.strategy}.`;
|
|
798
827
|
}
|
|
799
|
-
async function isEmptyCherryPick(path, commit) {
|
|
800
|
-
let cherryPickHead;
|
|
801
|
-
try {
|
|
802
|
-
cherryPickHead = await gitLine([
|
|
803
|
-
"-C", path, "rev-parse", "--verify", "CHERRY_PICK_HEAD^{commit}"
|
|
804
|
-
]);
|
|
805
|
-
}
|
|
806
|
-
catch {
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
return cherryPickHead === commit
|
|
810
|
-
&& await gitSucceeds(["-C", path, "diff", "--cached", "--quiet"]);
|
|
811
|
-
}
|
|
812
|
-
async function commitsBetween(repositoryPath, startCommit, resultCommit, workItemId) {
|
|
813
|
-
const commits = (await git([
|
|
814
|
-
"-C", repositoryPath, "rev-list", "--reverse",
|
|
815
|
-
`${startCommit}..${resultCommit}`
|
|
816
|
-
])).trim().split("\n").filter(Boolean);
|
|
817
|
-
const plan = [];
|
|
818
|
-
for (const commit of commits) {
|
|
819
|
-
if (await gitSucceeds([
|
|
820
|
-
"-C", repositoryPath,
|
|
821
|
-
"merge-base", "--is-ancestor", commit, "HEAD"
|
|
822
|
-
]))
|
|
823
|
-
continue;
|
|
824
|
-
plan.push({ label: `WorkItem ${workItemId} commit ${commit}`, commit });
|
|
825
|
-
}
|
|
826
|
-
return plan;
|
|
827
|
-
}
|
|
828
|
-
async function completeCherryPickResolution(path) {
|
|
829
|
-
const unmerged = (await git(["-C", path, "diff", "--name-only", "--diff-filter=U"])).trim();
|
|
830
|
-
if (unmerged.length > 0) {
|
|
831
|
-
throw new Error(`Manual resolution is incomplete: ${unmerged.split("\n").join(", ")}.`);
|
|
832
|
-
}
|
|
833
|
-
let cherryPickHead;
|
|
834
|
-
try {
|
|
835
|
-
cherryPickHead = await gitLine([
|
|
836
|
-
"-C", path, "rev-parse", "--verify", "CHERRY_PICK_HEAD"
|
|
837
|
-
]);
|
|
838
|
-
}
|
|
839
|
-
catch {
|
|
840
|
-
throw new Error("Manual resolution has no active cherry-pick.");
|
|
841
|
-
}
|
|
842
|
-
const emptyResolution = await gitSucceeds(["-C", path, "diff", "--cached", "--quiet"]);
|
|
843
|
-
if (emptyResolution) {
|
|
844
|
-
await git(["-C", path, "cherry-pick", "--skip"]);
|
|
845
|
-
}
|
|
846
|
-
else {
|
|
847
|
-
await git(["-C", path, "-c", "user.name=Yui", "-c", "user.email=yui@local",
|
|
848
|
-
"cherry-pick", "--continue"]);
|
|
849
|
-
}
|
|
850
|
-
return cherryPickHead;
|
|
851
|
-
}
|
|
852
|
-
async function completeMergeResolution(path) {
|
|
853
|
-
const affected = await conflictedPaths(path);
|
|
854
|
-
if (affected.length > 0) {
|
|
855
|
-
throw new Error(`Manual merge resolution is incomplete: ${affected.join(", ")}.`);
|
|
856
|
-
}
|
|
857
|
-
try {
|
|
858
|
-
await gitLine(["-C", path, "rev-parse", "--verify", "MERGE_HEAD"]);
|
|
859
|
-
}
|
|
860
|
-
catch {
|
|
861
|
-
throw new Error("Manual WorkItem resolution has no active merge.");
|
|
862
|
-
}
|
|
863
|
-
await git([
|
|
864
|
-
"-C", path,
|
|
865
|
-
"-c", "user.name=Yui",
|
|
866
|
-
"-c", "user.email=yui@local",
|
|
867
|
-
"commit", "--no-edit"
|
|
868
|
-
]);
|
|
869
|
-
}
|
|
870
|
-
async function continueUpstreamRebase(path) {
|
|
871
|
-
const affected = await conflictedPaths(path);
|
|
872
|
-
if (affected.length > 0) {
|
|
873
|
-
throw new RemoteBaselineConflictError(affected, `${REMOTE_BASELINE_CONFLICT_PREFIX}: ${affected.join(", ")}.`);
|
|
874
|
-
}
|
|
875
|
-
try {
|
|
876
|
-
await gitLine(["-C", path, "rev-parse", "--verify", "REBASE_HEAD"]);
|
|
877
|
-
}
|
|
878
|
-
catch {
|
|
879
|
-
throw new Error("Manual upstream resolution has no active rebase.");
|
|
880
|
-
}
|
|
881
|
-
try {
|
|
882
|
-
await git([
|
|
883
|
-
"-C", path,
|
|
884
|
-
"-c", "user.name=Yui",
|
|
885
|
-
"-c", "user.email=yui@local",
|
|
886
|
-
"-c", "core.editor=true",
|
|
887
|
-
"rebase", "--continue"
|
|
888
|
-
]);
|
|
889
|
-
}
|
|
890
|
-
catch (error) {
|
|
891
|
-
const nextAffected = await conflictedPaths(path);
|
|
892
|
-
if (nextAffected.length > 0) {
|
|
893
|
-
throw new RemoteBaselineConflictError(nextAffected, `${REMOTE_BASELINE_CONFLICT_PREFIX}: ${nextAffected.join(", ")}.`, { cause: error });
|
|
894
|
-
}
|
|
895
|
-
throw error;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
async function conflictedPaths(path) {
|
|
899
|
-
return (await git([
|
|
900
|
-
"-C", path,
|
|
901
|
-
"diff", "--name-only", "--diff-filter=U"
|
|
902
|
-
])).trim().split("\n").filter(Boolean);
|
|
903
|
-
}
|
|
904
828
|
/**
|
|
905
829
|
* Map a terminal check job back to the attempt's CheckResult[] shape. The
|
|
906
830
|
* runner stops at the first failing step, so unreached checks are "skipped"
|
|
@@ -1048,27 +972,41 @@ async function spawnCheck(command, cwd, outputFd, environment) {
|
|
|
1048
972
|
clearTimeout(timeout);
|
|
1049
973
|
return { ...completion, timedOut };
|
|
1050
974
|
}
|
|
1051
|
-
async function
|
|
1052
|
-
const
|
|
975
|
+
async function prepareIntegrationRuntimeHome(runtime, home) {
|
|
976
|
+
const homeDirectory = join(runtime.descriptor.roots.data, "home");
|
|
1053
977
|
try {
|
|
1054
|
-
await mkdir(
|
|
978
|
+
await mkdir(homeDirectory, { mode: 0o700 });
|
|
1055
979
|
}
|
|
1056
980
|
catch (error) {
|
|
1057
981
|
if (!isNodeCode(error, "EEXIST"))
|
|
1058
982
|
throw error;
|
|
1059
983
|
}
|
|
1060
|
-
const homeMetadata = await lstat(
|
|
984
|
+
const homeMetadata = await lstat(homeDirectory);
|
|
1061
985
|
if (!homeMetadata.isDirectory() || homeMetadata.isSymbolicLink()) {
|
|
1062
986
|
throw new Error("Integration runtime HOME is not an owned directory.");
|
|
1063
987
|
}
|
|
988
|
+
// The tmux socket endpoint is the one approved short-path IPC exception; every
|
|
989
|
+
// other temp consumer (TMPDIR/TMP/TEMP) stays on the Home-side runtime tmp.
|
|
990
|
+
const tmuxSocketRoot = integrationTmuxSocketRoot(home);
|
|
991
|
+
try {
|
|
992
|
+
await mkdir(tmuxSocketRoot, { mode: 0o700 });
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
if (!isNodeCode(error, "EEXIST"))
|
|
996
|
+
throw error;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function integrationCheckEnvironment(source, runtime, home) {
|
|
1000
|
+
const homeDirectory = join(runtime.descriptor.roots.data, "home");
|
|
1001
|
+
const tmuxSocketRoot = integrationTmuxSocketRoot(home);
|
|
1064
1002
|
return Object.freeze({
|
|
1065
1003
|
...selectEnvironment(source, INTEGRATION_OPERATIONAL_ENVIRONMENT_NAMES),
|
|
1066
1004
|
PATH: source.PATH || `${dirname(process.execPath)}:/usr/local/bin:/usr/bin:/bin`,
|
|
1067
|
-
HOME:
|
|
1005
|
+
HOME: homeDirectory,
|
|
1068
1006
|
TMPDIR: runtime.descriptor.roots.temporary,
|
|
1069
1007
|
TMP: runtime.descriptor.roots.temporary,
|
|
1070
1008
|
TEMP: runtime.descriptor.roots.temporary,
|
|
1071
|
-
TMUX_TMPDIR:
|
|
1009
|
+
TMUX_TMPDIR: tmuxSocketRoot,
|
|
1072
1010
|
...runtime.environment
|
|
1073
1011
|
});
|
|
1074
1012
|
}
|
|
@@ -1080,18 +1018,32 @@ function isNodeCode(error, code) {
|
|
|
1080
1018
|
}
|
|
1081
1019
|
function defaultIntegrationRuntimeIsolation(home, homeId) {
|
|
1082
1020
|
const controlHome = resolve(home);
|
|
1021
|
+
const runtimeRoot = managedIntegrationRuntimeRoot(controlHome);
|
|
1083
1022
|
return new FileTaskRuntimeIsolation({
|
|
1084
|
-
|
|
1023
|
+
// The integration check's provider data/cache/tmp live in a dedicated Home
|
|
1024
|
+
// partition — the same isolation contract every Task runtime obeys — not in
|
|
1025
|
+
// a system-wide `/tmp` root. Only the tmux socket ENDPOINT stays a short
|
|
1026
|
+
// `/tmp` path (see `integrationCheckEnvironment`) for the `sockaddr_un`
|
|
1027
|
+
// budget; ordinary runtime state is now Yui-managed under Home.
|
|
1028
|
+
runtimeRoot,
|
|
1085
1029
|
pathLayout: "compact",
|
|
1086
1030
|
controlPlane: {
|
|
1087
1031
|
yuiHome: controlHome,
|
|
1032
|
+
managedRuntimeRoot: runtimeRoot,
|
|
1088
1033
|
controllerSocketPath: controllerSocketPath(homeId),
|
|
1089
1034
|
tmuxNamespace: yuiTmuxServerName(controlHome),
|
|
1090
1035
|
globalInstallPaths: [process.execPath]
|
|
1091
1036
|
}
|
|
1092
1037
|
});
|
|
1093
1038
|
}
|
|
1094
|
-
|
|
1039
|
+
/**
|
|
1040
|
+
* Short `/tmp` directory that holds ONLY the integration check's tmux socket
|
|
1041
|
+
* endpoint. tmux uses a `sockaddr_un` path whose length budget cannot absorb a
|
|
1042
|
+
* deep Home path, so this single IPC endpoint is the one approved exception to
|
|
1043
|
+
* the unified-Home contract. All other integration runtime state (data, cache,
|
|
1044
|
+
* ordinary temp) lives in the Home partition from `managedIntegrationRuntimeRoot`.
|
|
1045
|
+
*/
|
|
1046
|
+
function integrationTmuxSocketRoot(home) {
|
|
1095
1047
|
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
1096
1048
|
const homeDigest = createHash("sha256").update(resolve(home)).digest("hex").slice(0, 16);
|
|
1097
1049
|
return join("/tmp", `yi-${uid.toString(36)}-${homeDigest}`);
|
|
@@ -1175,15 +1127,6 @@ async function git(args) {
|
|
|
1175
1127
|
});
|
|
1176
1128
|
}
|
|
1177
1129
|
}
|
|
1178
|
-
async function gitSucceeds(args) {
|
|
1179
|
-
try {
|
|
1180
|
-
await git(args);
|
|
1181
|
-
return true;
|
|
1182
|
-
}
|
|
1183
|
-
catch {
|
|
1184
|
-
return false;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
1130
|
async function gitLine(args) {
|
|
1188
1131
|
const value = (await git(args)).trim();
|
|
1189
1132
|
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(value)) {
|