@zq-silk/yui 0.6.0 → 0.6.2

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 (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
3
4
  import { createInterface } from "node:readline/promises";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { isDeepStrictEqual } from "node:util";
@@ -20,15 +21,29 @@ import { nativeAgentEnvironmentNames } from "./agent/launchEnvironment.js";
20
21
  import { runAgentCommand } from "./commands/agentCommands.js";
21
22
  import { runGlobalRoleCommand } from "./commands/globalRoleCommands.js";
22
23
  import { runConfigCommand } from "./commands/configCommands.js";
23
- import { parseControllerCleanupOptions, parseControllerStatusOptions, renderControllerResourceStatus, runInteractiveControllerCleanup } from "./commands/controllerCommands.js";
24
+ import { parseControllerCleanupOptions, parseControllerStatusOptions, parseControllerRuntimeSnapshot, renderControllerResourceStatus, renderRuntimeIdentitySection, summarizeDurablePhysicalMismatch, runInteractiveControllerCleanup } from "./commands/controllerCommands.js";
25
+ import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
26
+ import { parseSessionReconcileOptions, runSessionReconcileCommand } from "./commands/sessionCommands.js";
27
+ import { SessionOwnerReconciliation } from "./controller/sessionOwnerReconciliation.js";
24
28
  import { runJobCommand } from "./commands/jobCommands.js";
29
+ import { runDurableJobCommand } from "./commands/durableJobCommands.js";
30
+ import { runTelemetryCommand } from "./commands/telemetryCommands.js";
31
+ import { runResourcesCommand } from "./commands/resourcesCommands.js";
25
32
  import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
26
33
  import { runProjectCommand } from "./commands/projectCommands.js";
27
34
  import { runProfileCommand } from "./commands/profileCommands.js";
28
35
  import { dispatchPreparedReviewRound, failPendingReviewRound, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, validateTaskArchiveRequest } from "./commands/taskCommands.js";
29
36
  import { taskActor } from "./commands/taskActor.js";
30
37
  import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
38
+ import { runTaskChangeSetCommand } from "./commands/taskChangeSetCommands.js";
39
+ import { runTaskOverlapCommand } from "./commands/taskOverlapCommands.js";
40
+ import { createControllerIntegrationJobPort } from "./controller/jobClient.js";
31
41
  import { runTaskWorkspaceCommand } from "./commands/taskWorkspaceCommands.js";
42
+ import { runWorkflowCommandAsync } from "./commands/workflowCommands.js";
43
+ import { createUpdatePorts } from "./cli/updatePorts.js";
44
+ import { createReleaseWorkflowPorts } from "./release/releaseWorkflowPorts.js";
45
+ import { readRuntimeIdentity } from "./release/runtimeRelease.js";
46
+ import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
32
47
  import { reconcileTaskRemoteBaselines } from "./commands/taskCompletionGate.js";
33
48
  import { FileCompletionManager, resolveCliIdentity } from "./completion/fileCompletionManager.js";
34
49
  import { assertFileTaskControllerStorageCompatible, ensureFileTaskController, FileTaskWorkflowRuntime, refreshRunningFileTaskControllerConfiguration, refreshRunningFileTaskControllerEnvironment, restartFileTaskController, stopFileTaskController } from "./controller/clientRuntime.js";
@@ -37,6 +52,7 @@ import { FileSchedulerStoreAdapter } from "./controller/fileSchedulerStoreAdapte
37
52
  import { cleanControllerResource } from "./controller/resourceCleanupLinux.js";
38
53
  import { scanControllerResourceInventory } from "./controller/resourceInventoryLinux.js";
39
54
  import { runSessionNotifyCommand } from "./controller/sessionNotify.js";
55
+ import { openSchedulerTelemetry } from "./telemetry/telemetryWiring.js";
40
56
  import { runClaudeLifecycleHookCommand } from "./controller/claudeLifecycleHook.js";
41
57
  import { runCodexLifecycleHookCommand } from "./controller/codexLifecycleHook.js";
42
58
  import { buildDoctorReport, renderDoctor, runDoctorCommand } from "./doctor/doctor.js";
@@ -45,6 +61,7 @@ import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
45
61
  import { TaskWorkspaceCoordinator, WorkspaceCleanupBlockedError } from "./repository/taskWorkspaceCoordinator.js";
46
62
  import { FileTaskWorkspacePreparer, ReviewRoundWorkspaceEvidenceError } from "./repository/taskWorkspacePreparer.js";
47
63
  import { inspectStorageSchema } from "./storage/storageSchema.js";
64
+ import { collectRuntimeBuildIdentity, collectStorageIdentity, countDroppedInboxEvents, createProductionRuntimeIdentityPorts, evaluateStorageHealth, resolveStatusIdentityEnabled } from "./observability/runtimeIdentity.js";
48
65
  import { resolveYuiHome } from "./storage/taskStore.js";
49
66
  import { openCompatibleFileTaskStore, validateCompatibleFileTaskStore } from "./storage/compatibleTaskStore.js";
50
67
  import { resolveTaskRecordReference } from "./task/taskRecordReference.js";
@@ -82,7 +99,7 @@ void main().catch((error) => {
82
99
  process.exitCode = 5;
83
100
  });
84
101
  export async function main() {
85
- const taskFinalReviewContract = await preflightManagedTaskControlPlane();
102
+ const { contract: taskFinalReviewContract, verifiedStore } = await preflightManagedTaskControlPlane();
86
103
  if (args.length === 0) {
87
104
  emit(renderCommandHelp((await import("./cli/commandCatalog.js")).ROOT_COMMAND, VERSION));
88
105
  return;
@@ -113,6 +130,37 @@ export async function main() {
113
130
  return;
114
131
  }
115
132
  const home = resolveYuiHome(process.env);
133
+ if (args[0] === "release") {
134
+ const subcommand = args[1];
135
+ if (subcommand === "install" && args.length === 3) {
136
+ const result = runReleaseInstall(home, args[2]);
137
+ emit(renderReleaseInstallResult(result), false, result);
138
+ if (result.outcome === "aborted")
139
+ process.exitCode = 5;
140
+ return;
141
+ }
142
+ if (subcommand === "list" && args.length === 2) {
143
+ const result = runReleaseList(home);
144
+ emit(renderReleaseList(result), false, result);
145
+ return;
146
+ }
147
+ if (subcommand === "activate" && (args.length === 2 || args.length === 3)) {
148
+ const releaseId = args.length === 3
149
+ ? args[2]
150
+ : runReleaseList(home).active ?? undefined;
151
+ if (releaseId === undefined) {
152
+ throw usageError("Release activate usage: yui release activate <release-id> "
153
+ + "(or activate the active release when exactly one is installed).");
154
+ }
155
+ const result = await runReleaseActivate(home, releaseId);
156
+ emit(renderReleaseActivateResult(result), false, result);
157
+ if (result.outcome === "aborted" || result.outcome === "dual-owner") {
158
+ process.exitCode = 5;
159
+ }
160
+ return;
161
+ }
162
+ throw usageError("Release usage: yui release install <source-dir> | list | activate [release-id].");
163
+ }
116
164
  if (args[0] === "completion") {
117
165
  await completionCommand(home, invocation.node);
118
166
  return;
@@ -154,6 +202,17 @@ export async function main() {
154
202
  emit(renderDoctor(report.checks, report.review));
155
203
  return;
156
204
  }
205
+ if (args[0] === "execution") {
206
+ // Issue 11 read-only audit: opens the Home read-only, never writes state,
207
+ // never wakes a Leader.
208
+ if (args[1] !== "audit") {
209
+ throw usageError("Execution usage: yui execution audit [--task <id>] [--since <iso>] [--until <iso>].");
210
+ }
211
+ const options = parseExecutionAuditOptions(args.slice(2));
212
+ const result = runExecutionAuditCommand(home, options);
213
+ emit(result.output, false, result.report);
214
+ return;
215
+ }
157
216
  if (args[0] === "upgrade") {
158
217
  // Mirror doctor/controller: needs a Home but self-manages the schema check,
159
218
  // because upgrade must run against a non-current Home.
@@ -179,12 +238,50 @@ export async function main() {
179
238
  }
180
239
  throw usageError("Internal lifecycle callback usage is invalid.");
181
240
  }
241
+ if (args[0] === "session") {
242
+ if (args[1] !== "reconcile") {
243
+ throw usageError("Session usage: yui session reconcile [--report] [--cleanup].");
244
+ }
245
+ const options = parseSessionReconcileOptions(args.slice(2));
246
+ const store = openCompatibleFileTaskStore(home);
247
+ const tmux = new TmuxManager(process.env.YUI_TMUX_BIN ?? "tmux", new NodeCommandExecutor(), { yuiHome: home });
248
+ const reconciliation = new SessionOwnerReconciliation({
249
+ home,
250
+ store,
251
+ environment: process.env,
252
+ tmux
253
+ });
254
+ const result = await runSessionReconcileCommand({
255
+ reconciliation,
256
+ options,
257
+ environment: process.env
258
+ });
259
+ process.exitCode = result.exitCode;
260
+ emit(result.output, false, result.data);
261
+ return;
262
+ }
182
263
  if (args[0] === "controller") {
183
264
  const method = args[1];
184
265
  if (method === "identity" && args.length === 2) {
185
- // Internal lifecycle seam used by update/upgrade. The Controller socket
186
- // authenticates this exact launch identity; public `controller status`
187
- // intentionally redacts argv in its resource inventory.
266
+ // Issue 02: the stable, read-only runtime identity receipt. It survives
267
+ // a Controller stop and answers build ID, package digest, backend, and
268
+ // worker state without a socket round-trip. When no receipt exists yet
269
+ // (a Controller that predates this feature), fall back to the
270
+ // authenticated socket identity so the command stays useful during
271
+ // rollout step 1.
272
+ let receipt = null;
273
+ try {
274
+ receipt = readRuntimeIdentity(home);
275
+ }
276
+ catch {
277
+ // A corrupt or stale receipt (for example one written by an older
278
+ // Controller that predates the launch-identity fields) falls back to
279
+ // the live socket identity, which always carries the exact argv.
280
+ }
281
+ if (receipt !== null) {
282
+ emit("", false, receipt);
283
+ return;
284
+ }
188
285
  try {
189
286
  const identity = await callController(home, "controller.identity", {});
190
287
  emit("", false, identity);
@@ -218,6 +315,39 @@ export async function main() {
218
315
  scope: options.scope,
219
316
  environment: process.env
220
317
  });
318
+ if (resolveStatusIdentityEnabled(process.env)) {
319
+ // Issue 11 read-only identity/metrics section. Every fact is observed;
320
+ // missing producers render `unsupported` and storage contradictions
321
+ // fail closed with exit code 5.
322
+ const cliEntry = fileURLToPath(import.meta.url);
323
+ const packageRoot = resolve(cliEntry, "..", "..");
324
+ const build = collectRuntimeBuildIdentity(createProductionRuntimeIdentityPorts(packageRoot, cliEntry, process.env));
325
+ const storage = collectStorageIdentity(home);
326
+ const droppedEvents = countDroppedInboxEvents(home);
327
+ let runtime;
328
+ try {
329
+ const result = await callController(home, "controller.status", {}, { timeoutMs: 2_000 });
330
+ runtime = parseControllerRuntimeSnapshot(result, droppedEvents);
331
+ }
332
+ catch {
333
+ runtime = { source: "unsupported", droppedEvents };
334
+ }
335
+ const mismatch = summarizeDurablePhysicalMismatch(snapshot);
336
+ const identitySection = renderRuntimeIdentitySection({
337
+ build,
338
+ storage,
339
+ runtime,
340
+ mismatch,
341
+ inventoryRssBytes: snapshot.summary.rssBytes
342
+ });
343
+ emit(`${renderControllerResourceStatus(snapshot, options.verbose)}\n\n${identitySection}`, false, { ...snapshot, identity: { build, storage, runtime, mismatch } });
344
+ // Exit 5 only on hard contradictions (fail). A needs-repair state
345
+ // (pseudo-layout-7) is degraded but still readable via the file store,
346
+ // so it exits 0 with a DEGRADED health line and a precise repair action.
347
+ if (evaluateStorageHealth(storage).status === "fail")
348
+ process.exitCode = 5;
349
+ return;
350
+ }
221
351
  emit(renderControllerResourceStatus(snapshot, options.verbose), false, snapshot);
222
352
  return;
223
353
  }
@@ -270,8 +400,22 @@ export async function main() {
270
400
  emit(renderControllerResult(controllerMethod, result), false, controllerMethod === "restart" ? result : undefined);
271
401
  return;
272
402
  }
403
+ if (args[0] === "resources") {
404
+ await assertFileTaskControllerStorageCompatible(home);
405
+ const resourcesStore = openCompatibleFileTaskStore(home);
406
+ const result = await runResourcesCommand(args.slice(1), resourcesStore);
407
+ emit(result.output, false, result.data);
408
+ return;
409
+ }
273
410
  await assertFileTaskControllerStorageCompatible(home);
274
- const store = openCompatibleFileTaskStore(home);
411
+ // Reuse the store the exact runtime preflight already opened and read for
412
+ // this same Home. Opening a second store would parse the unchanged large
413
+ // state a second time; the per-instance fingerprint cache still invalidates
414
+ // on an external writer, and the storage lock + revision CAS are unchanged.
415
+ const store = verifiedStore !== undefined
416
+ && resolve(verifiedStore.rootDirectory()) === resolve(home)
417
+ ? verifiedStore
418
+ : openCompatibleFileTaskStore(home);
275
419
  const catalogs = new AgentConfigurationCatalogService(home, {
276
420
  environment: process.env
277
421
  });
@@ -287,7 +431,7 @@ export async function main() {
287
431
  terminalInput: process.stdin,
288
432
  onWarning: (message) => process.stderr.write(`Warning: ${message}\n`)
289
433
  });
290
- const schedulerStore = new FileSchedulerStoreAdapter(store);
434
+ const schedulerStore = new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home, process.env));
291
435
  const planner = new FileRoleLaunchPlanner(home, store, { environment: process.env });
292
436
  const workspacePreparer = new FileTaskWorkspacePreparer(home, store);
293
437
  const runtime = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
@@ -443,7 +587,37 @@ export async function main() {
443
587
  }
444
588
  if (resolved[0] === "task") {
445
589
  if (resolved[1] === "integration") {
446
- const result = await runTaskIntegrationCommand(resolved.slice(2), store, home, { environment: process.env });
590
+ const result = await runTaskIntegrationCommand(resolved.slice(2), store, home, {
591
+ environment: process.env,
592
+ jobPort: createControllerIntegrationJobPort(home, { environment: process.env, store })
593
+ });
594
+ emit(result.output, false, result.data);
595
+ return;
596
+ }
597
+ if (resolved[1] === "change-set") {
598
+ const result = await runTaskChangeSetCommand(resolved.slice(2), store);
599
+ emit(result.output, false, result.data);
600
+ return;
601
+ }
602
+ if (resolved[1] === "overlap") {
603
+ const result = await runTaskOverlapCommand(resolved.slice(2), store);
604
+ emit(result.output, false, result.data);
605
+ return;
606
+ }
607
+ if (resolved[1] === "workflow"
608
+ && (resolved[2] === "run" || resolved[2] === "resume")) {
609
+ const result = await runWorkflowCommandAsync(resolved.slice(2), store, {
610
+ environment: process.env,
611
+ yuiHome: home,
612
+ ports: createReleaseWorkflowPorts({
613
+ home,
614
+ updatePorts: createUpdatePorts(process.env),
615
+ projectStore: store
616
+ })
617
+ });
618
+ if (result.kind !== "output") {
619
+ throw new Error(`Task workflow ${resolved[2]} returned an invalid control result.`);
620
+ }
447
621
  emit(result.output, false, result.data);
448
622
  return;
449
623
  }
@@ -669,7 +843,12 @@ export async function main() {
669
843
  // transaction once its exact Lane ids exist.
670
844
  }
671
845
  }
672
- let executionLaneWorkspaces = await prepareExecutionLaneWorkspacesForCommand(resolved, store, workspacePreparer, process.env);
846
+ let executionLaneWorkspaces;
847
+ // Held only for a new Group's dispatch: the per-Project maintenance fence
848
+ // spans Lane preparation and the adoption transaction, and projectPaths is
849
+ // the under-fence snapshot the adoption CAS revalidates.
850
+ let laneDispatchRelease;
851
+ let laneDispatchProjectPaths;
673
852
  let workItemIntegrationProof;
674
853
  if (resolved[1] === "work" && resolved[2] === "accept") {
675
854
  const workItemId = resolved[3];
@@ -695,12 +874,18 @@ export async function main() {
695
874
  : { taskFinalReviewContract })
696
875
  });
697
876
  if (!completion.completed && !completion.activeTaskReview) {
698
- await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env });
877
+ await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env, jobPort: createControllerIntegrationJobPort(home, { environment: process.env, store }) });
699
878
  }
