@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,10 +1,10 @@
|
|
|
1
|
-
import { lstat } from "node:fs/promises";
|
|
2
1
|
import { isDeepStrictEqual } from "node:util";
|
|
3
2
|
import { isArchivePersistenceFailure, recordArchiveCleanup } from "../task/archiveDiagnostics.js";
|
|
4
3
|
import { archiveExecutionChecks, archiveSettlementChecks } from "../task/archivePreflight.js";
|
|
5
|
-
import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
|
|
6
4
|
import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
|
|
7
5
|
import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
|
|
6
|
+
import { withResourceRegistry } from "../resources/resourceRegistryStore.js";
|
|
7
|
+
import { jobCleanupBlocker } from "../job/jobCleanupInspection.js";
|
|
8
8
|
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
9
9
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
10
10
|
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
@@ -97,8 +97,9 @@ export class TaskWorkspaceCoordinator {
|
|
|
97
97
|
if (state === "dirty")
|
|
98
98
|
return "dirty";
|
|
99
99
|
this.#assertWorkItemRuntimeQuiescent(item);
|
|
100
|
-
this.#assertNoActiveWorkItemDurableJobs(item);
|
|
100
|
+
this.#assertNoActiveWorkItemDurableJobs(item, "before-role-stop");
|
|
101
101
|
await this.#stopLiveRoles(item.taskId, this.#workItemRoleNames(item));
|
|
102
|
+
this.#assertNoActiveWorkItemDurableJobs(item);
|
|
102
103
|
const laneCleanup = await this.preparer.cleanupExecutionLaneWorkspacesForWorkItem(item.taskId, item.id);
|
|
103
104
|
if (laneCleanup === "dirty")
|
|
104
105
|
return "dirty";
|
|
@@ -162,30 +163,32 @@ export class TaskWorkspaceCoordinator {
|
|
|
162
163
|
releaseMaintenance();
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
/** Business/runtime admission only. Physical workspaces may safely remain;
|
|
167
|
+
* their removal uses the same foreground cleanup after ordinary or force archive.
|
|
168
|
+
*/
|
|
169
|
+
async prepareTaskForArchive(taskId) {
|
|
170
|
+
const task = this.store.getTask(taskId);
|
|
171
|
+
if (task === null)
|
|
172
|
+
throw new Error(`Task not found: ${taskId}.`);
|
|
173
|
+
if (task.status !== "completed" && task.status !== "cancelled") {
|
|
174
|
+
throw new Error(`Task must be completed or retired before archive: ${task.id}.`);
|
|
175
|
+
}
|
|
176
|
+
const settlement = [...archiveSettlementChecks(this.store, task),
|
|
177
|
+
...archiveExecutionChecks(this.store, taskId, this.preparer.home, "before-role-stop")];
|
|
178
|
+
if (settlement.length > 0)
|
|
179
|
+
throw new CleanupInspectionError(settlement);
|
|
180
|
+
const managedWorkspaces = [...this.store.listManagedWorkspaces(task.id)]
|
|
181
|
+
.sort((left, right) => managedWorkspaceKey(left.owner)
|
|
182
|
+
.localeCompare(managedWorkspaceKey(right.owner)));
|
|
183
|
+
// Fence Project preparation while stopping exact Task runtimes; do not
|
|
184
|
+
// mutate Project paths until after the independent archive commit.
|
|
185
|
+
const projectIds = new Set(task.projectBindings.map(({ projectId }) => projectId));
|
|
186
|
+
for (const workspace of managedWorkspaces) {
|
|
187
|
+
for (const entry of workspace.entries)
|
|
188
|
+
projectIds.add(entry.projectId);
|
|
189
|
+
}
|
|
190
|
+
const releaseMaintenance = await acquireProjectMaintenanceLocks(this.preparer.home, projectIds);
|
|
167
191
|
try {
|
|
168
|
-
const task = this.store.getTask(taskId);
|
|
169
|
-
if (task === null)
|
|
170
|
-
throw new Error(`Task not found: ${taskId}.`);
|
|
171
|
-
if (task.status !== "completed" && task.status !== "cancelled") {
|
|
172
|
-
throw new Error(`Task must be completed or retired before archive cleanup: ${task.id}.`);
|
|
173
|
-
}
|
|
174
|
-
const settlement = [...archiveSettlementChecks(this.store, task), ...archiveExecutionChecks(this.store, taskId)];
|
|
175
|
-
if (settlement.length > 0)
|
|
176
|
-
throw new CleanupInspectionError(settlement);
|
|
177
|
-
const managedWorkspaces = [...this.store.listManagedWorkspaces(task.id)]
|
|
178
|
-
.sort((left, right) => managedWorkspaceKey(left.owner)
|
|
179
|
-
.localeCompare(managedWorkspaceKey(right.owner)));
|
|
180
|
-
// Archive cleanup removes worktrees from every Project the Task uses:
|
|
181
|
-
// hold each Project's maintenance fence so the Controller defers
|
|
182
|
-
// preparation and no Project maintenance or Task archive interleaves.
|
|
183
|
-
const projectIds = new Set(task.projectBindings.map(({ projectId }) => projectId));
|
|
184
|
-
for (const workspace of managedWorkspaces) {
|
|
185
|
-
for (const entry of workspace.entries)
|
|
186
|
-
projectIds.add(entry.projectId);
|
|
187
|
-
}
|
|
188
|
-
releaseMaintenance = await acquireProjectMaintenanceLocks(this.preparer.home, projectIds);
|
|
189
192
|
this.#assertTaskArchiveLifecycle(task);
|
|
190
193
|
const currentWorkspaces = [...this.store.listManagedWorkspaces(task.id)]
|
|
191
194
|
.sort((left, right) => managedWorkspaceKey(left.owner)
|
|
@@ -193,111 +196,43 @@ export class TaskWorkspaceCoordinator {
|
|
|
193
196
|
if (!isDeepStrictEqual(currentWorkspaces, managedWorkspaces)) {
|
|
194
197
|
throw new Error(`Task workspaces changed while waiting for archive cleanup: ${taskId}.`);
|
|
195
198
|
}
|
|
196
|
-
const laneWorkspaces = managedWorkspaces.filter(({ owner }) => owner.type === "execution-lane");
|
|
197
|
-
const integrationWorkspaces = managedWorkspaces.filter(({ owner }) => owner.type === "integration-attempt");
|
|
198
|
-
const workspaces = managedWorkspaces
|
|
199
|
-
.filter(({ owner }) => owner.type === "work-item");
|
|
200
|
-
const workItems = workspaces.map((workspace) => {
|
|
201
|
-
const workItemId = workspace.owner.type === "work-item"
|
|
202
|
-
? workspace.owner.workItemId
|
|
203
|
-
: "";
|
|
204
|
-
const item = this.store.getWorkItem(task.id, workItemId);
|
|
205
|
-
if (item === null)
|
|
206
|
-
throw new Error(`Work item not found: ${task.id}/${workItemId}.`);
|
|
207
|
-
if (item.status !== "accepted" && item.status !== "retired") {
|
|
208
|
-
throw new Error(`Work item must be completed or retired before archive cleanup: ${item.id}.`);
|
|
209
|
-
}
|
|
210
|
-
return item;
|
|
211
|
-
});
|
|
212
199
|
const allWorkItems = [...this.store.listWorkItems(task.id)]
|
|
213
200
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
214
201
|
const allReviewRounds = [...this.store.listReviewRounds(task.id)]
|
|
215
202
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
216
|
-
const reviewRounds = allReviewRounds.filter((round) => (round.workspace !== undefined && round.workspaceDisposition?.kind !== "removed"));
|
|
217
203
|
const snapshot = {
|
|
218
204
|
task,
|
|
219
205
|
managedWorkspaces,
|
|
220
206
|
workItems: allWorkItems,
|
|
221
207
|
reviewRounds: allReviewRounds
|
|
222
208
|
};
|
|
223
|
-
const checks = [];
|
|
224
|
-
for (const workspace of managedWorkspaces) {
|
|
225
|
-
const workspaceDisposition = workspace.owner.type === "work-item"
|
|
226
|
-
&& this.store.getWorkItem(task.id, workspace.owner.workItemId)?.status === "retired" ? "abandoned" : disposition;
|
|
227
|
-
const inspected = await this.preparer.inspectWorkspaceCleanup(workspace, workspaceDisposition);
|
|
228
|
-
// Child worktrees are removed first below. Their Git registrations
|
|
229
|
-
// necessarily exist now; the clone's removal boundary rechecks them
|
|
230
|
-
// after every child cleanup and never removes an unresolved owner.
|
|
231
|
-
checks.push(...inspected.filter(c => !(workspace.owner.type === "task"
|
|
232
|
-
&& c.reason === "git-worktree-registrations")));
|
|
233
|
-
}
|
|
234
|
-
if (checks.length > 0)
|
|
235
|
-
throw new CleanupInspectionError(checks);
|
|
236
209
|
const roleNames = this.store.listRoles(taskId).map(({ name }) => name);
|
|
237
210
|
await this.#stopLiveRoles(taskId, roleNames);
|
|
238
211
|
await this.runtime.releaseTaskTerminals(task.id);
|
|
239
|
-
this
|
|
240
|
-
|
|
241
|
-
this.#assertTaskArchiveLifecycle(task);
|
|
242
|
-
if (workspace.owner.type === "execution-lane") {
|
|
243
|
-
await this.preparer.cleanupExecutionLaneWorkspace(task.id, workspace.owner.executionGroupId, workspace.owner.executionLaneId);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
for (const workspace of integrationWorkspaces) {
|
|
247
|
-
this.#assertTaskArchiveLifecycle(task);
|
|
248
|
-
if (workspace.owner.type === "integration-attempt") {
|
|
249
|
-
await this.preparer.cleanupIntegrationWorkspace(task.id, workspace.owner.integrationAttemptId, { preserveChanges: true });
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
for (const round of reviewRounds) {
|
|
253
|
-
this.#assertTaskArchiveLifecycle(task);
|
|
254
|
-
await this.preparer.cleanupReviewRoundWorkspace(task.id, round.id);
|
|
212
|
+
if (this.runtime.assertTaskPhysicalResourcesReleased === undefined) {
|
|
213
|
+
throw new Error("Archive requires exact physical runtime inspection.");
|
|
255
214
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return await this.preparer.cleanupTaskForArchive(taskId, disposition);
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
const task = this.store.getTask(taskId);
|
|
265
|
-
return {
|
|
266
|
-
taskId,
|
|
267
|
-
status: error instanceof CleanupInspectionError && error.checks.every(c => c.reason === "dirty-worktree")
|
|
268
|
-
? "retained-dirty" : "failed",
|
|
269
|
-
...(task?.cwd === undefined ? {} : { path: task.cwd }),
|
|
270
|
-
error: error instanceof Error ? error.message : String(error),
|
|
271
|
-
...(error instanceof CleanupInspectionError
|
|
272
|
-
? { checks: error.checks, reason: error.checks[0]?.reason ?? "cleanup-failed",
|
|
273
|
-
resource: error.checks[0]?.resource ?? `task:${taskId}`, retryable: true }
|
|
274
|
-
: error instanceof WorkspaceCleanupBlockedError
|
|
275
|
-
? {
|
|
276
|
-
reason: error.reason,
|
|
277
|
-
resource: error.resource,
|
|
278
|
-
retryable: error.retryable
|
|
279
|
-
}
|
|
280
|
-
: {
|
|
281
|
-
reason: "cleanup-failed",
|
|
282
|
-
resource: `task:${taskId}`,
|
|
283
|
-
retryable: true
|
|
284
|
-
})
|
|
285
|
-
};
|
|
215
|
+
await this.runtime.assertTaskPhysicalResourcesReleased(task.id);
|
|
216
|
+
this.#assertTaskArchiveSnapshot(snapshot);
|
|
217
|
+
const rechecked = [...archiveSettlementChecks(this.store, task), ...archiveExecutionChecks(this.store, taskId, this.preparer.home)];
|
|
218
|
+
if (rechecked.length > 0)
|
|
219
|
+
throw new CleanupInspectionError(rechecked);
|
|
286
220
|
}
|
|
287
221
|
finally {
|
|
288
|
-
releaseMaintenance
|
|
222
|
+
releaseMaintenance();
|
|
289
223
|
}
|
|
290
224
|
}
|
|
291
|
-
/** Best-effort cleanup only after
|
|
225
|
+
/** Best-effort cleanup only after authorized archive admission has committed.
|
|
292
226
|
* This is one foreground attempt, not a retry worker. Each independent result
|
|
293
227
|
* is durable before continuing, and a failed audit write really fails.
|
|
294
228
|
*/
|
|
295
229
|
async cleanupArchivedTask(taskId, disposition) {
|
|
296
230
|
const task = this.store.getTask(taskId);
|
|
297
|
-
if (task?.status !== "archived"
|
|
298
|
-
|
|
299
|
-
throw new Error(`Force archive must commit before cleanup: ${taskId}.`);
|
|
231
|
+
if (task?.status !== "archived") {
|
|
232
|
+
throw new Error(`Archive must commit before cleanup: ${taskId}.`);
|
|
300
233
|
}
|
|
234
|
+
recordArchiveCleanup(this.store, taskId, { resource: `cleanup-pass:${taskId}`,
|
|
235
|
+
detail: "Begin one foreground exact-owner cleanup pass." }, "started");
|
|
301
236
|
const attempt = async (diagnostic, action) => {
|
|
302
237
|
recordArchiveCleanup(this.store, taskId, diagnostic, "started");
|
|
303
238
|
let status;
|
|
@@ -306,7 +241,7 @@ export class TaskWorkspaceCoordinator {
|
|
|
306
241
|
const result = await action();
|
|
307
242
|
status = result === "dirty" ? "retained" : result;
|
|
308
243
|
if (result === "dirty")
|
|
309
|
-
detail = "Dirty workspace retained;
|
|
244
|
+
detail = "Dirty workspace retained; archive does not discard local changes.";
|
|
310
245
|
}
|
|
311
246
|
catch (error) {
|
|
312
247
|
if (isArchivePersistenceFailure(error))
|
|
@@ -321,7 +256,7 @@ export class TaskWorkspaceCoordinator {
|
|
|
321
256
|
for (const role of this.store.listRoles(taskId)) {
|
|
322
257
|
const released = await attempt({ resource: `role:${taskId}/${role.name}`,
|
|
323
258
|
detail: "Exact Role runtime stop." }, async () => {
|
|
324
|
-
const checks = archiveExecutionChecks(this.store, taskId).filter(c => c.resource === `role:${taskId}/${role.name}`);
|
|
259
|
+
const checks = archiveExecutionChecks(this.store, taskId, this.preparer.home).filter(c => c.resource === `role:${taskId}/${role.name}`);
|
|
325
260
|
if (checks.length > 0)
|
|
326
261
|
throw new CleanupInspectionError(checks);
|
|
327
262
|
await this.#stopLiveRoles(taskId, [role.name]);
|
|
@@ -331,12 +266,16 @@ export class TaskWorkspaceCoordinator {
|
|
|
331
266
|
}
|
|
332
267
|
const physicalReleased = await attempt({ resource: `runtime:${taskId}`,
|
|
333
268
|
detail: "Verify exact physical resource release before workspace deletion." }, async () => {
|
|
334
|
-
const checks = archiveExecutionChecks(this.store, taskId);
|
|
269
|
+
const checks = archiveExecutionChecks(this.store, taskId, this.preparer.home);
|
|
335
270
|
if (checks.length > 0)
|
|
336
271
|
throw new CleanupInspectionError(checks);
|
|
337
272
|
if (!runtimeReleased)
|
|
338
273
|
throw new Error("One or more Role runtimes could not be safely released.");
|
|
339
274
|
await this.runtime.releaseTaskTerminals(taskId);
|
|
275
|
+
if (this.runtime.assertTaskPhysicalResourcesReleased === undefined) {
|
|
276
|
+
throw new Error("Exact physical resource release inspection is unavailable.");
|
|
277
|
+
}
|
|
278
|
+
await this.runtime.assertTaskPhysicalResourcesReleased(taskId);
|
|
340
279
|
return "released";
|
|
341
280
|
});
|
|
342
281
|
if (physicalReleased) {
|
|
@@ -367,31 +306,10 @@ export class TaskWorkspaceCoordinator {
|
|
|
367
306
|
case "execution-lane":
|
|
368
307
|
return await this.preparer.cleanupExecutionLaneWorkspace(taskId, owner.executionGroupId, owner.executionLaneId);
|
|
369
308
|
case "integration-attempt":
|
|
370
|
-
|
|
371
|
-
return "dirty";
|
|
372
|
-
return await this.preparer.cleanupIntegrationWorkspace(taskId, owner.integrationAttemptId, { preserveChanges: true });
|
|
309
|
+
return await this.preparer.cleanupIntegrationWorkspace(taskId, owner.integrationAttemptId);
|
|
373
310
|
case "task": {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (!delivery.allMerged || !delivery.allVerified) {
|
|
377
|
-
throw new Error("Task main retained: local commits are not covered by verified remote delivery. Force does not discard them.");
|
|
378
|
-
}
|
|
379
|
-
for (const entry of workspace.entries.filter(e => e.access === "write")) {
|
|
380
|
-
const covered = delivery.projects.find(p => p.projectId === entry.projectId);
|
|
381
|
-
const expected = covered?.expectedLocalCommit;
|
|
382
|
-
const path = await lstat(entry.path).catch(error => {
|
|
383
|
-
if (error.code === "ENOENT")
|
|
384
|
-
return null;
|
|
385
|
-
throw error;
|
|
386
|
-
});
|
|
387
|
-
if (expected !== null && expected !== undefined
|
|
388
|
-
&& path !== null) {
|
|
389
|
-
const actual = await this.preparer.git.inspect(entry.path, "HEAD");
|
|
390
|
-
if (actual.baseCommit !== expected && actual.baseCommit !== covered?.deliveryLocalCommit) {
|
|
391
|
-
throw new Error(`Task main HEAD changed: ${entry.path}; retained.`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
311
|
+
// The preparer reloads the shared identity, dirt, baseline and
|
|
312
|
+
// delivery checks; do not maintain a second post-archive gate.
|
|
395
313
|
const result = await this.preparer.cleanupTaskForArchive(taskId, disposition);
|
|
396
314
|
if (result.status !== "removed")
|
|
397
315
|
throw new Error(result.error ?? "Task main or dependent workspace retained.");
|
|
@@ -405,6 +323,17 @@ export class TaskWorkspaceCoordinator {
|
|
|
405
323
|
});
|
|
406
324
|
}
|
|
407
325
|
}
|
|
326
|
+
// The registry remains the authority for scratch/resources not represented
|
|
327
|
+
// by a current workspace. Do not invent ownership or sweep those paths.
|
|
328
|
+
// Expose exact unresolved registrations instead of claiming full release.
|
|
329
|
+
const registered = withResourceRegistry(this.preparer.home, undefined, registry => Object.values(registry.load().records).filter(record => record.owner.taskId === taskId && record.disposition !== "deleted"));
|
|
330
|
+
for (const record of registered) {
|
|
331
|
+
recordArchiveCleanup(this.store, taskId, {
|
|
332
|
+
resource: `resource:${record.id}`, paths: [record.path],
|
|
333
|
+
detail: `Registered ${record.kind} retained (${record.disposition}); owner=${JSON.stringify(record.owner)}. `
|
|
334
|
+
+ "No deletion attempted by archive for this remaining registration. Inspect with yui resources gc --dry-run."
|
|
335
|
+
}, "retained");
|
|
336
|
+
}
|
|
408
337
|
recordArchiveCleanup(this.store, taskId, { resource: `task:${taskId}`,
|
|
409
338
|
detail: "Foreground cleanup attempt finished; inspect retained resource references and warnings." }, "finished");
|
|
410
339
|
}
|
|
@@ -436,24 +365,18 @@ export class TaskWorkspaceCoordinator {
|
|
|
436
365
|
throw new WorkspaceCleanupBlockedError("active-turn", `work-item:${item.taskId}/${item.id}`, true, `Work item still has an active AgentRun: ${item.taskId}/${item.id}.`);
|
|
437
366
|
}
|
|
438
367
|
}
|
|
439
|
-
/**
|
|
440
|
-
*
|
|
441
|
-
* owns could still be using it. Queued, running, and unacknowledged
|
|
442
|
-
* unknown-needs-attention jobs are unsettled — the runner (or its corpse)
|
|
443
|
-
* may still hold the worktree. Acknowledged unknown jobs are settled: a
|
|
444
|
-
* human/Leader has taken responsibility for the outcome.
|
|
368
|
+
/** Check result/runner before stopping Roles, then physical references before
|
|
369
|
+
* cleaning any child lanes. The preparer repeats the full check at removal.
|
|
445
370
|
*/
|
|
446
|
-
#assertNoActiveWorkItemDurableJobs(item) {
|
|
371
|
+
#assertNoActiveWorkItemDurableJobs(item, phase = "before-removal") {
|
|
447
372
|
const jobs = this.store.listDurableJobs(item.taskId);
|
|
448
373
|
const blocking = jobs.filter((job) => (job.owner.kind === "work-item"
|
|
449
374
|
&& job.owner.workItemId === item.id
|
|
450
|
-
&& (job.
|
|
451
|
-
|| job.status === "running"
|
|
452
|
-
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined))));
|
|
375
|
+
&& jobCleanupBlocker(job, this.preparer.home, phase) !== undefined));
|
|
453
376
|
if (blocking.length > 0) {
|
|
454
377
|
throw new WorkspaceCleanupBlockedError("active-durable-job", `work-item:${item.taskId}/${item.id}`, true, `Work item ${item.id} still has ${blocking.length} active DurableJob(s): `
|
|
455
378
|
+ `${blocking.map((job) => `${job.id}/${job.status}`).join(", ")}. `
|
|
456
|
-
+ "
|
|
379
|
+
+ "Settle their outcomes and establish physical exit before cleanup; acknowledgement alone is not stop evidence.");
|
|
457
380
|
}
|
|
458
381
|
}
|
|
459
382
|
#workItemRoleNames(item) {
|
|
@@ -470,6 +393,10 @@ export class TaskWorkspaceCoordinator {
|
|
|
470
393
|
}
|
|
471
394
|
async #stopLiveRoles(taskId, roleNames) {
|
|
472
395
|
const targets = [...new Set(roleNames)];
|
|
396
|
+
const unresolved = archiveExecutionChecks(this.store, taskId, this.preparer.home, "before-role-stop")
|
|
397
|
+
.filter(check => targets.some(roleName => check.resource === `role:${taskId}/${roleName}`));
|
|
398
|
+
if (unresolved.length > 0)
|
|
399
|
+
throw new CleanupInspectionError(unresolved);
|
|
473
400
|
for (const roleName of targets) {
|
|
474
401
|
if (this.store.getActiveRun(taskId, roleName) !== null) {
|
|
475
402
|
throw new Error(`Role has an active AgentRun: ${taskId}/${roleName}.`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lstat, mkdir, readdir, readlink,
|
|
1
|
+
import { lstat, mkdir, readdir, readlink, rmdir, symlink, unlink } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
import { recordArchiveCleanup } from "../task/archiveDiagnostics.js";
|
|
@@ -25,7 +25,8 @@ import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
|
|
|
25
25
|
import { acquireProjectMaintenanceLocks, ProjectMaintenanceLockCancelledError, ProjectMaintenanceLockedError } from "./projectMaintenanceLock.js";
|
|
26
26
|
import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
|
|
27
27
|
import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
|
|
28
|
-
import { inspectWorkspaceCleanup } from "./workspaceCleanupInspection.js";
|
|
28
|
+
import { inspectWorkspaceCleanup, inspectWorkspaceContainer } from "./workspaceCleanupInspection.js";
|
|
29
|
+
import { defaultIntegrationRuntimeIsolation } from "../runtime/integrationRuntimeIsolation.js";
|
|
29
30
|
const MAIN_WORKTREE = "main";
|
|
30
31
|
const LEADER_ROLE = "leader";
|
|
31
32
|
/**
|
|
@@ -73,7 +74,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
73
74
|
this.maintenanceLockOptions = maintenanceLockOptions;
|
|
74
75
|
}
|
|
75
76
|
inspectWorkspaceCleanup(workspace, disposition, forceArchive = false) {
|
|
76
|
-
return inspectWorkspaceCleanup(this.store, this.git, workspace, disposition, forceArchive);
|
|
77
|
+
return inspectWorkspaceCleanup(this.store, this.git, workspace, disposition, forceArchive, this.home);
|
|
77
78
|
}
|
|
78
79
|
async #assertWorkspaceCleanup(workspace, disposition) {
|
|
79
80
|
const checks = await this.inspectWorkspaceCleanup(workspace, disposition);
|
|
@@ -1296,7 +1297,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1296
1297
|
this.#recordArchivePathRemoval(task, entry.path, result);
|
|
1297
1298
|
removed ||= result === "removed";
|
|
1298
1299
|
}
|
|
1299
|
-
await removeWorkspaceView(workspace.root);
|
|
1300
|
+
await removeWorkspaceView(workspace.root, workspace);
|
|
1300
1301
|
this.#resourceRegistrar().markWorkspaceDeleted(workspace);
|
|
1301
1302
|
this.store.removeManagedWorkspace(workspace.owner);
|
|
1302
1303
|
return removed ? "removed" : "missing";
|
|
@@ -1907,7 +1908,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1907
1908
|
this.#recordArchivePathRemoval(task, entry.path, result);
|
|
1908
1909
|
removed ||= result === "removed";
|
|
1909
1910
|
}
|
|
1910
|
-
await removeWorkspaceView(workspace.root);
|
|
1911
|
+
await removeWorkspaceView(workspace.root, workspace);
|
|
1911
1912
|
this.#resourceRegistrar().markWorkspaceDeleted(workspace);
|
|
1912
1913
|
this.store.transaction((tx) => {
|
|
1913
1914
|
const currentRound = tx.getReviewRound(task.id, round.id);
|
|
@@ -1976,7 +1977,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1976
1977
|
this.#recordArchivePathRemoval(task, entry.path, result);
|
|
1977
1978
|
removed ||= result === "removed";
|
|
1978
1979
|
}
|
|
1979
|
-
await removeWorkspaceView(workspace.root);
|
|
1980
|
+
await removeWorkspaceView(workspace.root, workspace);
|
|
1980
1981
|
this.#resourceRegistrar().markWorkspaceDeleted(workspace);
|
|
1981
1982
|
try {
|
|
1982
1983
|
this.#recordWorkspaceRemoval(task, workspace, this.#fallbackWorkspace(), {
|
|
@@ -2027,39 +2028,36 @@ export class FileTaskWorkspacePreparer {
|
|
|
2027
2028
|
throw new Error(`IntegrationAttempt must be terminal before cleanup: ${attempt.id}.`);
|
|
2028
2029
|
}
|
|
2029
2030
|
const workspace = this.store.getIntegrationWorkspace(task.id, attempt.id);
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2031
|
+
if (workspace === null)
|
|
2032
|
+
return "missing"; // no current resource ownership to delete
|
|
2033
|
+
// Terminal history does not authorize discarding conflict resolutions or
|
|
2034
|
+
// diagnostics. All entry points use the same exact-owner inspection.
|
|
2035
|
+
const runtime = options.runtimeIsolation ?? defaultIntegrationRuntimeIsolation(this.home, this.store.getHomeIdentity().homeId);
|
|
2036
|
+
const checks = await inspectWorkspaceCleanup(this.store, this.git, workspace, "abandoned", false, this.home, runtime);
|
|
2037
|
+
if (checks.length > 0)
|
|
2038
|
+
throw new CleanupInspectionError(checks);
|
|
2039
|
+
runtime.cleanup(runtime.preflight({ workspace, allowExactActive: true }), attempt.status === "committed" ? "completion" : "failure");
|
|
2035
2040
|
const project = requireProject(this.store, attempt.projectId);
|
|
2036
|
-
//
|
|
2037
|
-
//
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
const directory = recorded?.directory
|
|
2042
|
-
?? task.projectBindings.find(({ projectId }) => projectId === attempt.projectId)?.directory;
|
|
2043
|
-
if (directory === undefined) {
|
|
2044
|
-
throw new Error(`Integration Project binding is unavailable: ${task.id}/${project.id}.`);
|
|
2041
|
+
// Only the recorded entry grants resource ownership; no history-derived
|
|
2042
|
+
// path reconstruction after the current resource record has been removed.
|
|
2043
|
+
const recorded = workspace.entries[0];
|
|
2044
|
+
if (recorded === undefined || recorded.projectId !== project.id || workspace.entries.length !== 1) {
|
|
2045
|
+
throw new Error(`Integration workspace Project identity is unavailable: ${task.id}/${project.id}.`);
|
|
2045
2046
|
}
|
|
2046
2047
|
const result = await this.git.removeIntegrationWorktree({
|
|
2047
2048
|
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
2048
|
-
container: recorded
|
|
2049
|
-
|
|
2050
|
-
: this.#integrationWorkspaceRoot(task.id, attempt.id),
|
|
2051
|
-
directory,
|
|
2049
|
+
container: dirname(recorded.path),
|
|
2050
|
+
directory: recorded.directory,
|
|
2052
2051
|
taskSegment: this.#taskSegment(task),
|
|
2053
2052
|
integrationId: attempt.id,
|
|
2054
|
-
discardChanges:
|
|
2053
|
+
discardChanges: false
|
|
2055
2054
|
});
|
|
2056
2055
|
if (result !== "dirty") {
|
|
2057
|
-
for (const entry of workspace
|
|
2056
|
+
for (const entry of workspace.entries) {
|
|
2058
2057
|
this.#recordArchivePathRemoval(task, entry.path, result);
|
|
2059
2058
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
await rm(join(this.home, "artifacts", "integration-checks", task.id, attempt.id), { recursive: true, force: true });
|
|
2059
|
+
this.#resourceRegistrar().markWorkspaceDeleted(workspace);
|
|
2060
|
+
// Check logs are stable historical evidence, not worktree scratch.
|
|
2063
2061
|
this.store.removeManagedWorkspace({
|
|
2064
2062
|
type: "integration-attempt",
|
|
2065
2063
|
taskId: task.id,
|
|
@@ -2097,15 +2095,9 @@ export class FileTaskWorkspacePreparer {
|
|
|
2097
2095
|
if (main !== null) {
|
|
2098
2096
|
await this.#assertWorkspaceCleanup(main, disposition);
|
|
2099
2097
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
2100
|
-
//
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
taskId,
|
|
2104
|
-
status: "retained-dirty",
|
|
2105
|
-
...(task.cwd === undefined ? {} : { path: task.cwd })
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
|
-
for (const entry of main.entries) {
|
|
2098
|
+
// The shared preflight and removeTaskClone both inspect clone identity.
|
|
2099
|
+
// Do not re-enter linked-worktree inspection against a missing clone.
|
|
2100
|
+
for (const entry of main.entries.filter(e => e.access === "write")) {
|
|
2109
2101
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
2110
2102
|
const result = await this.git.removeTaskClone({
|
|
2111
2103
|
path: entry.path, container: main.root, directory: entry.directory,
|
|
@@ -2117,7 +2109,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
2117
2109
|
this.#recordArchivePathRemoval(task, entry.path, result);
|
|
2118
2110
|
}
|
|
2119
2111
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
2120
|
-
await removeWorkspaceView(main.root);
|
|
2112
|
+
await removeWorkspaceView(main.root, main);
|
|
2121
2113
|
this.#resourceRegistrar().markWorkspaceDeleted(main);
|
|
2122
2114
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
2123
2115
|
this.#recordWorkspaceRemoval(task, main, this.#fallbackWorkspace());
|
|
@@ -2175,9 +2167,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
2175
2167
|
#reviewRoundWorkspaceRoot(taskId, round) {
|
|
2176
2168
|
return join(resolveTaskRoot(this.home), safePathSegment(taskId), "reviews", safePathSegment(this.#reviewWorktreeName(round)));
|
|
2177
2169
|
}
|
|
2178
|
-
#integrationWorkspaceRoot(taskId, integrationId) {
|
|
2179
|
-
return integrationWorkspaceRoot(this.home, taskId, integrationId);
|
|
2180
|
-
}
|
|
2181
2170
|
#reviewWorktreeName(round) {
|
|
2182
2171
|
return round.scope === "task"
|
|
2183
2172
|
? `reviewer-${round.reviewerRoleName}`
|
|
@@ -2570,7 +2559,12 @@ async function ensureWorkspaceView(root, entries) {
|
|
|
2570
2559
|
await symlink(target, join(root, directory), "dir");
|
|
2571
2560
|
}
|
|
2572
2561
|
}
|
|
2573
|
-
async function removeWorkspaceView(root) {
|
|
2562
|
+
async function removeWorkspaceView(root, workspace) {
|
|
2563
|
+
if (workspace !== undefined) {
|
|
2564
|
+
const checks = await inspectWorkspaceContainer(workspace);
|
|
2565
|
+
if (checks.length > 0)
|
|
2566
|
+
throw new CleanupInspectionError(checks);
|
|
2567
|
+
}
|
|
2574
2568
|
let entries;
|
|
2575
2569
|
try {
|
|
2576
2570
|
entries = await readdir(root, { withFileTypes: true });
|
|
@@ -2584,6 +2578,8 @@ async function removeWorkspaceView(root) {
|
|
|
2584
2578
|
if (!entry.isSymbolicLink()) {
|
|
2585
2579
|
throw new Error(`Managed workspace contains an unexpected entry: ${join(root, entry.name)}.`);
|
|
2586
2580
|
}
|
|
2581
|
+
}
|
|
2582
|
+
for (const entry of entries) {
|
|
2587
2583
|
await unlink(join(root, entry.name));
|
|
2588
2584
|
}
|
|
2589
2585
|
await rmdir(root);
|