@zq-silk/yui 2.1.0 → 2.3.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/README.md +8 -2
- package/dist/cli/commandCatalog.js +32 -6
- package/dist/cli/commandDiscovery.js +4 -1
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli.js +23 -3078
- package/dist/commands/taskCommands.js +84 -28
- package/dist/commands/taskIntegrationCommands.js +8 -17
- package/dist/commands/taskUpstreamCommands.js +3 -1
- package/dist/context/runContextPack.js +132 -5
- package/dist/context/sourceRunContext.js +4 -2
- package/dist/context/taskContext.js +6 -1
- package/dist/controlPlaneCli.js +3099 -0
- package/dist/controller/clientRuntime.js +3 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
- package/dist/controller/jobControl.js +21 -0
- package/dist/executor/agentExecutor.js +1 -1
- package/dist/executor/effectiveLaunch.js +18 -5
- package/dist/executor/fileRoleLaunchPlanner.js +15 -19
- package/dist/integration/gitIntegrationService.js +58 -82
- package/dist/job/jobCleanupInspection.js +67 -0
- package/dist/message/messageContinuation.js +7 -2
- package/dist/nativeAgent/agent.js +176 -74
- package/dist/nativeAgent/cliDemo.js +37 -0
- package/dist/nativeAgent/codingTools.js +11 -0
- package/dist/nativeAgent/commandTool.js +215 -0
- package/dist/nativeAgent/compactionDemo.js +158 -0
- package/dist/nativeAgent/composition.js +71 -0
- package/dist/nativeAgent/context/budget.js +44 -0
- package/dist/nativeAgent/context/index.js +339 -0
- package/dist/nativeAgent/context/providerCompressor.js +103 -0
- package/dist/nativeAgent/demo.js +12 -1
- package/dist/nativeAgent/evaluation/cases.js +38 -0
- package/dist/nativeAgent/evaluation/checks.js +91 -0
- package/dist/nativeAgent/evaluation/demo.js +19 -0
- package/dist/nativeAgent/evaluation/files.js +54 -0
- package/dist/nativeAgent/evaluation/fixture.js +36 -0
- package/dist/nativeAgent/evaluation/index.js +239 -0
- package/dist/nativeAgent/executionOwner.js +209 -0
- package/dist/nativeAgent/filePatterns.js +170 -0
- package/dist/nativeAgent/fileToolsSupport.js +202 -0
- package/dist/nativeAgent/index.js +13 -0
- package/dist/nativeAgent/interaction/cli.js +358 -0
- package/dist/nativeAgent/interaction/contracts.js +1 -0
- package/dist/nativeAgent/interaction/index.js +3 -0
- package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
- package/dist/nativeAgent/interaction/renderer.js +34 -0
- package/dist/nativeAgent/localSafety.js +258 -0
- package/dist/nativeAgent/model/anthropicMessages.js +204 -0
- package/dist/nativeAgent/model/chatCompletions.js +210 -0
- package/dist/nativeAgent/model/errors.js +47 -0
- package/dist/nativeAgent/model/gateway.js +423 -0
- package/dist/nativeAgent/model/index.js +7 -0
- package/dist/nativeAgent/model/observationAdapter.js +21 -0
- package/dist/nativeAgent/model/protocols.js +19 -0
- package/dist/nativeAgent/model/responses.js +263 -0
- package/dist/nativeAgent/model/types.js +1 -0
- package/dist/nativeAgent/model/wire.js +73 -0
- package/dist/nativeAgent/observability/index.js +220 -0
- package/dist/nativeAgent/product/catalog.js +82 -0
- package/dist/nativeAgent/product/config.js +295 -0
- package/dist/nativeAgent/product/facts.js +30 -0
- package/dist/nativeAgent/product/index.js +62 -0
- package/dist/nativeAgent/product/location.js +44 -0
- package/dist/nativeAgent/product/runtime.js +276 -0
- package/dist/nativeAgent/product/storage.js +49 -0
- package/dist/nativeAgent/product/tools.js +47 -0
- package/dist/nativeAgent/product/transport.js +54 -0
- package/dist/nativeAgent/projectGuidance/index.js +425 -0
- package/dist/nativeAgent/searchTools.js +305 -0
- package/dist/nativeAgent/session/backends.js +293 -0
- package/dist/nativeAgent/session/catalog.js +97 -0
- package/dist/nativeAgent/session/catalogDemo.js +87 -0
- package/dist/nativeAgent/session/contracts.js +1 -0
- package/dist/nativeAgent/session/format.js +269 -0
- package/dist/nativeAgent/session/index.js +5 -0
- package/dist/nativeAgent/session/location.js +36 -0
- package/dist/nativeAgent/session/sqliteFormat.js +134 -0
- package/dist/nativeAgent/session/store.js +248 -0
- package/dist/nativeAgent/textTools.js +270 -133
- package/dist/nativeAgent/toolManager/executor.js +290 -0
- package/dist/nativeAgent/toolManager/index.js +4 -0
- package/dist/nativeAgent/validation.js +2 -2
- package/dist/repository/gitWorkspace.js +41 -12
- package/dist/repository/taskWorkspaceCoordinator.js +72 -145
- package/dist/repository/taskWorkspacePreparer.js +39 -43
- package/dist/repository/workspaceCleanupInspection.js +88 -7
- package/dist/resources/resourceRegistrar.js +16 -10
- package/dist/runtime/integrationRuntimeIsolation.js +21 -0
- package/dist/runtime/taskRuntimeIsolation.js +91 -21
- package/dist/task/archiveDiagnostics.js +26 -8
- package/dist/task/archivePreflight.js +26 -9
- package/dist/task/leaderArchive.js +9 -15
- package/dist/task/taskAuthority.js +56 -0
- package/dist/web/assets/client/api.js +8 -0
- package/dist/web/assets/client/app.js +74 -9
- package/dist/web/assets/client/components.js +95 -36
- package/dist/web/assets/client/detail.js +110 -64
- package/dist/web/assets/client/dock.js +95 -11
- package/dist/web/assets/client/evidence.js +286 -94
- package/dist/web/assets/client/i18n.js +182 -62
- package/dist/web/assets/client/overview.js +9 -10
- package/dist/web/assets/client/records.js +142 -7
- package/dist/web/assets/client/sections.js +234 -198
- package/dist/web/assets/client/sidebar.js +31 -2
- package/dist/web/assets/icons.js +4 -0
- package/dist/web/assets/shell.js +2 -1
- package/dist/web/assets/styles/components.js +64 -15
- package/dist/web/assets/styles/layout.js +11 -4
- package/dist/web/assets/styles/responsive.js +16 -6
- package/dist/web/assets/styles/tokens.js +1 -1
- package/dist/web/assets/styles/views.js +62 -42
- package/dist/web/webServer.js +12 -1
- package/dist/web/webTaskSurface.js +8 -1
- package/docs/agent-result-consumption.md +16 -0
- package/docs/agent-result-consumption.zh-CN.md +13 -0
- package/docs/examples/agent-offline.mjs +194 -0
- package/docs/native-agent.md +283 -0
- package/docs/observability/README.md +13 -3
- package/docs/observability/README.zh-CN.md +10 -3
- package/docs/release-workflow.md +72 -9
- package/docs/release-workflow.zh-CN.md +55 -6
- package/docs/roles-and-configuration.md +32 -0
- package/docs/roles-and-configuration.zh-CN.md +26 -0
- package/docs/task-delivery.md +36 -8
- package/docs/task-delivery.zh-CN.md +24 -7
- package/i18n/README.zh-CN.md +3 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +9 -0
- package/skills/yui-leader/references/execution.md +2 -1
- package/skills/yui-leader/references/integration.md +20 -0
- package/skills/yui-operator/SKILL.md +4 -0
- package/skills/yui-reviewer/SKILL.md +8 -0
- package/skills/yui-runtime/SKILL.md +11 -0
- package/skills/yui-runtime/references/publication.md +20 -4
- package/skills/yui-worker/SKILL.md +3 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { jobCleanupBlocker } from "../job/jobCleanupInspection.js";
|
|
2
3
|
import { describeCliFailure } from "../errors/cliFailure.js";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { findLiveControllerProcessForHome, inspectLiveControllerProcess } from "../core/controllerProcessIdentity.js";
|
|
@@ -525,8 +526,8 @@ export class FileTaskWorkflowRuntime {
|
|
|
525
526
|
* live, preserving the owner records for Operator recovery.
|
|
526
527
|
*/
|
|
527
528
|
async assertTaskPhysicalResourcesReleased(taskId) {
|
|
528
|
-
const activeJobs = this.store.
|
|
529
|
-
.filter((job) => job.
|
|
529
|
+
const activeJobs = this.store.listDurableJobs(taskId)
|
|
530
|
+
.filter((job) => jobCleanupBlocker(job, this.home) !== undefined);
|
|
530
531
|
if (activeJobs.length > 0) {
|
|
531
532
|
throw new WorkspaceCleanupBlockedError("physical-resource-live", `task:${taskId}`, true, `Task physical resources are not released: ${activeJobs.length} DurableJob(s) are still active: `
|
|
532
533
|
+ activeJobs.map(({ id }) => id).join(", "));
|
|
@@ -267,7 +267,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
267
267
|
default:
|
|
268
268
|
outcome = "obsolete";
|
|
269
269
|
}
|
|
270
|
-
if (outcome === "applied" && !isRunTerminalObservation(input)
|
|
270
|
+
if (outcome === "applied" && !isRunTerminalObservation(input)
|
|
271
|
+
&& input.kind !== "goal.updated" && input.kind !== "goal.cleared") {
|
|
271
272
|
this.persistRuntimeObservation(input, now);
|
|
272
273
|
}
|
|
273
274
|
return outcome;
|
|
@@ -700,7 +701,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
700
701
|
return "applied";
|
|
701
702
|
});
|
|
702
703
|
}
|
|
703
|
-
persistRuntimeObservation(input, now) {
|
|
704
|
+
persistRuntimeObservation(input, now, notifyGoal = true) {
|
|
704
705
|
this.store.transaction((store) => {
|
|
705
706
|
const taskId = input.fence.taskId;
|
|
706
707
|
const events = store.listEvents(taskId);
|
|
@@ -762,8 +763,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
762
763
|
? "provider-continuation-report"
|
|
763
764
|
: "provider-continuation-settled", now);
|
|
764
765
|
}
|
|
765
|
-
if ((input.kind === "goal.cleared"
|
|
766
|
-
|| (input.kind === "goal.updated" && input.payload.goalStatus !== "active"))
|
|
766
|
+
if (notifyGoal && (input.kind === "goal.cleared" || input.kind === "goal.updated")
|
|
767
767
|
&& input.fence.roleName !== "leader") {
|
|
768
768
|
routeRoleEvent(store, observationEvent, input.fence.roleName, input.kind === "goal.cleared"
|
|
769
769
|
? "provider-goal-cleared"
|
|
@@ -2363,6 +2363,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
2363
2363
|
observeProviderGoal(input, now) {
|
|
2364
2364
|
return this.store.transaction((store) => {
|
|
2365
2365
|
const taskId = input.fence.taskId;
|
|
2366
|
+
if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
|
|
2367
|
+
return "applied";
|
|
2366
2368
|
const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
|
|
2367
2369
|
const binding = sessions?.providerBinding;
|
|
2368
2370
|
if (sessions === null || sessions === undefined || binding === null || binding === undefined
|
|
@@ -2383,7 +2385,16 @@ export class FileSchedulerStoreAdapter {
|
|
|
2383
2385
|
? {}
|
|
2384
2386
|
: { tokenBudget: input.payload.goalTokenBudget })
|
|
2385
2387
|
});
|
|
2388
|
+
// Startup/re-attachment observes current state, not necessarily a change.
|
|
2389
|
+
// Compare under the same transaction that persists and routes the fact.
|
|
2390
|
+
if (isDeepStrictEqual(binding.goal, updated.goal)) {
|
|
2391
|
+
// Retain the existing dedup evidence: an old empty observation replayed
|
|
2392
|
+
// after a later real update must not clear that newer state.
|
|
2393
|
+
this.persistRuntimeObservation(input, now, false);
|
|
2394
|
+
return "applied";
|
|
2395
|
+
}
|
|
2386
2396
|
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, now));
|
|
2397
|
+
this.persistRuntimeObservation(input, now);
|
|
2387
2398
|
return "applied";
|
|
2388
2399
|
});
|
|
2389
2400
|
}
|
|
@@ -3127,14 +3138,16 @@ function mapRole(store, role) {
|
|
|
3127
3138
|
? resolveEffectiveLaunch({
|
|
3128
3139
|
role,
|
|
3129
3140
|
purpose,
|
|
3130
|
-
...(workspace === undefined ? {} : { workspace })
|
|
3141
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
3142
|
+
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
3131
3143
|
})
|
|
3132
3144
|
: liveSession !== null && workspace?.owner.type === "task"
|
|
3133
3145
|
? effectiveLaunchWithTaskMainWorkspace(liveSession.effective, workspace)
|
|
3134
3146
|
: liveSession?.effective ?? resolveEffectiveLaunch({
|
|
3135
3147
|
role,
|
|
3136
3148
|
purpose,
|
|
3137
|
-
...(workspace === undefined ? {} : { workspace })
|
|
3149
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
3150
|
+
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
3138
3151
|
});
|
|
3139
3152
|
return {
|
|
3140
3153
|
taskId: role.taskId,
|
|
@@ -19,6 +19,7 @@ import { requireManagedTaskCaller, ManagedRuntimeDriftError } from "../runtime/m
|
|
|
19
19
|
import { CliError } from "../errors/cliError.js";
|
|
20
20
|
import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAssignmentScope.js";
|
|
21
21
|
import { assertContextRecordReadable } from "../context/taskContext.js";
|
|
22
|
+
import { assertTaskProjectWriteAuthority } from "../task/taskAuthority.js";
|
|
22
23
|
export function createDurableJobControl(store) {
|
|
23
24
|
const authority = createJobCallAuthority(store);
|
|
24
25
|
return {
|
|
@@ -167,8 +168,27 @@ export function authorizeJobStart(store, job) {
|
|
|
167
168
|
throw jobDomainError("Job caller binding was revoked; no execution was started.");
|
|
168
169
|
}
|
|
169
170
|
requireJobAssignment(store, job, { scope, role });
|
|
171
|
+
assertLeaderJobWriteScope(store, job, { scope, role });
|
|
170
172
|
validateJobTarget(store, job);
|
|
171
173
|
}
|
|
174
|
+
function assertLeaderJobWriteScope(store, target, caller) {
|
|
175
|
+
if (caller.scope !== "task" || caller.role !== "leader")
|
|
176
|
+
return;
|
|
177
|
+
if (target.owner.kind !== "task" && target.owner.kind !== "integration-attempt") {
|
|
178
|
+
throw jobControlError("UNAUTHORIZED", "Direct Leader delivery does not grant another Assignment or Review workspace. Use the owning executor for its Job.");
|
|
179
|
+
}
|
|
180
|
+
const { taskId, projectId } = target;
|
|
181
|
+
const session = activeLiveRoleAgentSession(store.getTaskRoleSessionSet(taskId, caller.role));
|
|
182
|
+
try {
|
|
183
|
+
assertTaskProjectWriteAuthority(store, {
|
|
184
|
+
YUI_SESSION_SCOPE: "task", YUI_TASK_ID: taskId, YUI_ROLE: caller.role,
|
|
185
|
+
YUI_NATIVE_SESSION_ID: caller.nativeSessionId ?? session?.nativeSessionId
|
|
186
|
+
}, taskId, projectId);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw jobControlError("UNAUTHORIZED", error instanceof Error ? error.message : String(error));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
172
192
|
function requireJobAssignment(store, target, caller) {
|
|
173
193
|
try {
|
|
174
194
|
assertJobAssignmentScope(store, target, caller);
|
|
@@ -244,6 +264,7 @@ function assertNonSecretJobInput(params) {
|
|
|
244
264
|
function validateStartParams(store, params) {
|
|
245
265
|
validateJobTarget(store, params);
|
|
246
266
|
assertCallerAuthorized(store, params.caller, params.taskId);
|
|
267
|
+
assertLeaderJobWriteScope(store, params, params.caller);
|
|
247
268
|
if (params.caller.scope === "task") {
|
|
248
269
|
const session = store.getTaskRoleSessionSet(params.taskId, params.caller.role);
|
|
249
270
|
if (session?.sessions[session.activeAgentId]?.effective.executionAuthority !== "delivery") {
|
|
@@ -317,7 +317,7 @@ export function roleAgentSessionResumeMode(set, agentId, desired) {
|
|
|
317
317
|
if (roleSessionMayContinue(session.effective, desired))
|
|
318
318
|
return "resume";
|
|
319
319
|
throw new Error(`Role Agent session cannot continue under the next launch: ${agentId}. `
|
|
320
|
-
+ "Its Agent, adapter
|
|
320
|
+
+ "Its Agent, adapter, physical workspace or captured authority changed. Explicitly select a new Session "
|
|
321
321
|
+ "after resolving existing execution and resource ownership.");
|
|
322
322
|
}
|
|
323
323
|
export function bindTaskRoleProviderRuntime(set, binding, updatedAt) {
|
|
@@ -117,8 +117,8 @@ export function sameEffectiveLaunch(existing, desired) {
|
|
|
117
117
|
*
|
|
118
118
|
* Only facts that make continuation impossible participate: the Session
|
|
119
119
|
* protocol, the provider identity that owns the conversation, and the physical
|
|
120
|
-
* workspace the Session runs
|
|
121
|
-
* permission, Role context,
|
|
120
|
+
* workspace and captured authority the Session runs with. Launch configuration
|
|
121
|
+
* such as model, effort, provider permission, Role context, and AgentRun-scoped facts like
|
|
122
122
|
* ReviewRound identity or candidate commits shape the next Host process
|
|
123
123
|
* instead of ending the Session; that divergence is acknowledged where the
|
|
124
124
|
* configuration changes and stays visible as launch provenance.
|
|
@@ -151,6 +151,8 @@ function sessionContinuitySnapshot(snapshot) {
|
|
|
151
151
|
return {
|
|
152
152
|
schemaVersion: snapshot.schemaVersion,
|
|
153
153
|
executionAuthority: snapshot.executionAuthority,
|
|
154
|
+
profileAccess: snapshot.profileAccess,
|
|
155
|
+
writeProjectIds: snapshot.writeProjectIds,
|
|
154
156
|
contextProtocolVersion: snapshot.contextProtocolVersion,
|
|
155
157
|
agentId: snapshot.agentId,
|
|
156
158
|
// Two products on one connection plan are two different conversations. A
|
|
@@ -329,6 +331,8 @@ function effectiveLaunchConfigUnchecked(snapshot) {
|
|
|
329
331
|
: claudeConfigFromSnapshot(snapshot);
|
|
330
332
|
}
|
|
331
333
|
function effectiveWriteProjects(input, workspace) {
|
|
334
|
+
if (input.purpose === "planning" || input.executionAuthority === "planning")
|
|
335
|
+
return [];
|
|
332
336
|
if (input.purpose === "review") {
|
|
333
337
|
if (!("taskId" in input.role)) {
|
|
334
338
|
throw new Error("Review launch requires a Task Role.");
|
|
@@ -358,9 +362,18 @@ function effectiveWriteProjects(input, workspace) {
|
|
|
358
362
|
.map(({ projectId }) => projectId), "Workspace writable Project");
|
|
359
363
|
if (!("taskId" in input.role))
|
|
360
364
|
return [];
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
365
|
+
// Direct delivery belongs to the Leader in this Task's adopted main
|
|
366
|
+
// workspace. Assignment scope (including explicit []) still takes priority.
|
|
367
|
+
// Never infer authority from a path, a different owner's workspace or a
|
|
368
|
+
// desired Profile change to an already-frozen Session.
|
|
369
|
+
if (input.workItemWriteProjectIds === undefined) {
|
|
370
|
+
return input.role.name === "leader"
|
|
371
|
+
&& input.role.defaultAccess === "write"
|
|
372
|
+
&& input.workspace?.owner.type === "task"
|
|
373
|
+
&& input.workspace.owner.taskId === input.role.taskId
|
|
374
|
+
? workspaceWrite : [];
|
|
375
|
+
}
|
|
376
|
+
if (input.role.defaultAccess !== "write")
|
|
364
377
|
return [];
|
|
365
378
|
const requested = uniqueIdentities(input.workItemWriteProjectIds, "WorkItem writable Project");
|
|
366
379
|
if (requested.length === 0)
|
|
@@ -216,7 +216,7 @@ export class FileRoleLaunchPlanner {
|
|
|
216
216
|
// attempt was rejected, cancelled, or completed.
|
|
217
217
|
assertProviderConversationReplaceable(sessionSet.providerBinding);
|
|
218
218
|
}
|
|
219
|
-
return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined,
|
|
219
|
+
return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, {
|
|
220
220
|
purpose
|
|
221
221
|
});
|
|
222
222
|
}
|
|
@@ -246,9 +246,9 @@ export class FileRoleLaunchPlanner {
|
|
|
246
246
|
}
|
|
247
247
|
return this.#compile(role, input, { scope: "global" }, input.mode === "new" && role.name === "operator"
|
|
248
248
|
? operatorSessionTitle(this.#now(), this.store.getConfig().timeZone)
|
|
249
|
-
: undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined,
|
|
249
|
+
: undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, { purpose: "execution" });
|
|
250
250
|
}
|
|
251
|
-
#compile(role, input, owner, sessionTitle, knownNativeSessionId,
|
|
251
|
+
#compile(role, input, owner, sessionTitle, knownNativeSessionId, effective, sessionPolicy) {
|
|
252
252
|
const launchRole = effectiveRoleForLaunch(role, effective);
|
|
253
253
|
const binding = activeRoleAgentBinding(launchRole);
|
|
254
254
|
if (binding.agentId !== input.agentId || binding.adapterId !== input.adapterId) {
|
|
@@ -580,7 +580,7 @@ export class FileRoleLaunchPlanner {
|
|
|
580
580
|
childLifecycle: driver.capabilities.lifecycle.providerProcess
|
|
581
581
|
};
|
|
582
582
|
const scopedLaunch = owner.scope === "task"
|
|
583
|
-
? this.#applyWorkspaceScope(
|
|
583
|
+
? this.#applyWorkspaceScope(launch, effective)
|
|
584
584
|
: launch;
|
|
585
585
|
const ordinaryConversationLaunch = withCodexThreadEnvironment(scopedLaunch);
|
|
586
586
|
return {
|
|
@@ -640,20 +640,11 @@ export class FileRoleLaunchPlanner {
|
|
|
640
640
|
}
|
|
641
641
|
return { attemptId: run.attemptId, turnId: run.nativeTurnId };
|
|
642
642
|
}
|
|
643
|
-
#applyWorkspaceScope(
|
|
644
|
-
const workspace = workspaceOverride
|
|
645
|
-
?? (role.name === "leader"
|
|
646
|
-
? this.store.getTaskWorkspace(taskId)
|
|
647
|
-
: this.store.listWorkItems(taskId)
|
|
648
|
-
.find((item) => item.assignee === role.name && item.status === "open") === undefined
|
|
649
|
-
? this.store.getTaskWorkspace(taskId)
|
|
650
|
-
: this.store.getWorkItemWorkspace(taskId, this.store.listWorkItems(taskId).find((item) => item.assignee === role.name && item.status === "open").id));
|
|
651
|
-
if (workspace === null || workspace === undefined)
|
|
652
|
-
return launch;
|
|
643
|
+
#applyWorkspaceScope(launch, effective) {
|
|
653
644
|
return {
|
|
654
645
|
...launch,
|
|
655
646
|
env: {
|
|
656
|
-
...workspaceScopeEnvironment(launch.env,
|
|
647
|
+
...workspaceScopeEnvironment(launch.env, effective)
|
|
657
648
|
}
|
|
658
649
|
};
|
|
659
650
|
}
|
|
@@ -711,16 +702,21 @@ function resolveTaskRoleEffectiveLaunch(store, role, purpose = "execution") {
|
|
|
711
702
|
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
712
703
|
});
|
|
713
704
|
}
|
|
714
|
-
function workspaceScopeEnvironment(environment,
|
|
705
|
+
function workspaceScopeEnvironment(environment, effective) {
|
|
706
|
+
const entries = effective.workspace.entries.map(entry => ({
|
|
707
|
+
...entry,
|
|
708
|
+
access: entry.access === "write" && effective.writeProjectIds.includes(entry.projectId)
|
|
709
|
+
? "write" : "read"
|
|
710
|
+
}));
|
|
715
711
|
return {
|
|
716
712
|
...environment,
|
|
717
|
-
YUI_WRITABLE_PROJECT_IDS: JSON.stringify(
|
|
713
|
+
YUI_WRITABLE_PROJECT_IDS: JSON.stringify(entries
|
|
718
714
|
.filter(({ access }) => access === "write")
|
|
719
715
|
.map(({ projectId }) => projectId)),
|
|
720
|
-
YUI_CONTEXT_PROJECT_IDS: JSON.stringify(
|
|
716
|
+
YUI_CONTEXT_PROJECT_IDS: JSON.stringify(entries
|
|
721
717
|
.filter(({ access }) => access === "read")
|
|
722
718
|
.map(({ projectId }) => projectId)),
|
|
723
|
-
YUI_WORKSPACE_PROJECTS: JSON.stringify(Object.fromEntries(
|
|
719
|
+
YUI_WORKSPACE_PROJECTS: JSON.stringify(Object.fromEntries(entries.map(({ projectId, directory, access, path }) => [
|
|
724
720
|
projectId,
|
|
725
721
|
{ directory, access, path }
|
|
726
722
|
])))
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { lstat, mkdir, open, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, open, realpath, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { selectEnvironment } from "../agent/launchEnvironment.js";
|
|
6
|
-
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
7
6
|
import { durableJobIdempotencyKey } from "../job/durableJob.js";
|
|
8
7
|
import { readRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
9
8
|
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
10
9
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
11
10
|
import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
|
|
12
|
-
import { integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
|
|
11
|
+
import { FileTaskWorkspacePreparer, integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
|
|
13
12
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
13
|
+
import { integrationTmuxSocketRoot } from "../storage/homeLayout.js";
|
|
14
|
+
import { defaultIntegrationRuntimeIsolation } from "../runtime/integrationRuntimeIsolation.js";
|
|
16
15
|
import { advanceTaskProjectCommit } from "../task/task.js";
|
|
17
|
-
import {
|
|
16
|
+
import { taskIntegrationSettlementCheckout, taskIntegrationTargetCheckout } from "../task/taskAuthority.js";
|
|
18
17
|
import { recordGateArtifactReuse } from "../verification/gateArtifact.js";
|
|
19
18
|
import { touchGateArtifact } from "../verification/gateArtifactStore.js";
|
|
20
19
|
import { beginGateVerification, checkResultsFromGateArtifact, checkResultsFromGateJob, gateIdentityForCandidate, lookupReusableGateArtifact, recordGateArtifactFromJob, recordGateArtifactFromStepOutcomes, resolveVerificationGate, runGateStepsInProcess } from "../verification/verificationGateService.js";
|
|
@@ -75,8 +74,12 @@ export class GitIntegrationService {
|
|
|
75
74
|
#resourceRegistrar() {
|
|
76
75
|
return this.#resourceRegistrarValue ??= new ResourceRegistrar(this.home, this.now);
|
|
77
76
|
}
|
|
77
|
+
#targetCheckout(attempt) {
|
|
78
|
+
return taskIntegrationTargetCheckout(this.store, this.environment, attempt.taskId, attempt.projectId, attempt.targetRef);
|
|
79
|
+
}
|
|
78
80
|
async integrate(taskId, integrationId, signal) {
|
|
79
81
|
let initial = requireIntegration(this.store, taskId, integrationId);
|
|
82
|
+
this.#targetCheckout(initial);
|
|
80
83
|
if (!["running", "conflicted", "blocked", "validating"].includes(initial.status)) {
|
|
81
84
|
throw new Error(`Integration cannot continue from ${initial.status}.`);
|
|
82
85
|
}
|
|
@@ -95,6 +98,7 @@ export class GitIntegrationService {
|
|
|
95
98
|
throw new Error("Integration changed before execution admission; read its current record.");
|
|
96
99
|
}
|
|
97
100
|
initial = currentUnderFence;
|
|
101
|
+
this.#targetCheckout(initial);
|
|
98
102
|
const task = this.store.getTask(initial.taskId);
|
|
99
103
|
if (task === null || !task.projectBindings.some(({ projectId }) => projectId === initial.projectId)) {
|
|
100
104
|
throw new Error(`Integration Task Project is unavailable: ${initial.taskId}.`);
|
|
@@ -116,6 +120,7 @@ export class GitIntegrationService {
|
|
|
116
120
|
|| taskRepository === undefined) {
|
|
117
121
|
throw new Error(`Integration Task clone is unavailable: ${task.id}/${project.id}.`);
|
|
118
122
|
}
|
|
123
|
+
await assertTargetCheckout(taskRepository, initial.targetRef, this.#targetCheckout(initial));
|
|
119
124
|
// Issue 08: a Project with a VerificationPlan gates through its plan
|
|
120
125
|
// (bootstrap + L2) and reuses exact-SHA artifacts; an unconfigured
|
|
121
126
|
// Project keeps the existing explicit check path unchanged.
|
|
@@ -193,7 +198,7 @@ export class GitIntegrationService {
|
|
|
193
198
|
// moved or its worktree is dirty, so the check commands never run on a
|
|
194
199
|
// target that cannot be advanced. advanceTargetRef re-verifies both
|
|
195
200
|
// after the checks, so a move during the gate is still fenced at CAS.
|
|
196
|
-
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.beforeCommit);
|
|
201
|
+
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.beforeCommit, this.#targetCheckout(current));
|
|
197
202
|
if (gate !== undefined) {
|
|
198
203
|
return await this.#runVerificationGate(current, workspace, prepared.path, managedWorkspace, taskRepository, gate);
|
|
199
204
|
}
|
|
@@ -221,7 +226,7 @@ export class GitIntegrationService {
|
|
|
221
226
|
checks: checkResults
|
|
222
227
|
}, this.now());
|
|
223
228
|
this.store.saveIntegrationAttempt(task.id, current);
|
|
224
|
-
await advanceTargetRef(taskRepository, current.targetRef, candidateCommit, current.beforeCommit);
|
|
229
|
+
await advanceTargetRef(taskRepository, current.targetRef, candidateCommit, current.beforeCommit, this.#targetCheckout(current));
|
|
225
230
|
const committed = this.#recordCommitted(current);
|
|
226
231
|
return this.#terminalResult("committed", committed, workspace);
|
|
227
232
|
}
|
|
@@ -234,7 +239,7 @@ export class GitIntegrationService {
|
|
|
234
239
|
if (target === current.candidateCommit) {
|
|
235
240
|
try {
|
|
236
241
|
await assertIntegrationCandidate(workspace.path, current.candidateCommit, workspace.branch);
|
|
237
|
-
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.candidateCommit);
|
|
242
|
+
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.candidateCommit, this.#targetCheckout(current));
|
|
238
243
|
const committed = this.#recordCommitted(current);
|
|
239
244
|
return this.#terminalResult("committed", committed, workspace);
|
|
240
245
|
}
|
|
@@ -307,8 +312,21 @@ export class GitIntegrationService {
|
|
|
307
312
|
}
|
|
308
313
|
const target = await resolveRef(mainEntry.path, attempt.targetRef);
|
|
309
314
|
if (target === attempt.candidateCommit && target !== attempt.beforeCommit) {
|
|
310
|
-
|
|
311
|
-
|
|
315
|
+
if (attempt.checks.some(check => check.outcome === "failed")
|
|
316
|
+
|| attempt.jobId !== undefined
|
|
317
|
+
&& jobs.find(job => job.id === attempt.jobId).status !== "succeeded") {
|
|
318
|
+
throw new Error("Delivered Integration lacks successful candidate check evidence; inspect before settlement.");
|
|
319
|
+
}
|
|
320
|
+
await assertIntegrationCandidate(workspace.root, attempt.candidateCommit, workspaceEntry.branch);
|
|
321
|
+
assertRecordedSourceCandidate(attempt, { path: workspace.root, branch: workspaceEntry.branch });
|
|
322
|
+
await assertTargetReadyForChecks(mainEntry.path, attempt.targetRef, target, taskIntegrationSettlementCheckout(this.store, this.environment, taskId, attempt.projectId, attempt.targetRef));
|
|
323
|
+
return this.#recordCommitted(attempt, (tx, id) => {
|
|
324
|
+
authorize(tx, id);
|
|
325
|
+
taskIntegrationSettlementCheckout(tx, this.environment, id, attempt.projectId, attempt.targetRef);
|
|
326
|
+
if (JSON.stringify(requireIntegration(tx, id, integrationId)) !== JSON.stringify(attempt)) {
|
|
327
|
+
throw new Error("Integration changed before delivered settlement; inspect its current record.");
|
|
328
|
+
}
|
|
329
|
+
});
|
|
312
330
|
}
|
|
313
331
|
if (target !== attempt.beforeCommit) {
|
|
314
332
|
throw new Error(`Target moved to ${target}; cannot prove this Integration is unadvanced. Inspect before settlement.`);
|
|
@@ -350,12 +368,9 @@ export class GitIntegrationService {
|
|
|
350
368
|
throw new Error("Integration changed before cleanup; read its current record.");
|
|
351
369
|
}
|
|
352
370
|
integration = current;
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (activeJob !== undefined) {
|
|
357
|
-
throw new Error(`Integration ${integration.id} has an active DurableJob: ${activeJob.id}/${activeJob.status}.`);
|
|
358
|
-
}
|
|
371
|
+
const managedWorkspace = this.store.getIntegrationWorkspace(integration.taskId, integration.id);
|
|
372
|
+
if (managedWorkspace === null)
|
|
373
|
+
return "missing";
|
|
359
374
|
const task = this.store.getTask(integration.taskId);
|
|
360
375
|
if (task === null || !task.projectBindings.some(({ projectId }) => projectId === integration.projectId)) {
|
|
361
376
|
throw new Error(`Integration Task Project is unavailable: ${integration.id}.`);
|
|
@@ -367,42 +382,8 @@ export class GitIntegrationService {
|
|
|
367
382
|
if (taskRepository === undefined) {
|
|
368
383
|
throw new Error(`Integration Task clone is unavailable: ${task.id}/${project.id}.`);
|
|
369
384
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const runtime = this.#runtimePreparation(integration, managedWorkspace);
|
|
373
|
-
this.runtimeIsolation.cleanup(runtime, integration.status === "committed" ? "completion" : "failure");
|
|
374
|
-
}
|
|
375
|
-
const recorded = managedWorkspace?.entries[0];
|
|
376
|
-
const directory = recorded?.directory
|
|
377
|
-
?? task.projectBindings.find(({ projectId }) => projectId === integration.projectId)?.directory;
|
|
378
|
-
if (directory === undefined) {
|
|
379
|
-
throw new Error(`Integration Project binding is unavailable: ${task.id}/${project.id}.`);
|
|
380
|
-
}
|
|
381
|
-
const result = await this.git.removeIntegrationWorktree({
|
|
382
|
-
repositoryPath: taskRepository,
|
|
383
|
-
container: recorded !== undefined
|
|
384
|
-
? dirname(recorded.path)
|
|
385
|
-
: integrationWorkspaceRoot(this.home, task.id, integration.id),
|
|
386
|
-
directory,
|
|
387
|
-
taskSegment: taskWorkspaceRefSegment(task),
|
|
388
|
-
integrationId: integration.id,
|
|
389
|
-
discardChanges: integration.status === "failed"
|
|
390
|
-
});
|
|
391
|
-
if (result !== "dirty") {
|
|
392
|
-
if (managedWorkspace !== null) {
|
|
393
|
-
this.#resourceRegistrar().markWorkspaceDeleted(managedWorkspace);
|
|
394
|
-
}
|
|
395
|
-
await rm(integrationCheckDirectory(this.home, task.id, integration.id), {
|
|
396
|
-
recursive: true,
|
|
397
|
-
force: true
|
|
398
|
-
});
|
|
399
|
-
this.store.removeManagedWorkspace({
|
|
400
|
-
type: "integration-attempt",
|
|
401
|
-
taskId: integration.taskId,
|
|
402
|
-
integrationAttemptId: integration.id
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
return result;
|
|
385
|
+
return await new FileTaskWorkspacePreparer(this.home, this.store, this.git, this.now)
|
|
386
|
+
.cleanupIntegrationWorkspace(task.id, integration.id, { runtimeIsolation: this.runtimeIsolation });
|
|
406
387
|
}
|
|
407
388
|
finally {
|
|
408
389
|
release();
|
|
@@ -419,7 +400,7 @@ export class GitIntegrationService {
|
|
|
419
400
|
throw new Error(`Integration check admission requires a running, proven candidate: ${attempt.id}.`);
|
|
420
401
|
}
|
|
421
402
|
await assertIntegrationCandidate(path, attempt.candidateCommit, workspace.branch);
|
|
422
|
-
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit);
|
|
403
|
+
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit, this.#targetCheckout(attempt));
|
|
423
404
|
const { runtime, head, steps, environment, inputDigest } = await this.#checkSpecification(attempt, managedWorkspace, gate);
|
|
424
405
|
if (attempt.checkInputDigest !== undefined && attempt.checkInputDigest !== inputDigest) {
|
|
425
406
|
throw new Error("Integration check conditions changed since admission; inspect the original Job, then abort or start a new attempt.");
|
|
@@ -479,7 +460,7 @@ export class GitIntegrationService {
|
|
|
479
460
|
const job = await this.jobPort.getJob(attempt.taskId, attempt.jobId);
|
|
480
461
|
assertCheckJobIdentity(attempt, job, path);
|
|
481
462
|
await assertIntegrationCandidate(path, job.head, workspace.branch);
|
|
482
|
-
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit);
|
|
463
|
+
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit, this.#targetCheckout(attempt));
|
|
483
464
|
if (gate?.planDigest !== attempt.gatePlanDigest
|
|
484
465
|
|| gate?.toolchainDigest !== attempt.gateToolchainDigest) {
|
|
485
466
|
throw new Error("Integration verification plan/toolchain changed; the original Job cannot prove the current gate.");
|
|
@@ -642,7 +623,7 @@ export class GitIntegrationService {
|
|
|
642
623
|
checks
|
|
643
624
|
}, this.now());
|
|
644
625
|
this.store.saveIntegrationAttempt(attempt.taskId, validating);
|
|
645
|
-
await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.beforeCommit);
|
|
626
|
+
await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.beforeCommit, this.#targetCheckout(validating));
|
|
646
627
|
const committed = this.#recordCommitted(validating);
|
|
647
628
|
return this.#terminalResult("committed", committed, workspace);
|
|
648
629
|
}
|
|
@@ -709,9 +690,9 @@ export class GitIntegrationService {
|
|
|
709
690
|
}
|
|
710
691
|
assertCheckJobIdentity(attempt, job, workspace.path);
|
|
711
692
|
}
|
|
712
|
-
await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.beforeCommit);
|
|
693
|
+
await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.beforeCommit, this.#targetCheckout(attempt));
|
|
713
694
|
}
|
|
714
|
-
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.candidateCommit);
|
|
695
|
+
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.candidateCommit, this.#targetCheckout(attempt));
|
|
715
696
|
const committed = this.#recordCommitted(attempt);
|
|
716
697
|
return this.#terminalResult("committed", committed, workspace);
|
|
717
698
|
}
|
|
@@ -1004,26 +985,6 @@ function isNodeCode(error, code) {
|
|
|
1004
985
|
&& "code" in error
|
|
1005
986
|
&& error.code === code;
|
|
1006
987
|
}
|
|
1007
|
-
function defaultIntegrationRuntimeIsolation(home, homeId) {
|
|
1008
|
-
const controlHome = resolve(home);
|
|
1009
|
-
const runtimeRoot = managedIntegrationRuntimeRoot(controlHome);
|
|
1010
|
-
return new FileTaskRuntimeIsolation({
|
|
1011
|
-
// The integration check's provider data/cache/tmp live in a dedicated Home
|
|
1012
|
-
// partition — the same isolation contract every Task runtime obeys — not in
|
|
1013
|
-
// a system-wide `/tmp` root. Only the tmux socket ENDPOINT stays a short
|
|
1014
|
-
// `/tmp` path (see `integrationCheckEnvironment`) for the `sockaddr_un`
|
|
1015
|
-
// budget; ordinary runtime state is now Yui-managed under Home.
|
|
1016
|
-
runtimeRoot,
|
|
1017
|
-
pathLayout: "compact",
|
|
1018
|
-
controlPlane: {
|
|
1019
|
-
yuiHome: controlHome,
|
|
1020
|
-
managedRuntimeRoot: runtimeRoot,
|
|
1021
|
-
controllerSocketPath: controllerSocketPath(homeId),
|
|
1022
|
-
tmuxNamespace: yuiTmuxServerName(controlHome),
|
|
1023
|
-
globalInstallPaths: [process.execPath]
|
|
1024
|
-
}
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
988
|
/**
|
|
1028
989
|
* Reuse is enabled only for an immutable installed release. Development
|
|
1029
990
|
* checkouts have no content-addressed release identity, so they return.
|
|
@@ -1138,12 +1099,12 @@ async function resolveRef(repositoryPath, ref) {
|
|
|
1138
1099
|
* {@link advanceTargetRef} re-verifies both before the CAS, so a target that
|
|
1139
1100
|
* moves during the gate is still fenced.
|
|
1140
1101
|
*/
|
|
1141
|
-
async function assertTargetReadyForChecks(repositoryPath, targetRef, expectedHead) {
|
|
1102
|
+
async function assertTargetReadyForChecks(repositoryPath, targetRef, expectedHead, expectedCheckout) {
|
|
1142
1103
|
const current = await resolveRef(repositoryPath, targetRef);
|
|
1143
1104
|
if (current !== expectedHead) {
|
|
1144
1105
|
throw new Error(`Target moved to ${current}; expected ${expectedHead}.`);
|
|
1145
1106
|
}
|
|
1146
|
-
const checkedOutPaths = await
|
|
1107
|
+
const checkedOutPaths = await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
|
|
1147
1108
|
if (checkedOutPaths.length > 1) {
|
|
1148
1109
|
throw new Error(`Integration target is checked out in multiple worktrees: ${targetRef}.`);
|
|
1149
1110
|
}
|
|
@@ -1156,9 +1117,18 @@ async function assertTargetReadyForChecks(repositoryPath, targetRef, expectedHea
|
|
|
1156
1117
|
}
|
|
1157
1118
|
}
|
|
1158
1119
|
}
|
|
1159
|
-
async function
|
|
1120
|
+
async function assertTargetCheckout(repositoryPath, targetRef, expectedCheckout) {
|
|
1121
|
+
const paths = await checkedOutWorktreePaths(repositoryPath, fullTargetRef(targetRef));
|
|
1122
|
+
if (expectedCheckout !== undefined && (await realpath(repositoryPath) !== await realpath(expectedCheckout)
|
|
1123
|
+
|| paths.length !== 1 || await realpath(paths[0]) !== await realpath(expectedCheckout)
|
|
1124
|
+
|| (await git(["-C", expectedCheckout, "symbolic-ref", "HEAD"])).trim() !== fullTargetRef(targetRef))) {
|
|
1125
|
+
throw new Error("Managed Leader Integration target must be physically checked out in its captured Task-main workspace.");
|
|
1126
|
+
}
|
|
1127
|
+
return paths;
|
|
1128
|
+
}
|
|
1129
|
+
async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expectedHead, expectedCheckout) {
|
|
1160
1130
|
const ref = fullTargetRef(targetRef);
|
|
1161
|
-
const checkedOutPaths = await
|
|
1131
|
+
const checkedOutPaths = await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
|
|
1162
1132
|
if (checkedOutPaths.length === 0) {
|
|
1163
1133
|
await git([
|
|
1164
1134
|
"-C", repositoryPath, "update-ref",
|
|
@@ -1182,6 +1152,9 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
|
|
|
1182
1152
|
if (current !== expectedHead) {
|
|
1183
1153
|
throw new Error(`Target moved to ${current}; expected ${expectedHead}.`);
|
|
1184
1154
|
}
|
|
1155
|
+
if (expectedCheckout !== undefined) {
|
|
1156
|
+
await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
|
|
1157
|
+
}
|
|
1185
1158
|
// The candidate may be a rebased upstream result and therefore need not be
|
|
1186
1159
|
// a descendant of the old Task head. Advance the branch with a real
|
|
1187
1160
|
// compare-and-swap, then update only this checkout's index and worktree.
|
|
@@ -1193,6 +1166,9 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
|
|
|
1193
1166
|
candidateCommit,
|
|
1194
1167
|
expectedHead
|
|
1195
1168
|
]);
|
|
1169
|
+
if (expectedCheckout !== undefined) {
|
|
1170
|
+
await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
|
|
1171
|
+
}
|
|
1196
1172
|
await git(["-C", checkout, "read-tree", "--reset", "-u", candidateCommit]);
|
|
1197
1173
|
const advanced = await resolveRef(repositoryPath, targetRef);
|
|
1198
1174
|
if (advanced !== candidateCommit) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
2
|
+
import { processGenerationIsLive } from "../core/fileLockOwner.js";
|
|
3
|
+
import { scanProcessPathRefs } from "../resources/liveReferences.js";
|
|
4
|
+
import { inspectTaskRuntimeCleanupClaims, parseTaskRuntimeIsolationDescriptor, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR } from "../runtime/taskRuntimeIsolation.js";
|
|
5
|
+
/** Result uncertainty and physical occupancy are different facts. Reuse the
|
|
6
|
+
* recorded process generation and resource scanner, without rewriting the Job
|
|
7
|
+
* or treating acknowledgement as stop evidence. Every cleanup reads this anew.
|
|
8
|
+
*/
|
|
9
|
+
export function jobCleanupBlocker(job, home, phase = "before-removal") {
|
|
10
|
+
if (job.status === "queued" || job.status === "running")
|
|
11
|
+
return `DurableJob is ${job.status}.`;
|
|
12
|
+
if (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined) {
|
|
13
|
+
return "Unknown Job result requires explicit acknowledgement.";
|
|
14
|
+
}
|
|
15
|
+
if (job.status !== "unknown-needs-attention" && job.startedAt === undefined
|
|
16
|
+
&& job.process === undefined && job.operation.effect === "none")
|
|
17
|
+
return undefined;
|
|
18
|
+
if (job.process === undefined || home === undefined) {
|
|
19
|
+
return "Job has no exact process/Home evidence for physical inspection.";
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
if (processGenerationIsLive(job.process.pid, job.process.startIdentity)) {
|
|
23
|
+
return "The exact Job runner process is still live.";
|
|
24
|
+
}
|
|
25
|
+
// A reusable idle Role can share this workspace. Its exact-owner stop is
|
|
26
|
+
// allowed only after result/runner checks; path references must then be
|
|
27
|
+
// rechecked before any child deletion or ordinary archive commit.
|
|
28
|
+
if (phase === "before-role-stop")
|
|
29
|
+
return undefined;
|
|
30
|
+
// Steps may outlive a killed runner. Their cwd, logs and isolated runtime
|
|
31
|
+
// paths remain protective references; no process is signalled here.
|
|
32
|
+
const paths = [job.workspace, join(resolve(home), "artifacts", "jobs", job.taskId, job.id),
|
|
33
|
+
...["TMPDIR", "XDG_DATA_HOME", "XDG_CACHE_HOME"].flatMap(key => {
|
|
34
|
+
const path = job.env[key];
|
|
35
|
+
return path !== undefined && isAbsolute(path) ? [path] : [];
|
|
36
|
+
})];
|
|
37
|
+
const serialized = job.env[YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR];
|
|
38
|
+
if (serialized !== undefined) {
|
|
39
|
+
const descriptor = parseTaskRuntimeIsolationDescriptor(serialized);
|
|
40
|
+
const owner = descriptor.workspace.owner;
|
|
41
|
+
if (descriptor.taskId !== job.taskId || descriptor.workspace.root !== job.workspace
|
|
42
|
+
|| owner.type !== job.owner.kind
|
|
43
|
+
|| (owner.type === "integration-attempt" && job.owner.kind === "integration-attempt"
|
|
44
|
+
&& owner.integrationAttemptId !== job.owner.integrationAttemptId)
|
|
45
|
+
|| (owner.type === "work-item" && job.owner.kind === "work-item"
|
|
46
|
+
&& owner.workItemId !== job.owner.workItemId)) {
|
|
47
|
+
return "Job runtime descriptor does not match its exact owner/workspace.";
|
|
48
|
+
}
|
|
49
|
+
const claims = inspectTaskRuntimeCleanupClaims(descriptor);
|
|
50
|
+
const uncertain = claims.find(claim => claim.ownership !== "owned" || claim.state !== "inactive");
|
|
51
|
+
if (uncertain !== undefined)
|
|
52
|
+
return `Job runtime cleanup claim is unverified: ${uncertain.id}.`;
|
|
53
|
+
// cwd/fd references follow rename, unlike the recorded environment.
|
|
54
|
+
// Use the same exact claim inventory as deletion, before archive commits.
|
|
55
|
+
paths.push(descriptor.roots.runtime, ...claims.map(claim => claim.id));
|
|
56
|
+
}
|
|
57
|
+
const scan = scanProcessPathRefs(paths);
|
|
58
|
+
if (scan.diagnostics.some(diagnostic => diagnostic.severity === "error")
|
|
59
|
+
|| [...scan.refs.values()].some(refs => refs.length > 0)) {
|
|
60
|
+
return "Job workspace/log/runtime references are still live or unverified.";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return `Job physical inspection is unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|