700
879
  }
701
880
  let candidateMaterialization;
702
881
  let candidateMaterializationCommitted = false;
703
882
  try {
883
+ const preparedLanes = await prepareExecutionLaneWorkspacesForCommand(resolved, store, workspacePreparer, process.env);
884
+ if (preparedLanes !== undefined) {
885
+ executionLaneWorkspaces = preparedLanes.workspaces;
886
+ laneDispatchRelease = preparedLanes.release;
887
+ laneDispatchProjectPaths = preparedLanes.projectPaths;
888
+ }
704
889
  candidateMaterialization = await candidateMaterializationForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
705
890
  const candidateGitSnapshot = candidateMaterialization === undefined
706
891
  ? await candidateSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract)
@@ -726,6 +911,7 @@ export async function main() {
726
911
  ? {}
727
912
  : { candidateWorkspace: candidateMaterialization.workspace ?? null }),
728
913
  ...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
914
+ ...(laneDispatchProjectPaths === undefined ? {} : { laneDispatchProjectPaths }),
729
915
  ...(directTaskMainSnapshot === undefined ? {} : { directTaskMainSnapshot }),
730
916
  ...(actualTaskReviewCandidate === undefined
731
917
  ? {}
@@ -734,6 +920,13 @@ export async function main() {
734
920
  ...(laneSnapshotPreflight === undefined ? {} : { executionLaneGitSnapshot: laneSnapshotPreflight }),
735
921
  ...(taskRetirementProof === undefined ? {} : { taskRetirementProof })
736
922
  });
