@zq-silk/yui 0.16.1 → 1.0.0-alpha

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 (193) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +9 -4
  4. package/dist/agent/agent.js +4 -9
  5. package/dist/agent/executionComponents.js +4 -4
  6. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  7. package/dist/agentRun/agentRun.js +13 -23
  8. package/dist/artifacts/managedGit.js +3 -56
  9. package/dist/brief/taskBrief.js +3 -3
  10. package/dist/cli/agentConfigurationPicker.js +24 -21
  11. package/dist/cli/commandCatalog.js +18 -10
  12. package/dist/cli/roleWizard.js +41 -30
  13. package/dist/cli/updateCommand.js +35 -13
  14. package/dist/cli/updateOrchestrator.js +73 -44
  15. package/dist/cli/updatePorts.js +76 -86
  16. package/dist/cli/upgradeCommand.js +4 -8
  17. package/dist/cli.js +39 -33
  18. package/dist/commands/controllerCommands.js +2 -2
  19. package/dist/commands/executionAuditCommands.js +2 -2
  20. package/dist/commands/projectCommands.js +1 -1
  21. package/dist/commands/releaseCommands.js +9 -38
  22. package/dist/commands/taskCommands.js +91 -81
  23. package/dist/commands/taskCompletionGate.js +0 -117
  24. package/dist/commands/taskRoleRuntimeStatus.js +2 -2
  25. package/dist/commands/taskUpstreamCommands.js +93 -54
  26. package/dist/context/runContextPack.js +30 -54
  27. package/dist/context/runInputContract.js +9 -0
  28. package/dist/context/sessionBootstrapManifest.js +11 -19
  29. package/dist/controller/agentHostObservation.js +4 -2
  30. package/dist/controller/agentRuntimeObserver.js +4 -4
  31. package/dist/controller/clientRuntime.js +19 -44
  32. package/dist/controller/controller.js +24 -10
  33. package/dist/controller/fileSchedulerStoreAdapter.js +91 -24
  34. package/dist/controller/globalInputDelivery.js +31 -8
  35. package/dist/controller/globalRuntimeAttention.js +34 -0
  36. package/dist/controller/jobSupervisor.js +3 -3
  37. package/dist/controller/operatorNotification.js +31 -0
  38. package/dist/controller/providerRetryAdmission.js +3 -1
  39. package/dist/controller/providerRetryDelivery.js +136 -119
  40. package/dist/controller/runtime.js +2 -28
  41. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  42. package/dist/controller/structuredProviderObservation.js +28 -1
  43. package/dist/controller/updateReconciliation.js +72 -20
  44. package/dist/coordination/workMailbox.js +4 -4
  45. package/dist/core/controllerIdentity.js +25 -0
  46. package/dist/core/controllerProcessIdentity.js +2 -2
  47. package/dist/core/controllerServer.js +4 -2
  48. package/dist/core/protocol.js +1 -1
  49. package/dist/doctor/doctor.js +2 -1
  50. package/dist/domain/validation.js +6 -0
  51. package/dist/event/taskEvent.js +3 -3
  52. package/dist/execution/workItemExecution.js +2 -2
  53. package/dist/executor/agentAdapter.js +28 -96
  54. package/dist/executor/agentConfigurationCatalog.js +13 -68
  55. package/dist/executor/agentConfigurationFields.js +120 -0
  56. package/dist/executor/agentConfigurationProbe.js +55 -80
  57. package/dist/executor/agentExecutor.js +11 -8
  58. package/dist/executor/effectiveLaunch.js +3 -3
  59. package/dist/executor/executorRegistry.js +0 -9
  60. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  61. package/dist/grant/capabilityGrant.js +2 -2
  62. package/dist/input/inputRequest.js +4 -4
  63. package/dist/integration/changeSet.js +3 -3
  64. package/dist/integration/integrationAttempt.js +3 -3
  65. package/dist/integration/integrationSourceApplication.js +1 -1
  66. package/dist/interaction/operatorPresentation.js +2 -1
  67. package/dist/job/durableJob.js +1 -1
  68. package/dist/job/jobRunner.js +2 -2
  69. package/dist/message/message.js +8 -6
  70. package/dist/message/messageContinuation.js +8 -9
  71. package/dist/milestone/milestone.js +3 -3
  72. package/dist/observability/executionAudit.js +1 -10
  73. package/dist/observability/runtimeIdentity.js +0 -23
  74. package/dist/output/agentConfigurationPresentation.js +4 -2
  75. package/dist/profile/agentProfile.js +3 -3
  76. package/dist/release/releaseHandover.js +2 -2
  77. package/dist/release/releaseIdempotencyStore.js +0 -23
  78. package/dist/release/releaseWorkflowPorts.js +8 -4
  79. package/dist/release/runtimeRelease.js +9 -7
  80. package/dist/repository/gitWorkspace.js +66 -19
  81. package/dist/repository/project.js +3 -3
  82. package/dist/repository/taskWorkspacePreparer.js +75 -68
  83. package/dist/resources/liveReferences.js +1 -1
  84. package/dist/resources/resourceRegistry.js +90 -44
  85. package/dist/resources/sqliteResourceRegistry.js +5 -7
  86. package/dist/review/reviewRound.js +5 -5
  87. package/dist/role/role.js +7 -8
  88. package/dist/runtime/acpProtocol.js +2 -3
  89. package/dist/runtime/acpSession.js +21 -5
  90. package/dist/runtime/agentDriverObservation.js +1 -1
  91. package/dist/runtime/agentEndpoint.js +4 -1
  92. package/dist/runtime/agentHost.js +25 -65
  93. package/dist/runtime/agentHostCleanup.js +85 -0
  94. package/dist/runtime/agentHostProtocol.js +3 -3
  95. package/dist/runtime/builtinAgentDrivers.js +46 -119
  96. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  97. package/dist/runtime/codexAppServerRuntime.js +55 -89
  98. package/dist/runtime/codexInteractiveHost.js +1 -1
  99. package/dist/runtime/jsonLineChannel.js +35 -7
  100. package/dist/runtime/launchBroker.js +1 -1
  101. package/dist/runtime/processExitObservation.js +1 -1
  102. package/dist/runtime/providerContinuationReconciliationService.js +86 -38
  103. package/dist/runtime/providerRetry.js +15 -0
  104. package/dist/runtime/providerRuntimeIdentity.js +3 -3
  105. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  106. package/dist/runtime/runtimeCoherence.js +7 -3
  107. package/dist/runtime/runtimeObservation.js +3 -3
  108. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  109. package/dist/runtime/structuredProviderHost.js +106 -81
  110. package/dist/runtime/taskRuntimeIsolation.js +3 -3
  111. package/dist/runtime/tmuxAdapters.js +3 -3
  112. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  113. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  114. package/dist/scheduler/taskWake.js +1 -1
  115. package/dist/storage/baselineSchema.js +606 -0
  116. package/dist/storage/homeLayout.js +5 -16
  117. package/dist/storage/recordValidation.js +16 -4
  118. package/dist/storage/sqliteSchema.js +106 -1741
  119. package/dist/storage/sqliteStore.js +16 -14
  120. package/dist/storage/storageSchema.js +8 -7
  121. package/dist/storage/storageVersions.js +23 -16
  122. package/dist/storage/taskStore.js +3 -29
  123. package/dist/storage/upgrade/upgradeOrchestrator.js +14 -112
  124. package/dist/task/nextAction.js +33 -23
  125. package/dist/task/task.js +17 -8
  126. package/dist/task/taskActivation.js +5 -4
  127. package/dist/telemetry/sqliteTelemetryStore.js +2 -2
  128. package/dist/verification/gateArtifact.js +5 -3
  129. package/dist/verification/verificationPlan.js +6 -7
  130. package/dist/web/assets/assetManifest.js +2 -0
  131. package/dist/web/assets/client/app.js +72 -3
  132. package/dist/web/assets/client/components.js +2 -1
  133. package/dist/web/assets/client/i18n.js +2 -0
  134. package/dist/web/assets/client/taskSummary.js +345 -0
  135. package/dist/web/assets/client/taskSurface.js +44 -26
  136. package/dist/web/assets/client/view.js +33 -0
  137. package/dist/web/assets/shell.js +1 -0
  138. package/dist/web/assets/styles/cards.js +21 -0
  139. package/dist/web/webServer.js +39 -5
  140. package/dist/web/webSessions.js +165 -0
  141. package/dist/web/webSnapshot.js +23 -0
  142. package/dist/web/webTaskSurface.js +23 -0
  143. package/dist/workItem/workItem.js +6 -6
  144. package/dist/workspace/cleanupInspection.js +1 -9
  145. package/dist/worktree/managedWorkspace.js +3 -3
  146. package/docs/managed-turn-and-session-runtime.md +50 -7
  147. package/docs/managed-turn-and-session-runtime.zh-CN.md +36 -5
  148. package/docs/observability/README.md +47 -0
  149. package/docs/observability/README.zh-CN.md +37 -0
  150. package/docs/project-refresh.md +9 -0
  151. package/docs/project-refresh.zh-CN.md +8 -0
  152. package/docs/provider-retry.md +35 -0
  153. package/docs/release-workflow.md +70 -214
  154. package/docs/release-workflow.zh-CN.md +53 -157
  155. package/docs/roles-and-configuration.md +30 -0
  156. package/docs/roles-and-configuration.zh-CN.md +20 -0
  157. package/docs/sqlite-control-plane-design.md +48 -289
  158. package/docs/sqlite-control-plane-design.zh-CN.md +37 -53
  159. package/docs/storage-baseline.md +132 -0
  160. package/docs/storage-baseline.zh-CN.md +106 -0
  161. package/docs/task-delivery.md +110 -2
  162. package/docs/task-delivery.zh-CN.md +82 -2
  163. package/docs/task-discovery.md +9 -0
  164. package/docs/task-discovery.zh-CN.md +6 -0
  165. package/docs/testing/verification-levels.md +42 -164
  166. package/docs/testing/verification-levels.zh-CN.md +29 -115
  167. package/i18n/README.zh-CN.md +7 -4
  168. package/package.json +1 -1
  169. package/skills/yui-leader/SKILL.md +11 -0
  170. package/skills/yui-leader/references/execution.md +22 -0
  171. package/skills/yui-leader/references/planning.md +6 -2
  172. package/skills/yui-operator/SKILL.md +45 -13
  173. package/skills/yui-operator/references/task-delivery.md +99 -0
  174. package/skills/yui-runtime/SKILL.md +5 -3
  175. package/skills/yui-runtime/references/publication.md +56 -5
  176. package/skills/yui-runtime/references/recovery.md +12 -2
  177. package/dist/runtime/agentHostCompatibility.js +0 -127
  178. package/dist/storage/migrations/agentFailureContext.js +0 -22
  179. package/dist/storage/migrations/agentRunContract.js +0 -159
  180. package/dist/storage/migrations/artifactsToGit.js +0 -338
  181. package/dist/storage/migrations/collapseWorktreeLayout.js +0 -963
  182. package/dist/storage/migrations/currentInputContract.js +0 -86
  183. package/dist/storage/migrations/currentRuntimeContract.js +0 -228
  184. package/dist/storage/migrations/historicalVerificationPlan.js +0 -35
  185. package/dist/storage/migrations/integrationContinuation.js +0 -105
  186. package/dist/storage/migrations/narrowAgentFailureContext.js +0 -65
  187. package/dist/storage/migrations/notificationOnlyWakes.js +0 -74
  188. package/dist/storage/migrations/removeRuntimeGeneration.js +0 -207
  189. package/dist/storage/migrations/submitIntent.js +0 -126
  190. package/dist/storage/migrations/unifyHomeLayout.js +0 -925
  191. package/dist/storage/migrations/verificationPlanV1.js +0 -162
  192. package/dist/storage/migrations/verificationPolicy.js +0 -74
  193. package/dist/storage/migrations/workItemHistory.js +0 -46
