@zq-silk/yui 2.2.0 → 2.4.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 +19 -3
- package/dist/artifacts/artifactCapability.js +39 -10
- package/dist/artifacts/imageMetadata.js +120 -0
- package/dist/artifacts/managedGit.js +7 -4
- package/dist/artifacts/taskArtifactRepository.js +6 -0
- package/dist/cli/commandCatalog.js +25 -11
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli/invocationAuthority.js +1 -1
- package/dist/commands/executionAuditCommands.js +5 -0
- package/dist/commands/globalRoleCommands.js +41 -3
- package/dist/commands/projectCommands.js +89 -94
- package/dist/commands/roleSkillValidation.js +4 -8
- package/dist/commands/taskCommands.js +30 -9
- package/dist/commands/taskIntegrationCommands.js +5 -16
- package/dist/commands/taskUsageCommand.js +38 -0
- package/dist/context/roleSessionContext.js +18 -2
- package/dist/context/runContextPack.js +5 -1
- package/dist/context/sessionBootstrapManifest.js +10 -1
- package/dist/context/skillCatalog.js +108 -0
- package/dist/context/skillPackage.js +161 -0
- package/dist/context/taskContext.js +3 -2
- package/dist/context/taskSearch.js +127 -0
- package/dist/controlPlaneCli.js +51 -39
- package/dist/controller/clientRuntime.js +3 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
- package/dist/controller/jobControl.js +2 -0
- package/dist/controller/nativeHostInput.js +37 -0
- package/dist/controller/runtime.js +93 -2
- package/dist/controller/runtimeEventInbox.js +2 -2
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/doctor/doctor.js +10 -0
- package/dist/execution/reviewMainRun.js +1 -0
- package/dist/execution/workItemMainRun.js +1 -0
- package/dist/executor/codexConfigConflict.js +6 -4
- package/dist/executor/effectiveLaunch.js +24 -1
- package/dist/executor/fileRoleLaunchPlanner.js +13 -5
- package/dist/integration/gitIntegrationService.js +15 -79
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/job/httpPreview.js +61 -0
- package/dist/job/jobCleanupInspection.js +67 -0
- package/dist/job/jobRunner.js +12 -2
- package/dist/kernel/builtinCapabilities.js +3 -1
- package/dist/message/message.js +15 -1
- package/dist/output/rolePresentation.js +4 -1
- package/dist/repository/executionLaneGitSnapshot.js +2 -1
- package/dist/repository/gitCompatibility.js +62 -0
- package/dist/repository/gitWorkspace.js +86 -80
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/repository/taskWorkspaceCoordinator.js +72 -145
- package/dist/repository/taskWorkspacePreparer.js +39 -43
- package/dist/repository/workspaceCleanupInspection.js +88 -7
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +18 -11
- package/dist/runtime/agentEndpoint.js +8 -0
- package/dist/runtime/agentHost.js +170 -3
- package/dist/runtime/codexAppServerRuntime.js +1 -1
- package/dist/runtime/codexNativeAccess.js +175 -0
- package/dist/runtime/controlledCodexTui.js +116 -0
- package/dist/runtime/integrationRuntimeIsolation.js +21 -0
- package/dist/runtime/publicReply.js +23 -0
- package/dist/runtime/runtimeObservation.js +48 -0
- package/dist/runtime/structuredProviderHost.js +16 -7
- package/dist/runtime/taskCostMetrics.js +133 -0
- package/dist/runtime/taskRuntimeIsolation.js +91 -21
- package/dist/runtime/taskUsageMetrics.js +11 -2
- package/dist/runtime/taskUsageQuery.js +38 -0
- package/dist/scheduler/taskExecutionProjection.js +2 -0
- package/dist/scheduler/taskObservabilityProjection.js +9 -1
- package/dist/storage/homeLayout.js +13 -0
- package/dist/storage/sqliteSchema.js +44 -0
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskSearch.js +35 -0
- package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
- package/dist/task/archiveDiagnostics.js +26 -8
- package/dist/task/archivePreflight.js +26 -9
- package/dist/task/leaderArchive.js +9 -15
- package/dist/task/ordinaryArchive.js +42 -0
- package/dist/tmux/tmuxManager.js +8 -8
- package/dist/web/assets/assetManifest.js +11 -51
- package/dist/web/assets/client/app/catalog.js +215 -0
- package/dist/web/assets/client/app/main.js +148 -0
- package/dist/web/assets/client/app/selection.js +139 -0
- package/dist/web/assets/client/app/settings.js +326 -0
- package/dist/web/assets/client/app/taskView.js +280 -0
- package/dist/web/assets/client/domain/context.js +96 -0
- package/dist/web/assets/client/domain/records.js +126 -0
- package/dist/web/assets/client/domain/runs.js +119 -0
- package/dist/web/assets/client/domain/taskForms.js +197 -0
- package/dist/web/assets/client/domain/vocab.js +69 -0
- package/dist/web/assets/client/domain/work.js +116 -0
- package/dist/web/assets/client/i18n/en.js +900 -0
- package/dist/web/assets/client/i18n/zh-CN.js +898 -0
- package/dist/web/assets/client/index.js +114 -0
- package/dist/web/assets/client/layout/resizer.js +37 -0
- package/dist/web/assets/client/layout/router.js +35 -0
- package/dist/web/assets/client/layout/shortcuts.js +45 -0
- package/dist/web/assets/client/layout/sizing.js +95 -0
- package/dist/web/assets/client/layout/workspace.js +247 -0
- package/dist/web/assets/client/{api.js → lib/api.js} +28 -4
- package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
- package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
- package/dist/web/assets/client/lib/i18n.js +54 -0
- package/dist/web/assets/client/lib/markdown.js +167 -0
- package/dist/web/assets/client/lib/prefs.js +28 -0
- package/dist/web/assets/client/lib/theme.js +68 -0
- package/dist/web/assets/client/ui/containers.js +101 -0
- package/dist/web/assets/client/ui/controls.js +51 -0
- package/dist/web/assets/client/ui/forms.js +48 -0
- package/dist/web/assets/client/ui/metrics.js +137 -0
- package/dist/web/assets/client/ui/primitives.js +92 -0
- package/dist/web/assets/client/ui/text.js +33 -0
- package/dist/web/assets/client/ui/toast.js +12 -0
- package/dist/web/assets/client/views/bodySearch.js +172 -0
- package/dist/web/assets/client/views/createTask.js +85 -0
- package/dist/web/assets/client/views/dock/conversation.js +612 -0
- package/dist/web/assets/client/views/dock/discussion.js +173 -0
- package/dist/web/assets/client/views/dock/preview.js +129 -0
- package/dist/web/assets/client/views/dock/terminal.js +199 -0
- package/dist/web/assets/client/views/globalInput.js +67 -0
- package/dist/web/assets/client/{overview.js → views/overview.js} +62 -56
- package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
- package/dist/web/assets/client/views/skills.js +152 -0
- package/dist/web/assets/client/views/task/actions.js +36 -0
- package/dist/web/assets/client/views/task/delivery.js +185 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
- package/dist/web/assets/client/views/task/files.js +272 -0
- package/dist/web/assets/client/views/task/observation.js +153 -0
- package/dist/web/assets/client/views/task/overview.js +161 -0
- package/dist/web/assets/client/views/task/page.js +131 -0
- package/dist/web/assets/client/views/task/records.js +163 -0
- package/dist/web/assets/client/views/task/runtime.js +130 -0
- package/dist/web/assets/client/views/task/timeline.js +190 -0
- package/dist/web/assets/shared/geometry.js +19 -0
- package/dist/web/assets/{icons.js → shared/icons.js} +4 -0
- package/dist/web/assets/shared/taskTabs.js +8 -0
- package/dist/web/assets/shared/themes.js +93 -0
- package/dist/web/assets/shell/dialogs.js +63 -0
- package/dist/web/assets/shell/dock.js +40 -0
- package/dist/web/assets/shell/index.js +40 -0
- package/dist/web/assets/shell/settings.js +34 -0
- package/dist/web/assets/shell/sidebar.js +45 -0
- package/dist/web/assets/styles/index.js +41 -0
- package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +15 -13
- package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
- package/dist/web/assets/styles/markdown.js +22 -3
- package/dist/web/assets/styles/tokens.js +10 -63
- package/dist/web/assets/styles/ui/controls.js +76 -0
- package/dist/web/assets/styles/ui/forms.js +46 -0
- package/dist/web/assets/styles/ui/surfaces.js +122 -0
- package/dist/web/assets/styles/views/delivery.js +29 -0
- package/dist/web/assets/styles/views/dock.js +71 -0
- package/dist/web/assets/styles/views/page.js +32 -0
- package/dist/web/assets/styles/views/records.js +70 -0
- package/dist/web/assets/styles/views/settings.js +26 -0
- package/dist/web/assets/styles/views/sidebar.js +74 -0
- package/dist/web/assets/styles/views/task.js +80 -0
- package/dist/web/assets/styles/views/terminal.js +38 -0
- package/dist/web/taskPreviews.js +205 -0
- package/dist/web/tmuxWebTerminal.js +16 -9
- package/dist/web/webConversation.js +373 -0
- package/dist/web/webMaterials.js +155 -0
- package/dist/web/webServer.js +306 -18
- package/dist/web/webSettings.js +331 -0
- package/dist/web/webSkills.js +86 -0
- package/dist/web/webSnapshot.js +40 -8
- package/dist/web/webTaskSurface.js +114 -9
- package/dist/workspace/gitChangeSetCapture.js +3 -1
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
- package/docs/observability/README.md +13 -3
- package/docs/observability/README.zh-CN.md +10 -3
- package/docs/release-workflow.md +53 -0
- package/docs/release-workflow.zh-CN.md +40 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-delivery.md +36 -8
- package/docs/task-delivery.zh-CN.md +24 -7
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +4 -3
- package/package.json +1 -1
- 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 +3 -0
- package/skills/yui-runtime/SKILL.md +4 -0
- package/skills/yui-runtime/references/publication.md +20 -4
- package/skills/yui-worker/SKILL.md +3 -0
- package/dist/web/assets/client/app.js +0 -804
- package/dist/web/assets/client/components.js +0 -418
- package/dist/web/assets/client/detail.js +0 -333
- package/dist/web/assets/client/dock.js +0 -225
- package/dist/web/assets/client/evidence.js +0 -172
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1161
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -63
- package/dist/web/assets/client/sections.js +0 -313
- package/dist/web/assets/client/theme.js +0 -71
- package/dist/web/assets/shell.js +0 -161
- package/dist/web/assets/styles/components.js +0 -169
- package/dist/web/assets/styles/views.js +0 -292
|
@@ -63,13 +63,17 @@ export function assertProjectActive(project, action) {
|
|
|
63
63
|
throw new Error(`Project is retired and cannot ${action}: ${project.id}.`);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
export function addProjectKnowledge(project, id, title, body, now, provenance) {
|
|
66
|
+
export function addProjectKnowledge(project, id, title, body, now, provenance, applicability = {}) {
|
|
67
67
|
const timestamp = now.toISOString();
|
|
68
68
|
const knowledge = {
|
|
69
|
-
schemaVersion:
|
|
69
|
+
schemaVersion: 2,
|
|
70
|
+
version: 1,
|
|
71
|
+
history: [],
|
|
70
72
|
id: requireIdentity(id, "Project knowledge id"),
|
|
71
73
|
title: requireText(title, "Project knowledge title"),
|
|
72
74
|
body: requireText(body, "Project knowledge body"),
|
|
75
|
+
...(applicability.scope === undefined ? {} : { scope: requireText(applicability.scope, "Knowledge scope") }),
|
|
76
|
+
...(applicability.expiresWhen === undefined ? {} : { expiresWhen: requireText(applicability.expiresWhen, "Knowledge expiry condition") }),
|
|
73
77
|
status: "active",
|
|
74
78
|
...(provenance === undefined ? {} : { provenance: validateKnowledgeProvenance(provenance) }),
|
|
75
79
|
createdAt: timestamp,
|
|
@@ -114,19 +118,34 @@ export function updateProjectKnowledge(project, id, patch, now) {
|
|
|
114
118
|
if (entry.status === "retired") {
|
|
115
119
|
throw new Error(`Project knowledge is retired: ${knowledgeId}.`);
|
|
116
120
|
}
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
const { scope: _scope, expiresWhen: _expiresWhen, ...base } = entry;
|
|
122
|
+
const scope = patch.scope === undefined ? entry.scope
|
|
123
|
+
: patch.scope === null ? undefined : requireText(patch.scope, "Knowledge scope");
|
|
124
|
+
const expiresWhen = patch.expiresWhen === undefined ? entry.expiresWhen
|
|
125
|
+
: patch.expiresWhen === null ? undefined : requireText(patch.expiresWhen, "Knowledge expiry condition");
|
|
126
|
+
const next = {
|
|
127
|
+
...base,
|
|
128
|
+
...(scope === undefined ? {} : { scope }),
|
|
129
|
+
...(expiresWhen === undefined ? {} : { expiresWhen }),
|
|
119
130
|
title: patch.title === undefined
|
|
120
131
|
? entry.title
|
|
121
132
|
: requireText(patch.title, "Project knowledge title"),
|
|
122
133
|
body: patch.body === undefined
|
|
123
134
|
? entry.body
|
|
124
135
|
: requireText(patch.body, "Project knowledge body"),
|
|
136
|
+
...(patch.provenance === undefined ? {} : { provenance: validateKnowledgeProvenance(patch.provenance) }),
|
|
125
137
|
updatedAt: now.toISOString()
|
|
126
138
|
};
|
|
139
|
+
if (next.title === entry.title && next.body === entry.body
|
|
140
|
+
&& next.scope === entry.scope && next.expiresWhen === entry.expiresWhen
|
|
141
|
+
&& JSON.stringify(next.provenance) === JSON.stringify(entry.provenance))
|
|
142
|
+
return entry;
|
|
143
|
+
return { ...next, version: entry.version + 1, history: [...entry.history, knowledgeRevision(entry)] };
|
|
127
144
|
});
|
|
128
145
|
if (!found)
|
|
129
146
|
throw new Error(`Project knowledge not found: ${knowledgeId}.`);
|
|
147
|
+
if (knowledge.every((entry, index) => entry === project.knowledge[index]))
|
|
148
|
+
return project;
|
|
130
149
|
return validateProject({ ...project, knowledge, updatedAt: now.toISOString() });
|
|
131
150
|
}
|
|
132
151
|
export function retireProjectKnowledge(project, id, now) {
|
|
@@ -136,31 +155,42 @@ export function retireProjectKnowledge(project, id, now) {
|
|
|
136
155
|
if (entry.id !== knowledgeId)
|
|
137
156
|
return entry;
|
|
138
157
|
found = true;
|
|
158
|
+
if (entry.status === "retired")
|
|
159
|
+
return entry;
|
|
139
160
|
return {
|
|
140
161
|
...entry,
|
|
162
|
+
version: entry.version + 1,
|
|
163
|
+
history: [...entry.history, knowledgeRevision(entry)],
|
|
141
164
|
status: "retired",
|
|
142
165
|
updatedAt: now.toISOString()
|
|
143
166
|
};
|
|
144
167
|
});
|
|
145
168
|
if (!found)
|
|
146
169
|
throw new Error(`Project knowledge not found: ${knowledgeId}.`);
|
|
170
|
+
if (knowledge.every((entry, index) => entry === project.knowledge[index]))
|
|
171
|
+
return project;
|
|
147
172
|
return validateProject({ ...project, knowledge, updatedAt: now.toISOString() });
|
|
148
173
|
}
|
|
174
|
+
function knowledgeRevision(entry) {
|
|
175
|
+
const { schemaVersion: _schema, id: _id, history: _history, createdAt: _created, ...revision } = entry;
|
|
176
|
+
return revision;
|
|
177
|
+
}
|
|
149
178
|
/**
|
|
150
179
|
* Stable dedup fingerprint for a promotion candidate. Two proposals with the
|
|
151
|
-
* same source
|
|
152
|
-
*
|
|
180
|
+
* same source, content, applicability and replacement intent are identical.
|
|
181
|
+
* Trim outer whitespace only; preserve case, internal whitespace and Unicode.
|
|
182
|
+
* A JSON tuple avoids collisions between user-supplied identity delimiters.
|
|
153
183
|
*/
|
|
154
184
|
export function knowledgeProposalFingerprint(input) {
|
|
155
|
-
const
|
|
156
|
-
input.projectId,
|
|
157
|
-
input.source.taskId,
|
|
158
|
-
input.source.decisionId ?? "",
|
|
159
|
-
input.source.milestoneId ?? "",
|
|
160
|
-
input.source.commitSha ?? ""
|
|
161
|
-
].join("|");
|
|
185
|
+
const optional = (value) => value === undefined ? null : requireText(value, "Knowledge fingerprint field");
|
|
162
186
|
return createHash("sha256")
|
|
163
|
-
.update(
|
|
187
|
+
.update(JSON.stringify([
|
|
188
|
+
"knowledge-proposal-v2", requireText(input.projectId, "Project id"),
|
|
189
|
+
requireText(input.source.taskId, "Source Task"), optional(input.source.decisionId),
|
|
190
|
+
optional(input.source.milestoneId), optional(input.source.commitSha),
|
|
191
|
+
requireText(input.title, "Knowledge title"), requireText(input.body, "Knowledge body"),
|
|
192
|
+
optional(input.scope), optional(input.expiresWhen), optional(input.supersedesKnowledgeId)
|
|
193
|
+
]))
|
|
164
194
|
.digest("hex");
|
|
165
195
|
}
|
|
166
196
|
/** Digest of the cited source evidence, so a later Agent can verify it. */
|
|
@@ -334,17 +364,18 @@ export function validateProject(project) {
|
|
|
334
364
|
requireGitRef(project.developmentBranch, "Project development branch");
|
|
335
365
|
const knowledgeIds = new Set();
|
|
336
366
|
for (const entry of project.knowledge) {
|
|
337
|
-
if (entry.schemaVersion !==
|
|
338
|
-
throw new Error("Project knowledge must use schemaVersion
|
|
367
|
+
if (entry.schemaVersion !== 2)
|
|
368
|
+
throw new Error("Project knowledge must use schemaVersion 2.");
|
|
339
369
|
requireIdentity(entry.id, "Project knowledge id");
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
throw new Error("Project knowledge status is invalid.");
|
|
344
|
-
}
|
|
345
|
-
if (entry.provenance !== undefined) {
|
|
346
|
-
validateKnowledgeProvenance(entry.provenance);
|
|
370
|
+
validateKnowledgeRevision(entry);
|
|
371
|
+
if (!Array.isArray(entry.history) || entry.history.length !== entry.version - 1) {
|
|
372
|
+
throw new Error("Project knowledge history must contain every prior version.");
|
|
347
373
|
}
|
|
374
|
+
entry.history.forEach((revision, index) => {
|
|
375
|
+
validateKnowledgeRevision(revision);
|
|
376
|
+
if (revision.version !== index + 1)
|
|
377
|
+
throw new Error("Project knowledge history is out of order.");
|
|
378
|
+
});
|
|
348
379
|
requireTimestamp(entry.createdAt, "Project knowledge createdAt");
|
|
349
380
|
requireTimestamp(entry.updatedAt, "Project knowledge updatedAt");
|
|
350
381
|
if (knowledgeIds.has(entry.id))
|
|
@@ -363,6 +394,21 @@ export function validateProject(project) {
|
|
|
363
394
|
requireTimestamp(project.updatedAt, "Project updatedAt");
|
|
364
395
|
return project;
|
|
365
396
|
}
|
|
397
|
+
function validateKnowledgeRevision(entry) {
|
|
398
|
+
if (!Number.isSafeInteger(entry.version) || entry.version < 1)
|
|
399
|
+
throw new Error("Invalid knowledge version.");
|
|
400
|
+
requireText(entry.title, "Project knowledge title");
|
|
401
|
+
requireText(entry.body, "Project knowledge body");
|
|
402
|
+
if (entry.scope !== undefined)
|
|
403
|
+
requireText(entry.scope, "Knowledge scope");
|
|
404
|
+
if (entry.expiresWhen !== undefined)
|
|
405
|
+
requireText(entry.expiresWhen, "Knowledge expiry condition");
|
|
406
|
+
if (!["active", "retired"].includes(entry.status))
|
|
407
|
+
throw new Error("Project knowledge status is invalid.");
|
|
408
|
+
if (entry.provenance !== undefined)
|
|
409
|
+
validateKnowledgeProvenance(entry.provenance);
|
|
410
|
+
requireTimestamp(entry.updatedAt, "Project knowledge updatedAt");
|
|
411
|
+
}
|
|
366
412
|
function validateKnowledgeProvenance(provenance) {
|
|
367
413
|
requireIdentity(provenance.taskId, "Knowledge provenance task id");
|
|
368
414
|
if (provenance.decisionId !== undefined) {
|
|
@@ -394,8 +440,8 @@ function validateKnowledgeProvenance(provenance) {
|
|
|
394
440
|
return provenance;
|
|
395
441
|
}
|
|
396
442
|
function validateKnowledgeProposal(proposal, projectId) {
|
|
397
|
-
if (proposal.schemaVersion !==
|
|
398
|
-
throw new Error("Knowledge proposal must use schemaVersion
|
|
443
|
+
if (proposal.schemaVersion !== 2) {
|
|
444
|
+
throw new Error("Knowledge proposal must use schemaVersion 2.");
|
|
399
445
|
}
|
|
400
446
|
requireIdentity(proposal.id, "Knowledge proposal id");
|
|
401
447
|
if (proposal.projectId !== projectId) {
|
|
@@ -403,6 +449,9 @@ function validateKnowledgeProposal(proposal, projectId) {
|
|
|
403
449
|
}
|
|
404
450
|
requireText(proposal.title, "Knowledge proposal title");
|
|
405
451
|
requireText(proposal.body, "Knowledge proposal body");
|
|
452
|
+
if (proposal.fingerprint !== knowledgeProposalFingerprint(proposal)) {
|
|
453
|
+
throw new Error("Knowledge proposal fingerprint does not match its content.");
|
|
454
|
+
}
|
|
406
455
|
if (!["pending", "accepted", "rejected"].includes(proposal.status)) {
|
|
407
456
|
throw new Error("Knowledge proposal status is invalid.");
|
|
408
457
|
}
|
|
@@ -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}.`);
|