923
+ // The dispatch transaction has now adopted (or rejected) the prepared
924
+ // Lane workspaces. Release the held fence so later output/review
925
+ // handling can take the per-Project fence itself.
926
+ if (laneDispatchRelease !== undefined) {
927
+ laneDispatchRelease();
928
+ laneDispatchRelease = undefined;
929
+ }
737
930
  // The command transaction has now durably submitted the Candidate. Any
738
931
  // later output/review handling must not roll back its Git snapshot.
739
932
  candidateMaterializationCommitted = candidateMaterialization !== undefined;
@@ -867,6 +1060,10 @@ export async function main() {
867
1060
  if (!candidateMaterializationCommitted) {
868
1061
  await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
869
1062
  }
1063
+ if (laneDispatchRelease !== undefined) {
1064
+ laneDispatchRelease();
1065
+ laneDispatchRelease = undefined;
1066
+ }
870
1067
  throw error;
871
1068
  }
872
1069
  }
@@ -874,6 +1071,24 @@ export async function main() {
874
1071
  emit(runJobCommand(resolved.slice(1), store, { runtime }));
875
1072
  return;
876
1073
  }
1074
+ if (resolved[0] === "job") {
1075
+ emit(await runDurableJobCommand(resolved.slice(1), {
1076
+ home,
1077
+ json: jsonOutput,
1078
+ environment: process.env,
1079
+ store
1080
+ }));
1081
+ return;
1082
+ }
1083
+ if (resolved[0] === "telemetry") {
1084
+ emit(await runTelemetryCommand(resolved.slice(1), {
1085
+ home,
1086
+ json: jsonOutput,
1087
+ environment: process.env,
1088
+ store
1089
+ }));
1090
+ return;
1091
+ }
877
1092
  throw usageError(`Command is not connected to the restored FileTaskStore framework yet: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
878
1093
  }
879
1094
  async function preflightManagedTaskControlPlane() {
@@ -896,7 +1111,7 @@ async function preflightManagedTaskControlPlane() {
896
1111
  if (taskFinalReviewInvocation.request !== undefined) {
897
1112
  throw new Error("Task final-review contract requires a verified exact Task control-plane invocation.");
898
1113
  }
899
- return undefined;
1114
+ return { contract: undefined, verifiedStore: undefined };
900
1115
  }
901
1116
  if (process.env.YUI_SESSION_SCOPE !== "task") {
902
1117
  throw new Error("Exact Task control-plane invocation requires a managed Task runtime.");
@@ -926,23 +1141,27 @@ async function preflightManagedTaskControlPlane() {
926
1141
  const preallocatedClaudeCallback = args.length === 2
927
1142
  && args[0] === "internal"
928
1143
  && args[1] === "claude-hook";
929
- assertExactTaskRuntimeState(runtime, openCompatibleFileTaskStore(control.yuiHome), preallocatedClaudeCallback
1144
+ const verifiedStore = openCompatibleFileTaskStore(control.yuiHome);
1145
+ assertExactTaskRuntimeState(runtime, verifiedStore, preallocatedClaudeCallback
930
1146
  ? { preallocatedNativeSessionReservation: { yuiHome: control.yuiHome } }
931
1147
  : {});
932
1148
  const request = taskFinalReviewInvocation.request;
933
1149
  if (request === undefined)
934
- return undefined;
1150
+ return { contract: undefined, verifiedStore };
935
1151
  if (runtime.roleName !== "leader") {
936
1152
  throw new Error("Only the exact Task Leader invocation may establish a final-review contract.");
937
1153
  }
938
1154
  if (request.taskId !== runtime.taskId) {
939
1155
  throw new Error(`Task final-review contract Task id mismatch: expected ${runtime.taskId}, found ${request.taskId}.`);
940
1156
  }
941
- return createTaskFinalReviewContract({
942
- taskId: runtime.taskId,
943
- reviewerRoleName: request.reviewerRoleName,
944
- controlPlaneDigest: digest
945
- });
1157
+ return {
1158
+ contract: createTaskFinalReviewContract({
1159
+ taskId: runtime.taskId,
1160
+ reviewerRoleName: request.reviewerRoleName,
1161
+ controlPlaneDigest: digest
1162
+ }),
1163
+ verifiedStore
1164
+ };
946
1165
  }
947
1166
  function cleanupCliError(error, fallbackResource) {
948
1167
  if (error instanceof WorkspaceCleanupBlockedError) {
@@ -1145,20 +1364,45 @@ async function prepareExecutionLaneWorkspacesForCommand(args, store, preparer, e
1145
1364
  return undefined;
1146
1365
  const groupId = group?.id ?? `execution-group-${store.peekNextAgentRunId(item.taskId)}`;
1147
1366
  const laneIds = plan.laneIds;
1367
+ // A new Group's Lanes are not yet durable, so their worktrees would be
1368
+ // unadopted between preparation and the dispatch transaction. Hold ONE
1369
+ // per-Project maintenance fence across both, so a project migrate cannot
1370
+ // switch the catalog in that gap and strand a Lane on the external
1371
+ // checkout. An existing Group's Lanes are adopted inside their own fence,
1372
+ // so no outer fence is held.
1373
+ const held = group === undefined
1374
+ ? preparer.acquireTaskProjectMaintenanceLocks(item.taskId)
1375
+ : undefined;
1148
1376
  const map = new Map();
1149
1377
  try {
1378
+ let projectPaths;
1379
+ if (held !== undefined) {
1380
+ const paths = new Map();
1381
+ for (const { projectId } of held.current.projectBindings) {
1382
+ const project = store.getProject(projectId);
1383
+ if (project === null)
1384
+ throw new Error(`Project not found: ${projectId}.`);
1385
+ paths.set(projectId, project.path);
1386
+ }
1387
+ projectPaths = paths;
1388
+ }
1150
1389
  for (const laneId of laneIds.filter((value) => value.length > 0)) {
1151
1390
  map.set(laneId, await preparer.prepareExecutionLaneWorkspace(item.taskId, groupId, laneId, {
1152
1391
  purpose: "execution",
1153
1392
  workItemId: item.id
1154
- }));
1393
+ }, held === undefined ? undefined : { current: held.current }));
1155
1394
  }
1395
+ return { workspaces: map, release: held?.release, projectPaths };
1156
1396
  }
1157
1397
  catch (error) {
1398
+ // Compensate (discard unadopted Lane worktrees) BEFORE releasing the
1399
+ // fence: a concurrent project migrate must not switch the catalog while
1400
+ // external-backed worktrees are still identifiable for removal.
1158
1401
  await preparer.discardUnadoptedExecutionLaneWorkspaces(map);
1402
+ if (held !== undefined)
1403
+ held.release();
1159
1404
  throw error;
1160
1405
  }
1161
- return map;
1162
1406
  }
1163
1407
  async function prepareReviewLaneWorkspaces(taskId, reviewRoundId, store, preparer) {
1164
1408
  const round = store.getReviewRound(taskId, reviewRoundId);
@@ -1,14 +1,19 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
- import { DEFAULT_RECONCILIATION_INTERVAL_SECONDS, reconciliationIntervalMilliseconds } from "../config/yuiConfig.js";
2
+ import { DEFAULT_RECONCILIATION_INTERVAL_SECONDS, reconciliationIntervalMilliseconds, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode } from "../config/yuiConfig.js";
3
3
  import { resolveTimeZone } from "../output/timePresentation.js";
4
- import { REVIEW_TRIGGERS } from "../review/reviewConfig.js";
4
+ import { REVIEW_FINDING_LEDGER_MODES, REVIEW_TRIGGERS } from "../review/reviewConfig.js";
5
+ import { DEFAULT_LEADER_NEXT_ACTION_MODE, LEADER_NEXT_ACTION_MODES, resolveLeaderNextActionMode } from "../config/yuiConfig.js";
5
6
  const CONFIG_SET_USAGE = "Config set usage: yui config set "
6
7
  + "<--time-zone <IANA timezone> | "
7
- + "--reconciliation-interval-seconds <seconds>>.";
8
+ + "--reconciliation-interval-seconds <seconds> | "
9
+ + "--resources-gc-mode <report|quarantine> | "
10
+ + "--resources-gc-auto-quarantine <true|false>>.";
8
11
  export function runConfigCommand(args, store) {
9
12
  const [command, ...rest] = args;
10
13
  if (command === "review")
11
14
  return runReviewConfigCommand(rest, store);
15
+ if (command === "leader-next-action")
16
+ return runLeaderNextActionConfigCommand(rest, store);
12
17
  if (command === "show") {
13
18
  if (rest.length !== 0)
14
19
  throw usageError("Config show usage: yui config show.");
@@ -19,6 +24,9 @@ export function runConfigCommand(args, store) {
19
24
  return [
20
25
  `Time zone: ${resolveTimeZone(config.timeZone)}`,
21
26
  `Reconciliation interval: ${reconciliationIntervalSeconds} seconds`,
27
+ `Leader next-action mode: ${resolveLeaderNextActionMode(config.leaderNextActionMode)}`,
28
+ `Resources GC mode: ${resolveResourcesGcMode(config.resourcesGcMode)}`,
29
+ `Resources GC auto-quarantine: ${resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine) ? "on" : "off"}`,
22
30
  ""
23
31
  ].join("\n");
24
32
  }
@@ -41,12 +49,65 @@ export function runConfigCommand(args, store) {
41
49
  });
42
50
  return `Reconciliation interval set to ${reconciliationIntervalSeconds} seconds\n`;
43
51
  }
52
+ if (rest[0] === "--resources-gc-mode") {
53
+ const resourcesGcMode = validatedConfigValue(() => resolveResourcesGcMode(rest[1]));
54
+ store.transaction((tx) => {
55
+ tx.saveConfig({ ...tx.getConfig(), resourcesGcMode });
56
+ });
57
+ return `Resources GC mode set to ${resourcesGcMode}\n`;
58
+ }
59
+ if (rest[0] === "--resources-gc-auto-quarantine") {
60
+ const resourcesGcAutoQuarantine = validatedConfigValue(() => resolveResourcesGcAutoQuarantine(rest[1] === "true" ? true : rest[1] === "false" ? false : rest[1]));
61
+ store.transaction((tx) => {
62
+ tx.saveConfig({ ...tx.getConfig(), resourcesGcAutoQuarantine });
63
+ });
64
+ return `Resources GC auto-quarantine set to ${resourcesGcAutoQuarantine ? "on" : "off"}\n`;
65
+ }
44
66
  throw configSetUsageError();
45
67
  }
46
68
  throw usageError(command === undefined
47
69
  ? "Config command is required."
48
70
  : `Unknown command: config ${command}`);
49
71
  }
72
+ function runLeaderNextActionConfigCommand(args, store) {
73
+ const [command, ...rest] = args;
74
+ if (command === "show") {
75
+ if (rest.length !== 0) {
76
+ throw usageError("Config leader-next-action show usage: yui config leader-next-action show.");
77
+ }
78
+ return `Leader next-action mode: ${resolveLeaderNextActionMode(store.getConfig().leaderNextActionMode)}\n`;
79
+ }
80
+ if (command === "set") {
81
+ const usage = "Config leader-next-action set usage: "
82
+ + `yui config leader-next-action set <${LEADER_NEXT_ACTION_MODES.join("|")}>.`;
83
+ if (rest.length !== 1)
84
+ throw usageError(usage);
85
+ let mode;
86
+ try {
87
+ mode = resolveLeaderNextActionMode(rest[0]);
88
+ }
89
+ catch (error) {
90
+ throw usageError(error instanceof Error ? error.message : String(error), usage);
91
+ }
92
+ store.transaction((tx) => {
93
+ tx.saveConfig({ ...tx.getConfig(), leaderNextActionMode: mode });
94
+ });
95
+ return `Leader next-action mode set to ${mode}\n`;
96
+ }
97
+ if (command === "clear") {
98
+ if (rest.length !== 0) {
99
+ throw usageError("Config leader-next-action clear usage: yui config leader-next-action clear.");
100
+ }
101
+ store.transaction((tx) => {
102
+ const { leaderNextActionMode: _mode, ...config } = tx.getConfig();
103
+ tx.saveConfig(config);
104
+ });
105
+ return `Leader next-action mode reset to ${DEFAULT_LEADER_NEXT_ACTION_MODE}\n`;
106
+ }
107
+ throw usageError(command === undefined
108
+ ? "Config leader-next-action command is required."
109
+ : `Unknown command: config leader-next-action ${command}`);
110
+ }
50
111
  function runReviewConfigCommand(args, store) {
51
112
  const [command, ...rest] = args;
52
113
  if (command === "show") {
@@ -55,7 +116,7 @@ function runReviewConfigCommand(args, store) {
55
116
  const review = store.getConfig().review;
56
117
  return review === undefined
57
118
  ? "Review: disabled\n"
58
- : `Review: ${review.roleName} (${review.trigger})\n`;
119
+ : `Review: ${review.roleName} (${review.trigger}; finding ledger: ${review.findingLedger ?? "shadow"})\n`;
59
120
  }
60
121
  if (command === "clear") {
61
122
  if (rest.length !== 0)
@@ -68,14 +129,15 @@ function runReviewConfigCommand(args, store) {
68
129
  }
69
130
  if (command === "set") {
70
131
  const usage = "Config review set usage: "
71
- + "yui config review set --role <global-role> --trigger <always|leader|final>.";
72
- if (rest.length !== 4)
132
+ + "yui config review set --role <global-role> --trigger <always|leader|final> "
133
+ + "[--finding-ledger <shadow|enforce>].";
134
+ if (rest.length !== 4 && rest.length !== 6)
73
135
  throw usageError(usage);
74
136
  const options = new Map();
75
137
  for (let index = 0; index < rest.length; index += 2) {
76
138
  const name = rest[index];
77
139
  const value = rest[index + 1];
78
- if (!["--role", "--trigger"].includes(name)
140
+ if (!["--role", "--trigger", "--finding-ledger"].includes(name)
79
141
  || value === undefined
80
142
  || options.has(name)) {
81
143
  throw usageError(usage);
@@ -93,13 +155,25 @@ function runReviewConfigCommand(args, store) {
93
155
  throw usageError(`Global Role not found: ${roleName}.`);
94
156
  }
95
157
  const trigger = rawTrigger;
158
+ const rawLedgerMode = options.get("--finding-ledger")?.trim();
159
+ let findingLedger;
160
+ if (rawLedgerMode !== undefined) {
161
+ if (!REVIEW_FINDING_LEDGER_MODES.includes(rawLedgerMode)) {
162
+ throw usageError(usage);
163
+ }
164
+ findingLedger = rawLedgerMode;
165
+ }
96
166
  store.transaction((tx) => {
97
167
  tx.saveConfig({
98
168
  ...tx.getConfig(),
99
- review: { roleName, trigger }
169
+ review: {
170
+ roleName,
171
+ trigger,
172
+ ...(findingLedger === undefined ? {} : { findingLedger })
173
+ }
100
174
  });
101
175
  });
102
- return `Review set to ${roleName} (${trigger})\n`;
176
+ return `Review set to ${roleName} (${trigger}; finding ledger: ${findingLedger ?? "shadow"})\n`;
103
177
  }
104
178
  throw usageError(command === undefined
105
179
  ? "Config review command is required."