@@ -545,7 +545,7 @@ export class FileTaskWorkspacePreparer {
545
545
  const taskSegment = taskWorkspaceRefSegment(workspaceIdentity === undefined ? task : { ...task, workspaceIdentity });
546
546
  const root = this.#taskWorkspaceRoot(task.id);
547
547
  const prepared = [];
548
- const createdClonePaths = new Set();
548
+ const createdClones = [];
549
549
  const baselines = new Map();
550
550
  try {
551
551
  // Resolve every remote baseline before creating any Task clone. A later
@@ -595,7 +595,14 @@ export class FileTaskWorkspacePreparer {
595
595
  })
596
596
  : await this.git.inspect(previous.path, "HEAD");
597
597
  if (previous === undefined)
598
- createdClonePaths.add(physical.root);
598
+ createdClones.push({
599
+ project,
600
+ entry: {
601
+ projectId: project.id, directory: binding.directory, access: "write",
602
+ path: destination, branch: identity.branch,
603
+ baseRef: baseline.recordedBaseRef, baseCommit: physical.baseCommit
604
+ }
605
+ });
599
606
  const physicalBranch = await this.git.headRef(physical.root);
600
607
  if (physical.root !== (previous?.path ?? destination)
601
608
  || physicalBranch !== (previous?.branch ?? identity.branch)) {
@@ -794,16 +801,7 @@ export class FileTaskWorkspacePreparer {
794
801
  // A failed or conflicted preparation owns no durable record: drop the
795
802
  // branches too, so a retry mints a clean identity without half-created
796
803
  // refs behind. Adopted (already catalogued) worktrees are never touched.
797
- await this.#discardUnadoptedEntries(task, taskSegment, prepared, MAIN_WORKTREE, true);
798
- const recorded = new Set(prepared.map(({ entry }) => entry.path));
799
- for (const path of createdClonePaths) {
800
- if (recorded.has(path))
801
- continue;
802
- const removal = await this.git.removeStrandedWorktree(path);
803
- if (removal === "dirty") {
804
- throw new Error(`Unadopted Task clone is dirty and was retained at ${path}; inspect it and retry.`);
805
- }
806
- }
804
+ await this.#discardUnadoptedEntries(task, taskSegment, createdClones, MAIN_WORKTREE, true, undefined, error);
807
805
  if (activate)
808
806
  await removeWorkspaceView(root);
809
807
  throw error;
@@ -1125,7 +1123,7 @@ export class FileTaskWorkspacePreparer {
1125
1123
  });
1126
1124
  }
1127
1125
  catch (error) {
1128
- await this.#discardUnadoptedEntries(lockedTask, taskSegment, prepared, item.id);
1126
+ await this.#discardUnadoptedEntries(lockedTask, taskSegment, prepared, item.id, false, undefined, error);
1129
1127
  throw error;
1130
1128
  }
1131
1129
  }
