@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.
Files changed (206) hide show
  1. package/README.md +19 -3
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +25 -11
  7. package/dist/cli/interactionPolicy.js +4 -0
  8. package/dist/cli/invocationAuthority.js +1 -1
  9. package/dist/commands/executionAuditCommands.js +5 -0
  10. package/dist/commands/globalRoleCommands.js +41 -3
  11. package/dist/commands/projectCommands.js +89 -94
  12. package/dist/commands/roleSkillValidation.js +4 -8
  13. package/dist/commands/taskCommands.js +30 -9
  14. package/dist/commands/taskIntegrationCommands.js +5 -16
  15. package/dist/commands/taskUsageCommand.js +38 -0
  16. package/dist/context/roleSessionContext.js +18 -2
  17. package/dist/context/runContextPack.js +5 -1
  18. package/dist/context/sessionBootstrapManifest.js +10 -1
  19. package/dist/context/skillCatalog.js +108 -0
  20. package/dist/context/skillPackage.js +161 -0
  21. package/dist/context/taskContext.js +3 -2
  22. package/dist/context/taskSearch.js +127 -0
  23. package/dist/controlPlaneCli.js +51 -39
  24. package/dist/controller/clientRuntime.js +3 -2
  25. package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
  26. package/dist/controller/jobControl.js +2 -0
  27. package/dist/controller/nativeHostInput.js +37 -0
  28. package/dist/controller/runtime.js +93 -2
  29. package/dist/controller/runtimeEventInbox.js +2 -2
  30. package/dist/controller/runtimeEventProcessor.js +3 -3
  31. package/dist/doctor/doctor.js +10 -0
  32. package/dist/execution/reviewMainRun.js +1 -0
  33. package/dist/execution/workItemMainRun.js +1 -0
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +13 -5
  37. package/dist/integration/gitIntegrationService.js +15 -79
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/job/httpPreview.js +61 -0
  40. package/dist/job/jobCleanupInspection.js +67 -0
  41. package/dist/job/jobRunner.js +12 -2
  42. package/dist/kernel/builtinCapabilities.js +3 -1
  43. package/dist/message/message.js +15 -1
  44. package/dist/output/rolePresentation.js +4 -1
  45. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  46. package/dist/repository/gitCompatibility.js +62 -0
  47. package/dist/repository/gitWorkspace.js +86 -80
  48. package/dist/repository/gitWorktreeInventory.js +121 -0
  49. package/dist/repository/project.js +74 -25
  50. package/dist/repository/taskWorkspaceCoordinator.js +72 -145
  51. package/dist/repository/taskWorkspacePreparer.js +39 -43
  52. package/dist/repository/workspaceCleanupInspection.js +88 -7
  53. package/dist/resources/resourceDiscovery.js +2 -2
  54. package/dist/resources/resourceGc.js +7 -4
  55. package/dist/resources/resourceRegistrar.js +18 -11
  56. package/dist/runtime/agentEndpoint.js +8 -0
  57. package/dist/runtime/agentHost.js +170 -3
  58. package/dist/runtime/codexAppServerRuntime.js +1 -1
  59. package/dist/runtime/codexNativeAccess.js +175 -0
  60. package/dist/runtime/controlledCodexTui.js +116 -0
  61. package/dist/runtime/integrationRuntimeIsolation.js +21 -0
  62. package/dist/runtime/publicReply.js +23 -0
  63. package/dist/runtime/runtimeObservation.js +48 -0
  64. package/dist/runtime/structuredProviderHost.js +16 -7
  65. package/dist/runtime/taskCostMetrics.js +133 -0
  66. package/dist/runtime/taskRuntimeIsolation.js +91 -21
  67. package/dist/runtime/taskUsageMetrics.js +11 -2
  68. package/dist/runtime/taskUsageQuery.js +38 -0
  69. package/dist/scheduler/taskExecutionProjection.js +2 -0
  70. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  71. package/dist/storage/homeLayout.js +13 -0
  72. package/dist/storage/sqliteSchema.js +44 -0
  73. package/dist/storage/sqliteStore.js +53 -0
  74. package/dist/storage/storageVersions.js +1 -1
  75. package/dist/storage/storeRpc.js +2 -0
  76. package/dist/storage/taskSearch.js +35 -0
  77. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  78. package/dist/task/archiveDiagnostics.js +26 -8
  79. package/dist/task/archivePreflight.js +26 -9
  80. package/dist/task/leaderArchive.js +9 -15
  81. package/dist/task/ordinaryArchive.js +42 -0
  82. package/dist/tmux/tmuxManager.js +8 -8
  83. package/dist/web/assets/assetManifest.js +11 -51
  84. package/dist/web/assets/client/app/catalog.js +215 -0
  85. package/dist/web/assets/client/app/main.js +148 -0
  86. package/dist/web/assets/client/app/selection.js +139 -0
  87. package/dist/web/assets/client/app/settings.js +326 -0
  88. package/dist/web/assets/client/app/taskView.js +280 -0
  89. package/dist/web/assets/client/domain/context.js +96 -0
  90. package/dist/web/assets/client/domain/records.js +126 -0
  91. package/dist/web/assets/client/domain/runs.js +119 -0
  92. package/dist/web/assets/client/domain/taskForms.js +197 -0
  93. package/dist/web/assets/client/domain/vocab.js +69 -0
  94. package/dist/web/assets/client/domain/work.js +116 -0
  95. package/dist/web/assets/client/i18n/en.js +900 -0
  96. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  97. package/dist/web/assets/client/index.js +114 -0
  98. package/dist/web/assets/client/layout/resizer.js +37 -0
  99. package/dist/web/assets/client/layout/router.js +35 -0
  100. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  101. package/dist/web/assets/client/layout/sizing.js +95 -0
  102. package/dist/web/assets/client/layout/workspace.js +247 -0
  103. package/dist/web/assets/client/{api.js → lib/api.js} +28 -4
  104. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  105. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  106. package/dist/web/assets/client/lib/i18n.js +54 -0
  107. package/dist/web/assets/client/lib/markdown.js +167 -0
  108. package/dist/web/assets/client/lib/prefs.js +28 -0
  109. package/dist/web/assets/client/lib/theme.js +68 -0
  110. package/dist/web/assets/client/ui/containers.js +101 -0
  111. package/dist/web/assets/client/ui/controls.js +51 -0
  112. package/dist/web/assets/client/ui/forms.js +48 -0
  113. package/dist/web/assets/client/ui/metrics.js +137 -0
  114. package/dist/web/assets/client/ui/primitives.js +92 -0
  115. package/dist/web/assets/client/ui/text.js +33 -0
  116. package/dist/web/assets/client/ui/toast.js +12 -0
  117. package/dist/web/assets/client/views/bodySearch.js +172 -0
  118. package/dist/web/assets/client/views/createTask.js +85 -0
  119. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  120. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  121. package/dist/web/assets/client/views/dock/preview.js +129 -0
  122. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  123. package/dist/web/assets/client/views/globalInput.js +67 -0
  124. package/dist/web/assets/client/{overview.js → views/overview.js} +62 -56
  125. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  126. package/dist/web/assets/client/views/skills.js +152 -0
  127. package/dist/web/assets/client/views/task/actions.js +36 -0
  128. package/dist/web/assets/client/views/task/delivery.js +185 -0
  129. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  130. package/dist/web/assets/client/views/task/files.js +272 -0
  131. package/dist/web/assets/client/views/task/observation.js +153 -0
  132. package/dist/web/assets/client/views/task/overview.js +161 -0
  133. package/dist/web/assets/client/views/task/page.js +131 -0
  134. package/dist/web/assets/client/views/task/records.js +163 -0
  135. package/dist/web/assets/client/views/task/runtime.js +130 -0
  136. package/dist/web/assets/client/views/task/timeline.js +190 -0
  137. package/dist/web/assets/shared/geometry.js +19 -0
  138. package/dist/web/assets/{icons.js → shared/icons.js} +4 -0
  139. package/dist/web/assets/shared/taskTabs.js +8 -0
  140. package/dist/web/assets/shared/themes.js +93 -0
  141. package/dist/web/assets/shell/dialogs.js +63 -0
  142. package/dist/web/assets/shell/dock.js +40 -0
  143. package/dist/web/assets/shell/index.js +40 -0
  144. package/dist/web/assets/shell/settings.js +34 -0
  145. package/dist/web/assets/shell/sidebar.js +45 -0
  146. package/dist/web/assets/styles/index.js +41 -0
  147. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +15 -13
  148. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  149. package/dist/web/assets/styles/markdown.js +22 -3
  150. package/dist/web/assets/styles/tokens.js +10 -63
  151. package/dist/web/assets/styles/ui/controls.js +76 -0
  152. package/dist/web/assets/styles/ui/forms.js +46 -0
  153. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  154. package/dist/web/assets/styles/views/delivery.js +29 -0
  155. package/dist/web/assets/styles/views/dock.js +71 -0
  156. package/dist/web/assets/styles/views/page.js +32 -0
  157. package/dist/web/assets/styles/views/records.js +70 -0
  158. package/dist/web/assets/styles/views/settings.js +26 -0
  159. package/dist/web/assets/styles/views/sidebar.js +74 -0
  160. package/dist/web/assets/styles/views/task.js +80 -0
  161. package/dist/web/assets/styles/views/terminal.js +38 -0
  162. package/dist/web/taskPreviews.js +205 -0
  163. package/dist/web/tmuxWebTerminal.js +16 -9
  164. package/dist/web/webConversation.js +373 -0
  165. package/dist/web/webMaterials.js +155 -0
  166. package/dist/web/webServer.js +306 -18
  167. package/dist/web/webSettings.js +331 -0
  168. package/dist/web/webSkills.js +86 -0
  169. package/dist/web/webSnapshot.js +40 -8
  170. package/dist/web/webTaskSurface.js +114 -9
  171. package/dist/workspace/gitChangeSetCapture.js +3 -1
  172. package/docs/managed-turn-and-session-runtime.md +9 -0
  173. package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
  174. package/docs/observability/README.md +13 -3
  175. package/docs/observability/README.zh-CN.md +10 -3
  176. package/docs/release-workflow.md +53 -0
  177. package/docs/release-workflow.zh-CN.md +40 -0
  178. package/docs/storage-baseline.md +38 -1
  179. package/docs/storage-baseline.zh-CN.md +22 -0
  180. package/docs/task-delivery.md +36 -8
  181. package/docs/task-delivery.zh-CN.md +24 -7
  182. package/docs/task-discovery.md +28 -0
  183. package/docs/testing/verification-levels.md +2 -2
  184. package/i18n/README.zh-CN.md +4 -3
  185. package/package.json +1 -1
  186. package/skills/yui-leader/references/execution.md +2 -1
  187. package/skills/yui-leader/references/integration.md +20 -0
  188. package/skills/yui-operator/SKILL.md +4 -0
  189. package/skills/yui-reviewer/SKILL.md +3 -0
  190. package/skills/yui-runtime/SKILL.md +4 -0
  191. package/skills/yui-runtime/references/publication.md +20 -4
  192. package/skills/yui-worker/SKILL.md +3 -0
  193. package/dist/web/assets/client/app.js +0 -804
  194. package/dist/web/assets/client/components.js +0 -418
  195. package/dist/web/assets/client/detail.js +0 -333
  196. package/dist/web/assets/client/dock.js +0 -225
  197. package/dist/web/assets/client/evidence.js +0 -172
  198. package/dist/web/assets/client/forms.js +0 -230
  199. package/dist/web/assets/client/i18n.js +0 -1161
  200. package/dist/web/assets/client/markdown.js +0 -114
  201. package/dist/web/assets/client/records.js +0 -63
  202. package/dist/web/assets/client/sections.js +0 -313
  203. package/dist/web/assets/client/theme.js +0 -71
  204. package/dist/web/assets/shell.js +0 -161
  205. package/dist/web/assets/styles/components.js +0 -169
  206. 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: 1,
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
- return {
118
- ...entry,
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 identity, title, and body are the same conclusion regardless of
152
- * who proposed them or when.
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 sourceKey = [
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(`${sourceKey}\u0000${input.title}\u0000${input.body}`)
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 !== 1)
338
- throw new Error("Project knowledge must use schemaVersion 1.");
367
+ if (entry.schemaVersion !== 2)
368
+ throw new Error("Project knowledge must use schemaVersion 2.");
339
369
  requireIdentity(entry.id, "Project knowledge id");
340
- requireText(entry.title, "Project knowledge title");
341
- requireText(entry.body, "Project knowledge body");
342
- if (!["active", "retired"].includes(entry.status)) {
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 !== 1) {
398
- throw new Error("Knowledge proposal must use schemaVersion 1.");
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
- async cleanupTaskForArchive(taskId, disposition) {
166
- let releaseMaintenance;
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.#assertTaskArchiveSnapshot(snapshot);
240
- for (const workspace of laneWorkspaces) {
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
- for (const item of workItems) {
257
- this.#assertTaskArchiveLifecycle(task);
258
- await this.preparer.cleanupWorkItemWorkspace(task.id, item.id, item.status === "accepted" ? disposition : "abandoned");
259
- }
260
- this.#assertTaskArchiveLifecycle(task);
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 explicit force admission has committed.
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
- || !this.store.listEvents(taskId).some(e => e.type === "task.archived" && e.payload.force === "true")) {
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; force archive does not discard local changes.";
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
- if (await this.preparer.inspectIntegrationWorkspace(taskId, owner.integrationAttemptId) === "dirty")
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
- const current = this.store.getTask(taskId);
375
- const delivery = projectTaskRemoteDeliveryFromStore(this.store, current);
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
- * rr5/f4: A WorkItem workspace must not be removed while a DurableJob it
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.status === "queued"
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
- + "Cancel or acknowledge them before cleanup.");
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}.`);