@@ -1276,7 +1274,7 @@ export class FileTaskWorkspacePreparer {
1276
1274
  return workspace;
1277
1275
  }
1278
1276
  catch (error) {
1279
- await this.#discardUnadoptedEntries(lockedTask, taskSegment, prepared, managedWorktreeName(owner), true);
1277
+ await this.#discardUnadoptedEntries(lockedTask, taskSegment, prepared, managedWorktreeName(owner), true, undefined, error);
1280
1278
  throw error;
1281
1279
  }
1282
1280
  }
@@ -1354,48 +1352,39 @@ export class FileTaskWorkspacePreparer {
1354
1352
  return "missing";
1355
1353
  const task = requireTask(this.store, workspace.owner.taskId);
1356
1354
  const taskSegment = this.#taskSegment(task);
1355
+ const root = this.#executionLaneWorkspaceRoot(task.id, workspace.owner.executionGroupId, workspace.owner.executionLaneId);
1356
+ if (workspace.root !== root)
1357
+ throw new Error("Unadopted Lane root does not match its managed owner.");
1357
1358
  const writable = workspace.entries.filter(({ access }) => access === "write");
1358
- let state;
1359
+ const completed = [];
1360
+ let target = root;
1359
1361
  try {
1360
- state = await this.#inspectEntries(task.id, taskSegment, managedWorktreeName(workspace.owner), writable);
1361
- }
1362
- catch (error) {
1363
- // A `project migrate` between preparation and compensation switches the
1364
- // catalog, so the worktree's common-dir no longer matches the Project's
1365
- // current repository. Fall back to removing the stranded worktree
1366
- // through its own Git identity.
1367
- if (!(error instanceof Error && error.message.includes("belongs to another project"))) {
1368
- throw error;
1369
- }
1370
- let removed = false;
1362
+ // This owner was never adopted. Its captured identity is sufficient to
1363
+ // inspect its own Git; no catalog-error matching or filesystem fallback.
1371
1364
  for (const entry of writable) {
1372
- const result = await this.git.removeStrandedWorktree(entry.path);
1373
- if (result === "dirty")
1374
- return "dirty";
1375
- removed ||= result === "removed";
1365
+ target = entry.path;
1366
+ const result = await this.git.removeStrandedWorktree(entry.path, {
1367
+ container: root, directory: entry.directory, taskSegment,
1368
+ roleName: managedWorktreeName(workspace.owner), expectedCommit: entry.baseCommit,
1369
+ deleteBranch: true
1370
+ });
1371
+ if (result === "dirty") {
1372
+ if (completed.length === 0)
1373
+ return "dirty";
1374
+ throw new Error(`Unadopted Lane is dirty and retained at ${entry.path}.`);
1375
+ }
1376
+ completed.push({ path: entry.path, result });
1376
1377
  }
1377
- await removeWorkspaceView(workspace.root);
1378
- return removed ? "removed" : "missing";
1378
+ target = root;
1379
+ await removeWorkspaceView(root);
1380
+ return completed.some(({ result }) => result === "removed") ? "removed" : "missing";
1379
1381
  }
1380
- if (state === "dirty")
1381
- return "dirty";
1382
- let removed = false;
1383
- for (const entry of writable) {
1384
- const project = requireProject(this.store, entry.projectId);
1385
- const result = await this.git.removeWorktree({
1386
- repositoryPath: this.#taskRepositoryPath(task.id, project.id),
1387
- container: dirname(entry.path),
1388
- directory: entry.directory,
1389
- taskSegment,
1390
- roleName: managedWorktreeName(workspace.owner),
1391
- deleteBranch: true
1392
- });
1393
- if (result === "dirty")
1394
- return "dirty";
1395
- removed ||= result === "removed";
1382
+ catch (error) {
1383
+ const result = { completed, failedPath: target,
1384
+ remaining: writable.map(({ path }) => path).filter(path => !completed.some(done => done.path === path)) };
1385
+ throw Object.assign(new Error(`Unadopted Lane cleanup failed: ${errorText(error)}. Effects: ${JSON.stringify(result)}. `
1386
+ + "Failed-command effects are unknown; inspect the exact target before continuing.", { cause: error }), { result });
1396
1387
  }
1397
- await removeWorkspaceView(workspace.root);
1398
- return removed ? "removed" : "missing";
1399
1388
  }
1400
1389
  async cleanupExecutionLaneWorkspacesForWorkItem(taskId, workItemId) {
1401
1390
  let result = "missing";
@@ -1754,7 +1743,7 @@ export class FileTaskWorkspacePreparer {
1754
1743
  });
1755
1744
  }
1756
1745
  catch (error) {
1757
- await this.#discardUnadoptedEntries(task, taskSegment, prepared, this.#reviewWorktreeName(round), existing === null, new Set([...retained.values()].map(({ entry }) => entry.path)));
1746
+ await this.#discardUnadoptedEntries(task, taskSegment, prepared, this.#reviewWorktreeName(round), existing === null, new Set([...retained.values()].map(({ entry }) => entry.path)), error);
1758
1747
  throw error;
1759
1748
  }
1760
1749
  }
@@ -2243,28 +2232,43 @@ export class FileTaskWorkspacePreparer {
2243
2232
  return this.store.getConfig().defaultWorkspace ?? process.cwd();
2244
2233
  }
2245
2234
  async #discardUnadoptedEntries(task, taskSegment, prepared, roleName, deleteBranch = false, adoptedPaths = new Set(this.store.listManagedWorkspaces(task.id)
2246
- .flatMap((workspace) => workspace.entries.map(({ path }) => path)))) {
2247
- for (const { project, entry } of prepared.filter(({ entry }) => entry.access === "write" && !adoptedPaths.has(entry.path))) {
2248
- if (roleName === MAIN_WORKTREE) {
2249
- const removal = await this.git.removeStrandedWorktree(entry.path);
2235
+ .flatMap((workspace) => workspace.entries.map(({ path }) => path))), originalError) {
2236
+ const candidates = prepared.filter(({ entry }) => entry.access === "write" && !adoptedPaths.has(entry.path));
2237
+ const completed = [];
2238
+ for (const { project, entry } of candidates) {
2239
+ try {
2240
+ const removal = roleName === MAIN_WORKTREE
2241
+ ? await this.git.removeTaskClone({
2242
+ path: entry.path, container: this.#taskWorkspaceRoot(task.id),
2243
+ directory: entry.directory, taskSegment, branch: entry.branch,
2244
+ expectedCommit: entry.baseCommit
2245
+ })
2246
+ : await this.git.removeWorktree({
2247
+ repositoryPath: this.#taskRepositoryPath(task.id, project.id),
2248
+ container: dirname(entry.path),
2249
+ directory: entry.directory,
2250
+ taskSegment,
2251
+ roleName,
2252
+ ...(deleteBranch ? { deleteBranch: true } : {})
2253
+ });
2250
2254
  if (removal === "dirty") {
2251
- throw new Error(`Unadopted Task clone is dirty and was retained at ${entry.path}; inspect it and retry.`);
2255
+ throw new Error(`Unadopted workspace is dirty and was retained at ${entry.path}.`);
2252
2256
  }
2257
+ completed.push({ path: entry.path, result: removal });
2253
2258
  this.#resourceRegistrar().markPathsDeleted([entry.path]);
2254
- continue;
2255
2259
  }
2256
- const removal = await this.git.removeWorktree({
2257
- repositoryPath: this.#taskRepositoryPath(task.id, project.id),
2258
- container: dirname(entry.path),
2259
- directory: entry.directory,
2260
- taskSegment,
2261
- roleName,
2262
- ...(deleteBranch ? { deleteBranch: true } : {})
2263
- });
2264
- if (removal === "dirty") {
2265
- throw new Error(`Unadopted managed worktree is dirty and was retained at ${entry.path}; inspect it and retry.`);
2260
+ catch (cleanupError) {
2261
+ const remaining = candidates.map(({ entry }) => entry.path)
2262
+ .filter(path => !completed.some(done => done.path === path));
2263
+ const result = { completed, failedPath: entry.path, remaining };
2264
+ throw Object.assign(new AggregateError([originalError, cleanupError], `Workspace preparation failed: ${errorText(originalError)}. Compensation failed: ${errorText(cleanupError)}. `
2265
+ + `Effects: ${JSON.stringify(result)}. Remaining paths may be partially changed; inspect before retrying.`), { result });
2266
2266
  }
2267
- this.#resourceRegistrar().markPathsDeleted([entry.path]);
2267
+ }
2268
+ if (originalError instanceof Error && completed.length > 0) {
2269
+ // Preserve the original error class (notably a safe CAS retry), while
2270
+ // making completed compensation visible to the caller's existing report.
2271
+ originalError.message += ` Workspace compensation: ${JSON.stringify(completed)}.`;
2268
2272
  }
2269
2273
  }
2270
2274
  #taskRepositoryPath(taskId, projectId) {
@@ -2653,3 +2657,6 @@ function errorCode(error) {
2653
2657
  ? String(error.code)
2654
2658
  : undefined;
2655
2659
  }
2660
+ function errorText(error) {
2661
+ return error instanceof Error ? error.message : String(error);
2662
+ }
@@ -91,7 +91,7 @@ export async function scanLiveReferences(input) {
91
91
  diagnostics.push(...tmuxResult.diagnostics);
92
92
  // 3. Controller discovery: a running Controller protects only its own
93
93
  // discovery record. Its cwd and open files are covered by the /proc
94
- // scan above, so a live Controller does not blanket-protect legacy
94
+ // scan above, so a live Controller does not blanket-protect unattributed
95
95
  // deployments and worktrees that no descriptor references — automatic
96
96
  // GC (which runs inside the Controller) would otherwise be a no-op.
97
97
  const discovery = readControllerDiscovery(home);
@@ -43,7 +43,7 @@ export function listResourceRecords(state) {
43
43
  return Object.values(state.records);
44
44
  }
45
45
  export function parseResourceRegistryState(value) {
46
- if (typeof value !== "object" || value === null) {
46
+ if (!isRecord(value)) {
47
47
  throw new Error("Resource registry root is not an object.");
48
48
  }
49
49
  const record = value;
@@ -51,15 +51,12 @@ export function parseResourceRegistryState(value) {
51
51
  throw new Error(`Resource registry schemaVersion is ${String(record.schemaVersion)}; `
52
52
  + `expected ${RESOURCE_REGISTRY_SCHEMA_VERSION}.`);
53
53
  }
54
- if (typeof record.records !== "object" || record.records === null) {
54
+ if (!isRecord(record.records)) {
55
55
  throw new Error("Resource registry records is not an object.");
56
56
  }
57
57
  const records = {};
58
58
  for (const [key, entry] of Object.entries(record.records)) {
59
- const parsed = parseResourceRecord(entry);
60
- if (parsed === undefined) {
61
- throw new Error(`Resource registry record ${key} is malformed.`);
62
- }
59
+ const parsed = parseResourceRecord(entry, key);
63
60
  if (parsed.id !== key) {
64
61
  throw new Error(`Resource registry record key ${key} does not match id ${parsed.id}.`);
65
62
  }
@@ -70,47 +67,96 @@ export function parseResourceRegistryState(value) {
70
67
  records: Object.freeze(records)
71
68
  });
72
69
  }
73
- function parseResourceRecord(value) {
74
- if (typeof value !== "object" || value === null)
75
- return undefined;
76
- const record = value;
70
+ function parseResourceRecord(value, key) {
71
+ const invalid = (field) => {
72
+ throw new Error(`Resource registry record ${key} is malformed at ${field}; stored evidence was not repaired.`);
73
+ };
74
+ const object = (value, field) => isRecord(value) ? value : invalid(field);
75
+ const text = (value, field) => {
76
+ if (typeof value !== "string" || value.trim().length === 0)
77
+ invalid(field);
78
+ };
79
+ const timestamp = (value, field) => {
80
+ text(value, field);
81
+ if (!Number.isFinite(Date.parse(value)))
82
+ invalid(field);
83
+ };
84
+ const member = (value, choices, field) => {
85
+ if (typeof value !== "string" || !choices.includes(value))
86
+ invalid(field);
87
+ };
88
+ const optionalText = (record, fields, prefix = "") => {
89
+ for (const field of fields) {
90
+ if (record[field] !== undefined)
91
+ text(record[field], `${prefix}${field}`);
92
+ }
93
+ };
94
+ const record = object(value, "record");
77
95
  if (record.schemaVersion !== RESOURCE_REGISTRY_SCHEMA_VERSION)
78
- return undefined;
79
- if (typeof record.id !== "string" || typeof record.kind !== "string"
80
- || typeof record.path !== "string" || typeof record.disposition !== "string"
81
- || typeof record.updatedAt !== "string") {
82
- return undefined;
96
+ invalid("schemaVersion");
97
+ text(record.id, "id");
98
+ text(record.path, "path");
99
+ timestamp(record.updatedAt, "updatedAt");
100
+ member(record.kind, ["worktree", "deployment", "runtime-artifact"], "kind");
101
+ member(record.disposition, ["active", "releasable", "quarantined", "deleted",
102
+ "retained-dirty", "retained-unowned", "retained-unproven", "cleanup-failed"], "disposition");
103
+ member(record.cleanliness, ["clean", "dirty", "unknown", "n/a"], "cleanliness");
104
+ if (!Array.isArray(record.activeRefs))
105
+ invalid("activeRefs");
106
+ record.activeRefs.forEach((ref, index) => text(ref, `activeRefs[${index}]`));
107
+ const owner = object(record.owner, "owner");
108
+ text(owner.home, "owner.home");
109
+ member(owner.basis, ["durable-record", "marker", "descriptor", "naming-convention", "unattributed"], "owner.basis");
110
+ optionalText(owner, ["projectId", "taskId", "workItemId", "reviewRoundId", "integrationAttemptId"], "owner.");
111
+ for (const field of ["createdAt", "lastReferencedAt"]) {
112
+ if (record[field] !== undefined)
113
+ timestamp(record[field], field);
83
114
  }
84
- if (!Number.isFinite(Date.parse(record.updatedAt)))
85
- return undefined;
86
- if (typeof record.owner !== "object" || record.owner === null)
87
- return undefined;
88
- const owner = record.owner;
89
- if (typeof owner.home !== "string" || typeof owner.basis !== "string") {
90
- return undefined;
115
+ if (record.sizeBytes !== undefined
116
+ && (typeof record.sizeBytes !== "number" || !Number.isFinite(record.sizeBytes) || record.sizeBytes < 0)) {
117
+ invalid("sizeBytes");
91
118
  }
119
+ if (record.blocker !== undefined && typeof record.blocker !== "string")
120
+ invalid("blocker");
121
+ if (record.git !== undefined) {
122
+ const git = object(record.git, "git");
123
+ text(git.repositoryPath, "git.repositoryPath");
124
+ optionalText(git, ["commonDir", "branch", "head"], "git.");
125
+ }
126
+ if (record.quarantine !== undefined) {
127
+ const quarantine = object(record.quarantine, "quarantine");
128
+ text(quarantine.path, "quarantine.path");
129
+ text(quarantine.originalPath, "quarantine.originalPath");
130
+ timestamp(quarantine.movedAt, "quarantine.movedAt");
131
+ member(quarantine.method, ["move", "git-worktree-remove"], "quarantine.method");
132
+ if (quarantine.gitRestore !== undefined) {
133
+ const restore = object(quarantine.gitRestore, "quarantine.gitRestore");
134
+ text(restore.repositoryPath, "quarantine.gitRestore.repositoryPath");
135
+ optionalText(restore, ["branch", "head"], "quarantine.gitRestore.");
136
+ }
137
+ }
138
+ if (record.cleanupReceipt !== undefined) {
139
+ const receipt = object(record.cleanupReceipt, "cleanupReceipt");
140
+ timestamp(receipt.removedAt, "cleanupReceipt.removedAt");
141
+ member(receipt.method, ["git-worktree-remove", "quarantine-purge", "runtime-cleanup"], "cleanupReceipt.method");
142
+ }
143
+ // Validation enforces the existing persisted contract, without defaults,
144
+ // filtering, or a second historical reader. Constructors own new values.
145
+ const parsed = record;
92
146
  return Object.freeze({
93
- schemaVersion: RESOURCE_REGISTRY_SCHEMA_VERSION,
94
- id: record.id,
95
- kind: record.kind,
96
- path: record.path,
97
- owner: Object.freeze({ ...owner }),
98
- ...(record.git === undefined ? {} : { git: record.git }),
99
- ...(typeof record.createdAt === "string" ? { createdAt: record.createdAt } : {}),
100
- ...(typeof record.lastReferencedAt === "string"
101
- ? { lastReferencedAt: record.lastReferencedAt }
102
- : {}),
103
- ...(typeof record.sizeBytes === "number" ? { sizeBytes: record.sizeBytes } : {}),
104
- cleanliness: (record.cleanliness ?? "unknown"),
105
- activeRefs: Array.isArray(record.activeRefs)
106
- ? Object.freeze(record.activeRefs.filter((ref) => typeof ref === "string"))
107
- : Object.freeze([]),
108
- disposition: record.disposition,
109
- ...(typeof record.blocker === "string" ? { blocker: record.blocker } : {}),
110
- ...(record.quarantine === undefined ? {} : { quarantine: record.quarantine }),
111
- ...(record.cleanupReceipt === undefined
112
- ? {}
113
- : { cleanupReceipt: record.cleanupReceipt }),
114
- updatedAt: record.updatedAt
147
+ ...parsed,
148
+ owner: Object.freeze({ ...parsed.owner }),
149
+ activeRefs: Object.freeze([...parsed.activeRefs]),
150
+ ...(parsed.git === undefined ? {} : { git: Object.freeze({ ...parsed.git }) }),
151
+ ...(parsed.quarantine === undefined ? {} : { quarantine: Object.freeze({
152
+ ...parsed.quarantine,
153
+ ...(parsed.quarantine.gitRestore === undefined ? {} : {
154
+ gitRestore: Object.freeze({ ...parsed.quarantine.gitRestore })
155
+ })
156
+ }) }),
157
+ ...(parsed.cleanupReceipt === undefined ? {} : { cleanupReceipt: Object.freeze({ ...parsed.cleanupReceipt }) })
115
158
  });
116
159
  }
160
+ function isRecord(value) {
161
+ return typeof value === "object" && value !== null && !Array.isArray(value);
162
+ }
@@ -14,7 +14,7 @@ import { existsSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { isDeepStrictEqual } from "node:util";
16
16
  import Database from "better-sqlite3";
17
- import { migrateSqliteSchema } from "../storage/sqliteSchema.js";
17
+ import { validateSqliteSchema } from "../storage/sqliteSchema.js";
18
18
  import { emptyResourceRegistry, parseResourceRegistryState } from "./resourceRegistry.js";
19
19
  import { RESOURCE_REGISTRY_SCHEMA_VERSION } from "./resourceTypes.js";
20
20
  export const SQLITE_RESOURCE_REGISTRY_TABLE = "resource_registry";
@@ -31,10 +31,10 @@ export class SqliteResourceRegistry {
31
31
  }
32
32
  this.#db = new Database(dbPath);
33
33
  try {
34
+ validateSqliteSchema(this.#db);
34
35
  this.#db.pragma("journal_mode = WAL");
35
36
  this.#db.pragma("foreign_keys = ON");
36
37
  this.#db.pragma("busy_timeout = 5000");
37
- migrateSqliteSchema(this.#db, { mode: "validate" });
38
38
  }
39
39
  catch (error) {
40
40
  this.#db.close();
@@ -42,7 +42,7 @@ export class SqliteResourceRegistry {
42
42
  }
43
43
  }
44
44
  load() {
45
- const rows = this.#db.prepare(`SELECT payload FROM ${SQLITE_RESOURCE_REGISTRY_TABLE}`).all();
45
+ const rows = this.#db.prepare(`SELECT id, payload FROM ${SQLITE_RESOURCE_REGISTRY_TABLE}`).all();
46
46
  if (rows.length === 0)
47
47
  return emptyResourceRegistry();
48
48
  const records = {};
@@ -52,14 +52,12 @@ export class SqliteResourceRegistry {
52
52
  parsed = JSON.parse(row.payload);
53
53
  }
54
54
  catch (error) {
55
- throw new Error(`SQLite resource registry has a corrupt payload: `
55
+ throw new Error(`SQLite resource registry record ${row.id} has a corrupt payload: `
56
56
  + `${error instanceof Error ? error.message : "unknown error"}.`, { cause: error });
57
57
  }
58
58
  const state = parseResourceRegistryState({
59
59
  schemaVersion: RESOURCE_REGISTRY_SCHEMA_VERSION,
60
- records: { [typeof parsed === "object" && parsed !== null && "id" in parsed
61
- ? String(parsed.id)
62
- : ""]: parsed }
60
+ records: { [row.id]: parsed }
63
61
  });
64
62
  Object.assign(records, state.records);
65
63
  }
@@ -5,7 +5,7 @@ import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
5
5
  import { assertExecutionGroupTransition, retryFailedExecutionLanes, validateExecutionGroup, validateReviewExecutionAssignment } from "../execution/workItemExecution.js";
6
6
  export function createReviewRound(id, taskId, workItemId, candidateId, reviewerRoleName, requestedBy, reviewBaseCommit, now, executionGroup) {
7
7
  return validateReviewRound({
8
- schemaVersion: 8,
8
+ schemaVersion: 1,
9
9
  id: requireIdentity(id, "ReviewRound id"),
10
10
  taskId: requireIdentity(taskId, "Task id"),
11
11
  workItemId: requireIdentity(workItemId, "Work Item id"),
@@ -22,7 +22,7 @@ export function createReviewRound(id, taskId, workItemId, candidateId, reviewerR
22
22
  export function createTaskReviewRound(id, taskId, reviewerRoleName, requestedBy, taskCandidate, now, taskFinalReviewContract, executionGroup) {
23
23
  const candidate = validateTaskReviewCandidate(taskCandidate);
24
24
  return validateReviewRound({
25
- schemaVersion: 8,
25
+ schemaVersion: 1,
26
26
  id: requireIdentity(id, "ReviewRound id"),
27
27
  taskId: requireIdentity(taskId, "Task id"),
28
28
  reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
@@ -48,7 +48,7 @@ export function createTaskReviewRound(id, taskId, reviewerRoleName, requestedBy,
48
48
  export function createTaskDeltaReviewRound(id, taskId, reviewerRoleName, requestedBy, taskCandidate, deltaRecheck, now, taskFinalReviewContract, executionGroup) {
49
49
  const candidate = validateTaskReviewCandidate(taskCandidate);
50
50
  return validateReviewRound({
51
- schemaVersion: 8,
51
+ schemaVersion: 1,
52
52
  id: requireIdentity(id, "ReviewRound id"),
53
53
  taskId: requireIdentity(taskId, "Task id"),
54
54
  reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
@@ -285,8 +285,8 @@ export function validateReviewRound(round) {
285
285
  "createdAt",
286
286
  "endedAt"
287
287
  ], "ReviewRound");
288
- if (round.schemaVersion !== 8)
289
- throw new Error("ReviewRound must use schemaVersion 8.");
288
+ if (round.schemaVersion !== 1)
289
+ throw new Error("ReviewRound must use schemaVersion 1.");
290
290
  validateTaskRecordReference({ taskId: round.taskId, localId: round.id }, "reviewRound");
291
291
  requireIdentity(round.reviewerRoleName, "Reviewer Role");
292
292
  requireCommit(round.reviewBaseCommit, "Review base commit");
package/dist/role/role.js CHANGED
@@ -30,14 +30,14 @@ export function createRole(taskId, name, bindings, activeAgentId, workspace, now
30
30
  const owner = createRoleOwner(name, bindings, activeAgentId, workspace, now, profile, defaultAccess);
31
31
  return validateTaskRole({
32
32
  ...owner,
33
- schemaVersion: 4,
33
+ schemaVersion: 1,
34
34
  taskId: requireSafeIdentity(taskId, "Task id")
35
35
  });
36
36
  }
37
37
  export function createGlobalRole(name, bindings, activeAgentId, workspace, now, profile = {}, defaultAccess = "write") {
38
38
  return validateGlobalRole({
39
39
  ...createRoleOwner(name, bindings, activeAgentId, workspace, now, profile, defaultAccess),
40
- schemaVersion: 3
40
+ schemaVersion: 1
41
41
  });
42
42
  }
43
43
  export function copyGlobalRoleToTaskRole(globalRole, taskId, now, name = globalRole.name) {
@@ -45,7 +45,7 @@ export function copyGlobalRoleToTaskRole(globalRole, taskId, now, name = globalR
45
45
  const timestamp = now.toISOString();
46
46
  return validateTaskRole({
47
47
  ...cloneProfile(globalRole),
48
- schemaVersion: 4,
48
+ schemaVersion: 1,
49
49
  launchRevision: 1,
50
50
  defaultAccess: globalRole.defaultAccess,
51
51
  taskId: requireSafeIdentity(taskId, "Task id"),
@@ -189,6 +189,8 @@ export function unbindRoleAgent(role, sessions, agentId, now) {
189
189
  return { role: updatedRole, sessions: updatedSessions };
190
190
  }
191
191
  export function validateGlobalRole(role) {
192
+ if (Object.hasOwn(role, "taskId"))
193
+ throw new Error("Global Role cannot carry a Task identity.");
192
194
  return validateRoleOwner(role);
193
195
  }
194
196
  export function validateTaskRole(role) {
@@ -218,8 +220,7 @@ function createRoleOwner(name, bindings, activeAgentId, workspace, now, profile,
218
220
  };
219
221
  }
220
222
  function validateRoleOwner(role) {
221
- const expectedSchemaVersion = "taskId" in role ? 4 : 3;
222
- if (role.schemaVersion !== expectedSchemaVersion) {
223
+ if (role.schemaVersion !== 1) {
223
224
  throw new Error("Role schema version is invalid.");
224
225
  }
225
226
  if (!Number.isSafeInteger(role.launchRevision) || role.launchRevision < 1) {
@@ -256,9 +257,7 @@ function validateRoleOwner(role) {
256
257
  function validateRoleAgentBinding(binding) {
257
258
  const agentId = requireSafeIdentity(binding.agentId, "Role Agent id");
258
259
  const adapterId = requireSupportedAdapterId(binding.adapterId);
259
- // Storage 10 backfilled every stored binding, so a missing component here is
260
- // a corrupt record rather than an old one. Defaulting it would invent a
261
- // product identity for data that never lost one.
260
+ // The current contract requires an explicit execution component.
262
261
  if (binding.component === undefined) {
263
262
  throw new Error(`Role Agent binding is missing its execution component: ${agentId}.`);
264
263
  }
@@ -126,9 +126,8 @@ export function acpPromptRequest(sessionId, text) {
126
126
  /**
127
127
  * Read a `configOptions` array from a session setup or set-option result.
128
128
  *
129
- * Returns undefined when the field is absent, which is how an Agent that
130
- * predates config options answers; that is a different fact from an Agent
131
- * answering with an empty list, which offers nothing configurable. Callers must
129
+ * Returns undefined when the field is absent: configuration support is then
130
+ * unobserved. An explicit empty list instead offers nothing configurable. Callers must
132
131
  * be able to tell those apart, so the absence is not flattened to `[]`.
133
132
  */
134
133
  export function readAcpConfigOptions(value) {
@@ -5,6 +5,7 @@ import { ACP_CONFIGURATION_ORDER, acpRunConfigurationOptions, confirmAcpConfigur
5
5
  import { handshakeObservationFrom, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
6
6
  import { JsonLineChannel, terminateProcessGroup } from "./jsonLineChannel.js";
7
7
  import { ProviderDeliveryUnknownError, ProviderTurnRejectedError } from "./providerErrors.js";
8
+ import { providerDeliveryFailureFrom } from "./agentError.js";
8
9
  /**
9
10
  * One Agent Client Protocol Session over an Agent's stdio.
10
11
  *
@@ -86,9 +87,24 @@ export class AcpStructuredProviderSession {
86
87
  this.mirror = mirror;
87
88
  }
88
89
  static async open(input) {
89
- const channel = new JsonLineChannel(input.child, input.mirror);
90
+ let receivingAttemptId;
91
+ const channel = new JsonLineChannel(input.child, input.mirror, error => {
92
+ // Before session/new has answered, the opening caller owns the failure.
93
+ if (!session.#sessionId)
94
+ return;
95
+ input.onDiagnostic?.({
96
+ nativeSessionId: session.#sessionId,
97
+ ...((receivingAttemptId ?? session.#activeAttemptId) === undefined
98
+ ? {} : { attemptId: receivingAttemptId ?? session.#activeAttemptId }),
99
+ failure: providerDeliveryFailureFrom(error, { phase: "turn-reconcile", inputDisposition: "unknown" })
100
+ });
101
+ });
90
102
  const session = new AcpStructuredProviderSession(input.child, input.exit, input.processInstanceId, channel, input.onTerminal, input.mirror);
91
- channel.onMessage((message) => session.#receive(message));
103
+ channel.onMessage((message) => {
104
+ receivingAttemptId = session.#activeAttemptId;
105
+ session.#receive(message);
106
+ receivingAttemptId = undefined;
107
+ });
92
108
  channel.onClose((error) => session.#fail(error));
93
109
  const negotiated = readAcpInitializeResult(await session.#request("initialize", acpInitializeRequest(input.clientVersion)));
94
110
  session.#negotiated = negotiated;
@@ -463,7 +479,7 @@ export class AcpStructuredProviderSession {
463
479
  if (message.id === undefined)
464
480
  this.#notification(method, message.params);
465
481
  else
466
- void this.#serve(method, message);
482
+ void this.#serve(method, message).catch(error => this.#fail(error));
467
483
  }
468
484
  #response(message) {
469
485
  const id = typeof message.id === "number" ? message.id : undefined;
@@ -638,8 +654,8 @@ export class AcpStructuredProviderSession {
638
654
  ...(error === undefined ? { result: result ?? {} } : { error })
639
655
  });
640
656
  }
641
- catch {
642
- // The pipe is gone; the closure path already reports that fact.
657
+ catch (error) {
658
+ this.#fail(error instanceof Error ? error : new Error(String(error)));
643
659
  }
644
660
  }
645
661
  #fail(error) {
@@ -40,7 +40,7 @@ function observation(input, mapped) {
40
40
  const eventId = hookEventId(input, mapped, requestOccurrenceId);
41
41
  const fence = { ...input.fence, ...mapped.fence };
42
42
  return createRuntimeObservation({
43
- schemaVersion: 4,
43
+ schemaVersion: 1,
44
44
  eventId,
45
45
  semanticKey: requestOccurrenceId === undefined
46
46
  ? runtimeObservationSemanticKey({
@@ -46,6 +46,7 @@ export function createAgentEndpointFactory(start = startStructuredProviderSessio
46
46
  onStarted: (value) => emit({ type: "started", value }),
47
47
  onTerminal: (value) => emit({ type: "terminal", value }),
48
48
  onInput: (value) => emit({ type: "input", value }),
49
+ onDiagnostic: (value) => emit({ type: "diagnostic", value }),
49
50
  onGoal: (value) => emit({ type: "goal", value })
50
51
  });
51
52
  endpoint = new BuiltinAgentEndpoint(opened.session, configuration, opened.recoveredTerminal);
@@ -193,7 +194,9 @@ class BuiltinAgentEndpoint {
193
194
  });
194
195
  }
195
196
  observe(value) {
196
- if (value.type !== "goal"
197
+ if (value.type === "diagnostic" && value.value.nativeSessionId !== this.nativeSessionId)
198
+ return;
199
+ if (value.type !== "goal" && value.type !== "diagnostic"
197
200
  && (value.value.nativeSessionId !== this.nativeSessionId || value.value.conversationId !== this.conversationId))
198
201
  return;
199
202
  if (value.type === "goal" && value.value !== null && value.value.conversationId !== this.conversationId)