@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/README.md CHANGED
@@ -703,13 +703,13 @@ hiding the resources that remain. Use `--all` to include discovered Yui homes.
703
703
 
704
704
  `controller restart` replaces the Controller process and its scheduler/socket services with the currently installed Yui version. It does not stop or restart managed tmux/Agent sessions.
705
705
 
706
- Its recovery reconciliation runs every 120 seconds by default. Normal durable state changes enqueue a Task, Role, or Operator key and return immediately; keys received in the same fixed 100 ms window trigger one non-overlapping targeted pass. Operator presentation has an independent lane, so a blocked Task workspace operation cannot delay a user question. Periodic Git/worktree work is limited to Tasks with durable Task-mailbox work, while active Role liveness uses one tmux inventory. A Codex turn-complete Hook writes directly to storage without starting or waiting for the Controller, then gives a legal yield/input/completion two seconds to win before closing a forgotten Run. Durable mailboxes freeze the current batch while new signals merge into the next batch; failures release the current batch for recovery. Recommended InputRequest and pending Turn deadlines share one nearest-deadline selector and therefore do not wait for the recovery interval. Explicit `task reconcile` still requests an immediate recovery pass. The retained loop is:
706
+ Its recovery reconciliation runs every 120 seconds by default. Normal durable state changes enqueue a Task, Role, or Operator key and return immediately; keys received in the same fixed 100 ms window trigger one non-overlapping targeted pass. Operator presentation has an independent lane, so a blocked Task workspace operation cannot delay a user question. Periodic Git/worktree work is limited to Tasks with durable Task-mailbox work, while active Role liveness uses one tmux inventory. A Codex turn-complete Hook writes directly to storage without starting or waiting for the Controller, then gives a legal yield/input/completion two seconds to win before closing a forgotten Run. Durable mailboxes freeze the current batch while new signals merge into the next batch. Task-orchestration failures retain the exact Controller-owned processing batch for two bounded fast retries and later periodic recovery; a successful retry completes that batch before newer pending work is claimed. Recommended InputRequest and pending Turn deadlines share one nearest-deadline selector and therefore do not wait for the recovery interval. Explicit `task reconcile` still requests an immediate recovery pass. The retained loop is:
707
707
 
708
- 1. prepare active Project Task main worktrees;
709
- 2. stop archived Task tmux sessions and clean only clean worktrees;
708
+ 1. dispatch pending Leader wakes whose Task workspaces are already ready;
709
+ 2. prepare active Project Task main worktrees with durable orchestration work;
710
710
  3. deliver queued Worker Runs;
711
- 4. detect exited active Role processes;
712
- 5. dispatch pending Leader wakes when the Leader is idle.
711
+ 4. resolve due Turn completions and reconcile Role liveness;
712
+ 5. dispatch Leader work created or unblocked by the later recovery phases.
713
713
 
714
714
  Automated input is sent only through tmux. Each pass performs one non-blocking process-state readiness check; a busy startup is retried through a small bounded mailbox timer, while later busy sessions are normally woken by Codex turn-complete events. A pane-local receipt prevents the same Run from being typed twice after a Controller retry.
715
715
 
@@ -30,5 +30,6 @@ export const YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES = Object.freeze([
30
30
  "YUI_CONTEXT_PROJECT_IDS",
31
31
  "YUI_WORKSPACE_PROJECTS",
32
32
  "YUI_LEADER_ACTION_RUN_ID",
33
- "YUI_LEADER_ACTION_RECEIPT_ID"
33
+ "YUI_LEADER_ACTION_RECEIPT_ID",
34
+ "YUI_JOB_CALLER_KEY"
34
35
  ]);
@@ -219,6 +219,12 @@ const taskChildren = [
219
219
  summary: "Show consolidated working context for a Task.",
220
220
  usage: "yui task context <task>"
221
221
  },
222
+ {
223
+ name: "next-action",
224
+ summary: "Project the durable Task records into one protocol-level next action.",
225
+ usage: "yui task next-action <task> [--json]",
226
+ options: ["--json"]
227
+ },
222
228
  {
223
229
  name: "archive",
224
230
  summary: "Archive a Task after confirming the main worktree outcome.",
@@ -308,6 +314,75 @@ const taskChildren = [
308
314
  }
309
315
  ]
310
316
  },
317
+ {
318
+ name: "grant",
319
+ summary: "Manage capability grants for a Task. Issue and revoke require the authenticated global Operator session.",
320
+ sections: [{ id: "manage", title: "Commands", entries: ["issue", "show", "list", "revoke"] }],
321
+ children: [
322
+ {
323
+ name: "issue",
324
+ summary: "Issue a capability grant to a Task. Requires the authenticated global Operator session; the granter is bound to it.",
325
+ usage: "yui task grant issue <task> --action <name> (repeatable) [--scope-project <id>...] [--scope-repo <owner/name>...] [--scope-package <name>...] [--scope-home <path>] [--param <name=v1,v2>...] [--expires-at <iso-8601>] [--max-uses <int>] [--irreversibility-ceiling <none|reversible|irreversible>]",
326
+ options: ["--action", "--scope-project", "--scope-repo", "--scope-package", "--scope-home", "--param", "--expires-at", "--max-uses", "--irreversibility-ceiling"]
327
+ },
328
+ {
329
+ name: "show",
330
+ summary: "Show one capability grant.",
331
+ usage: "yui task grant show <task> <grant-id>"
332
+ },
333
+ {
334
+ name: "list",
335
+ summary: "List capability grants for a Task.",
336
+ usage: "yui task grant list <task>"
337
+ },
338
+ {
339
+ name: "revoke",
340
+ summary: "Revoke a capability grant. Requires the authenticated global Operator session; the revoker is bound to it.",
341
+ usage: "yui task grant revoke <task> <grant-id>",
342
+ options: []
343
+ }
344
+ ]
345
+ },
346
+ {
347
+ name: "workflow",
348
+ summary: "Manage release workflows for a Task.",
349
+ sections: [{ id: "manage", title: "Commands", entries: ["create", "show", "list", "run", "resume", "status"] }],
350
+ children: [
351
+ {
352
+ name: "create",
353
+ summary: "Create a release workflow for a Task.",
354
+ usage: "yui task workflow create <task> --grant <grant-id> --source-repo <owner/name> --source-commit <sha> [--source-artifact <name@integrity>] --step <id>:<kind> (repeatable) [--step-irreversibility <id>=<level> (repeatable)] [--step-param <id>:<key>=<value> (repeatable)]",
355
+ options: ["--grant", "--source-repo", "--source-commit", "--source-artifact", "--step", "--step-irreversibility", "--step-param"]
356
+ },
357
+ {
358
+ name: "show",
359
+ summary: "Show one release workflow.",
360
+ usage: "yui task workflow show <task> <workflow-id>"
361
+ },
362
+ {
363
+ name: "list",
364
+ summary: "List release workflows for a Task.",
365
+ usage: "yui task workflow list <task>"
366
+ },
367
+ {
368
+ name: "run",
369
+ summary: "Run a release workflow from its resume cursor.",
370
+ usage: "yui task workflow run <task> <workflow-id> [--grant <grant-id>] [--max-steps <int>]",
371
+ options: ["--grant", "--max-steps"]
372
+ },
373
+ {
374
+ name: "resume",
375
+ summary: "Resume a release workflow from its first unconfirmed step.",
376
+ usage: "yui task workflow resume <task> <workflow-id> [--grant <grant-id>] [--max-steps <int>]",
377
+ options: ["--grant", "--max-steps"]
378
+ },
379
+ {
380
+ name: "status",
381
+ summary: "Show a release workflow and its step states.",
382
+ usage: "yui task workflow status <task> <workflow-id>"
383
+ }
384
+ ]
385
+ },
311
386
  {
312
387
  name: "role",
313
388
  summary: "Manage Roles within a Task.",
@@ -508,7 +583,7 @@ const taskChildren = [
508
583
  {
509
584
  name: "review",
510
585
  summary: "Control Task-final ReviewRounds.",
511
- sections: [{ id: "manage", title: "Commands", entries: ["request", "group", "retry"] }],
586
+ sections: [{ id: "manage", title: "Commands", entries: ["request", "group", "retry", "finding"] }],
512
587
  children: [
513
588
  {
514
589
  name: "request",
@@ -533,13 +608,46 @@ const taskChildren = [
533
608
  name: "retry",
534
609
  summary: "Retry a failed Task-final ReviewRound without a Reviewer Run.",
535
610
  usage: "yui task review retry <task>/<review-round>"
611
+ },
612
+ {
613
+ name: "finding",
614
+ summary: "Inspect and disposition the cross-Round Review finding ledger.",
615
+ executable: true,
616
+ sections: [{ id: "manage", title: "Commands", entries: ["list", "dispose", "repair-wave", "extract"] }],
617
+ children: [
618
+ {
619
+ name: "list",
620
+ summary: "List the Task's review findings with disposition and repair lineage.",
621
+ usage: "yui task review finding list <task>"
622
+ },
623
+ {
624
+ name: "dispose",
625
+ summary: "Record one Leader disposition for a review finding.",
626
+ usage: "yui task review finding dispose <task>/<finding> --disposition <fixed-pending-review|verified-fixed|accepted-risk|not-actionable|superseded> [--work-item <id>] [--commit <sha>] [--verification <text>] [--note <text>] [--superseded-by <stable-key>]",
627
+ options: ["--disposition", "--work-item", "--commit", "--verification", "--note", "--superseded-by"],
628
+ optionValues: {
629
+ "--disposition": ["fixed-pending-review", "verified-fixed", "accepted-risk", "not-actionable", "superseded"]
630
+ }
631
+ },
632
+ {
633
+ name: "repair-wave",
634
+ summary: "Group open P1/P2 findings into parallel repair groups by overlap.",
635
+ usage: "yui task review finding repair-wave <task> [--create]",
636
+ options: ["--create"]
637
+ },
638
+ {
639
+ name: "extract",
640
+ summary: "Reconcile findings from one completed ReviewRound into the ledger.",
641
+ usage: "yui task review finding extract <task>/<review-round>"
642
+ }
643
+ ]
536
644
  }
537
645
  ]
538
646
  },
539
647
  {
540
648
  name: "integration",
541
649
  summary: "Safely integrate ChangeSets with Leader-owned conflict decisions.",
542
- sections: [{ id: "manage", title: "Commands", entries: ["start", "continue", "resolve", "abort", "list", "show", "cleanup"] }],
650
+ sections: [{ id: "manage", title: "Commands", entries: ["start", "continue", "resolve", "abort", "supersede", "list", "show", "cleanup", "queue"] }],
543
651
  children: [
544
652
  {
545
653
  name: "start",
@@ -565,9 +673,29 @@ const taskChildren = [
565
673
  usage: "yui task integration abort <task>/<integration> --reason <text>",
566
674
  options: ["--reason"]
567
675
  },
676
+ {
677
+ name: "supersede",
678
+ summary: "Mark a committed Integration as obsolete, retaining its evidence.",
679
+ usage: "yui task integration supersede <task>/<integration> --reason <text>",
680
+ options: ["--reason"]
681
+ },
568
682
  { name: "list", summary: "List Integration Attempts.", usage: "yui task integration list <task>" },
569
683
  { name: "show", summary: "Show one Integration Attempt.", usage: "yui task integration show <task>/<integration>" },
570
- { name: "cleanup", summary: "Remove a terminal Integration worktree and branch.", usage: "yui task integration cleanup <task>/<integration>" }
684
+ { name: "cleanup", summary: "Remove a terminal Integration worktree and branch.", usage: "yui task integration cleanup <task>/<integration>" },
685
+ {
686
+ name: "queue",
687
+ summary: "Manage the serialized integration queue.",
688
+ sections: [{ id: "queue", title: "Commands", entries: ["enqueue", "list", "show", "process", "supersede", "requeue", "reconcile"] }],
689
+ children: [
690
+ { name: "enqueue", summary: "Enqueue a ChangeSet for serialized integration.", usage: "yui task integration queue enqueue <task> --project <project> --change-set <id> [--target <ref>] [--check <command> ...]", options: ["--project", "--change-set", "--target", "--check"] },
691
+ { name: "list", summary: "List integration queue entries.", usage: "yui task integration queue list <task> [--project <project>]", options: ["--project"] },
692
+ { name: "show", summary: "Show one integration queue entry.", usage: "yui task integration queue show <task>/<entry>" },
693
+ { name: "process", summary: "Process queued integration entries.", usage: "yui task integration queue process <task> [--project <project>] [--limit <n>]", options: ["--project", "--limit"] },
694
+ { name: "supersede", summary: "Supersede a queued entry.", usage: "yui task integration queue supersede <task>/<entry> --reason <text>", options: ["--reason"] },
695
+ { name: "requeue", summary: "Requeue a conflicted entry.", usage: "yui task integration queue requeue <task>/<entry>" },
696
+ { name: "reconcile", summary: "Reconcile a blocked entry.", usage: "yui task integration queue reconcile <task>/<entry>" }
697
+ ]
698
+ }
571
699
  ]
572
700
  },
573
701
  {
@@ -635,6 +763,20 @@ const taskChildren = [
635
763
  { name: "show", summary: "Show one Task event.", usage: "yui task event show <task> <event>" }
636
764
  ]
637
765
  },
766
+ {
767
+ name: "overlap",
768
+ summary: "Show read-only cross-Task overlap diagnostics.",
769
+ usage: "yui task overlap [--project <project>] [--base <sha>] [--task <task> ...]",
770
+ options: ["--project", "--base", "--task"]
771
+ },
772
+ {
773
+ name: "change-set",
774
+ summary: "Inspect ChangeSets captured from WorkItem Candidates.",
775
+ sections: [{ id: "inspect", title: "Commands", entries: ["show"] }],
776
+ children: [
777
+ { name: "show", summary: "Show one ChangeSet.", usage: "yui task change-set show <task>/<change-set>" }
778
+ ]
779
+ },
638
780
  { name: "enter", summary: "Enter a Task Role, defaulting to Leader.", usage: "yui task enter <task> [role]" }
639
781
  ];
640
782
  export const ROOT_COMMAND = buildNode({
@@ -647,7 +789,8 @@ export const ROOT_COMMAND = buildNode({
647
789
  ] },
648
790
  { id: "workflow", title: "Workflow", entries: ["operator", "project", "task"] },
649
791
  { id: "configuration", title: "Configuration", entries: ["config", "agent", "profile", "role"] },
650
- { id: "operations", title: "Operations", entries: ["web", "controller", "jobs"] },
792
+ { id: "operations", title: "Operations", entries: ["web", "controller", "execution", "job", "jobs", "telemetry", "release"] },
793
+ { id: "resources", title: "Resources", entries: ["resources"] },
651
794
  { id: "internal", title: "Internal", entries: ["internal"] }
652
795
  ],
653
796
  children: [
@@ -713,24 +856,80 @@ export const ROOT_COMMAND = buildNode({
713
856
  },
714
857
  {
715
858
  name: "identity",
716
- summary: "Read the authenticated Controller launch identity.",
859
+ summary: "Read the stable runtime identity receipt (build, backend, worker).",
717
860
  hidden: true
718
861
  },
719
862
  { name: "stop", summary: "Stop the Controller." },
720
863
  { name: "restart", summary: "Restart internal services without stopping tmux sessions." }
721
864
  ]
722
865
  },
866
+ {
867
+ name: "execution",
868
+ summary: "Read-only execution history audit.",
869
+ sections: [{
870
+ id: "reports",
871
+ title: "Commands",
872
+ entries: ["audit"]
873
+ }],
874
+ children: [
875
+ {
876
+ name: "audit",
877
+ summary: "Report Runs, wakes, Sessions, Reviews, Integrations, and telemetry volume.",
878
+ usage: "yui execution audit [--task <id>] [--since <iso>] [--until <iso>]",
879
+ options: ["--task", "--since", "--until"]
880
+ }
881
+ ]
882
+ },
883
+ {
884
+ name: "release",
885
+ summary: "Install and activate immutable local runtime releases.",
886
+ sections: [{
887
+ id: "commands",
888
+ title: "Commands",
889
+ entries: ["install", "list", "activate"]
890
+ }],
891
+ children: [
892
+ {
893
+ name: "install",
894
+ summary: "Install a runtime package as an immutable release.",
895
+ usage: "yui release install <source-dir>"
896
+ },
897
+ { name: "list", summary: "List installed releases and the active pointer." },
898
+ {
899
+ name: "activate",
900
+ summary: "Activate a release via atomic Controller handover.",
901
+ usage: "yui release activate [release-id]"
902
+ }
903
+ ]
904
+ },
905
+ {
906
+ name: "resources",
907
+ summary: "Inspect and garbage-collect managed worktrees, deployments, and runtime artifacts.",
908
+ sections: [{ id: "gc", title: "Commands", entries: ["gc"] }],
909
+ children: [
910
+ {
911
+ name: "gc",
912
+ summary: "Plan or apply resource garbage collection.",
913
+ usage: "yui resources gc [--dry-run|--apply|--purge|--restore] [--quarantine-ttl-hours <hours>]",
914
+ options: ["--dry-run", "--apply", "--purge", "--restore", "--quarantine-ttl-hours"]
915
+ }
916
+ ]
917
+ },
723
918
  {
724
919
  name: "config",
725
920
  summary: "Inspect or update Yui configuration.",
726
- sections: [{ id: "manage", title: "Commands", entries: ["show", "set", "review"] }],
921
+ sections: [{ id: "manage", title: "Commands", entries: ["show", "set", "review", "leader-next-action"] }],
727
922
  children: [
728
923
  { name: "show", summary: "Show effective Yui configuration." },
729
924
  {
730
925
  name: "set",
731
926
  summary: "Update Yui configuration.",
732
- usage: "yui config set <--time-zone <IANA timezone> | --reconciliation-interval-seconds <5-300>>",
733
- options: ["--time-zone", "--reconciliation-interval-seconds"]
927
+ usage: "yui config set <--time-zone <IANA timezone> | --reconciliation-interval-seconds <5-300> | --resources-gc-mode <report|quarantine> | --resources-gc-auto-quarantine <true|false>>",
928
+ options: ["--time-zone", "--reconciliation-interval-seconds", "--resources-gc-mode", "--resources-gc-auto-quarantine"],
929
+ optionValues: {
930
+ "--resources-gc-mode": ["report", "quarantine"],
931
+ "--resources-gc-auto-quarantine": ["true", "false"]
932
+ }
734
933
  },
735
934
  {
736
935
  name: "review",
@@ -741,12 +940,29 @@ export const ROOT_COMMAND = buildNode({
741
940
  {
742
941
  name: "set",
743
942
  summary: "Enable review with a Global Role.",
744
- usage: "yui config review set --role <global-role> --trigger <always|leader|final>",
745
- options: ["--role", "--trigger"],
746
- optionValues: { "--trigger": ["always", "leader", "final"] }
943
+ usage: "yui config review set --role <global-role> --trigger <always|leader|final> [--finding-ledger <shadow|enforce>]",
944
+ options: ["--role", "--trigger", "--finding-ledger"],
945
+ optionValues: {
946
+ "--trigger": ["always", "leader", "final"],
947
+ "--finding-ledger": ["shadow", "enforce"]
948
+ }
747
949
  },
748
950
  { name: "clear", summary: "Disable global review." }
749
951
  ]
952
+ },
953
+ {
954
+ name: "leader-next-action",
955
+ summary: "Configure the Leader next-action/duplicate-guard mode.",
956
+ sections: [{ id: "manage", title: "Commands", entries: ["show", "set", "clear"] }],
957
+ children: [
958
+ { name: "show", summary: "Show the Leader next-action mode." },
959
+ {
960
+ name: "set",
961
+ summary: "Set the Leader next-action mode (display|warn|enforce).",
962
+ usage: "yui config leader-next-action set <display|warn|enforce>"
963
+ },
964
+ { name: "clear", summary: "Reset to the default display mode." }
965
+ ]
750
966
  }
751
967
  ]
752
968
  },
@@ -896,12 +1112,23 @@ export const ROOT_COMMAND = buildNode({
896
1112
  name: "task",
897
1113
  summary: "Manage Tasks, WorkItems, Agent Runs, and integration.",
898
1114
  sections: [
899
- { id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "update", "activate", "complete", "reopen", "retire", "list", "show", "context", "archive", "rebuild", "history", "replace", "reconcile"] },
900
- { id: "collaboration", title: "Collaboration", entries: ["message", "input", "work", "run", "review", "integration", "role", "enter"] },
1115
+ { id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "update", "activate", "complete", "reopen", "retire", "list", "show", "context", "next-action", "archive", "rebuild", "history", "replace", "reconcile"] },
1116
+ { id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "work", "run", "review", "integration", "role", "enter", "overlap", "change-set"] },
901
1117
  { id: "knowledge", title: "Task Knowledge", entries: ["brief", "decision", "milestone", "event"] }
902
1118
  ],
903
1119
  children: taskChildren
904
1120
  },
1121
+ {
1122
+ name: "job",
1123
+ summary: "Start, inspect, cancel, or acknowledge a Controller-managed DurableJob.",
1124
+ sections: [{ id: "manage", title: "Commands", entries: ["start", "get", "cancel", "acknowledge"] }],
1125
+ children: [
1126
+ { name: "start", summary: "Start a DurableJob for build, test, package, or Integration checks.", usage: "yui job start --task <id> --project <project> --head <sha> --workspace <dir> --step <name>=<command> [--step ...] [--owner task|work-item:<id>|integration-attempt:<id>] [--env <k=v>...]" },
1127
+ { name: "get", summary: "Show a DurableJob record and its terminal result.", usage: "yui job get --task <id> --job <job-id>" },
1128
+ { name: "cancel", summary: "Request cancellation of a running or queued DurableJob.", usage: "yui job cancel --task <id> --job <job-id>" },
1129
+ { name: "acknowledge", summary: "Acknowledge an unknown-needs-attention DurableJob so Task lifecycle gates can proceed.", usage: "yui job acknowledge --task <id> --job <job-id>" }
1130
+ ]
1131
+ },
905
1132
  {
906
1133
  name: "jobs",
907
1134
  summary: "Inspect scheduler wake and recovery records.",
@@ -911,6 +1138,17 @@ export const ROOT_COMMAND = buildNode({
911
1138
  { name: "retry", summary: "Retry a failed Leader recovery.", usage: "yui jobs retry <id>" }
912
1139
  ]
913
1140
  },
1141
+ {
1142
+ name: "telemetry",
1143
+ summary: "Inspect and compact the bounded provider-progress sidecar.",
1144
+ sections: [{ id: "manage", title: "Commands", entries: ["status", "prune", "compact", "read"] }],
1145
+ children: [
1146
+ { name: "status", summary: "Show sidecar health, row counts, and retention settings.", usage: "yui telemetry status" },
1147
+ { name: "prune", summary: "Apply terminal retention and active-Run caps.", usage: "yui telemetry prune [--task <id>] [--keep <n>] [--dry-run]" },
1148
+ { name: "compact", summary: "Fold legacy semantic progress events into a staged Home's sidecar.", usage: "yui telemetry compact --from <home> --staged <dir> [--keep <n>] [--dry-run]" },
1149
+ { name: "read", summary: "Page through retained progress rows or read a Run aggregate.", usage: "yui telemetry read --task <id> [--run <id>] [--aggregate] [--limit <n>] [--offset <n>]" }
1150
+ ]
1151
+ },
914
1152
  {
915
1153
  name: "internal",
916
1154
  summary: "Internal Yui callbacks.",
@@ -241,6 +241,14 @@ function activateAndVerify(ports, staged, home, storageBackupPath, lifecycle, pa
241
241
  : postSwitchRecoveryAction(home, storageBackupPath),
242
242
  recoverable: false,
243
243
  version: staged.version,
244
+ // P1-1 (rr22): ANY failure to start or authenticate the replacement
245
+ // Controller leaves the lifecycle handoff unresolved — not just
246
+ // UPDATE_CONTROLLER_UNKNOWN_ACTIVE. Even when the old identity is
247
+ // restored below, the running Controller is not the replacement the
248
+ // step promised, so the adapter must persist the step as unknown
249
+ // rather than letting a controller-home query confirm it on binary
250
+ // health alone.
251
+ controllerOwnershipUnknown: true,
244
252
  ...(storageBackupPath === undefined ? {} : { storageBackupPath })
245
253
  };
246
254
  // An ownership-unknown mismatch is a live-process blocker, not a
@@ -30,30 +30,89 @@
30
30
  * can resolve the true state.
31
31
  */
32
32
  import { spawnSync } from "node:child_process";
33
- import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
33
+ import { accessSync, constants, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
34
34
  import { tmpdir } from "node:os";
35
- import { dirname, isAbsolute, join, resolve } from "node:path";
35
+ import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
36
36
  import { fileURLToPath } from "node:url";
37
37
  import { runtimeError } from "../errors/cliError.js";
38
+ import { isConcreteVersion } from "../domain/validation.js";
38
39
  import { STORAGE_DOCTOR_CHECK_NAMES } from "../doctor/doctor.js";
39
40
  import { inspectStorageSchema } from "../storage/storageSchema.js";
40
41
  import { correlateUpgradeReceipt } from "../storage/upgrade/upgradeOrchestrator.js";
41
42
  import { readSwitchProgress } from "../storage/upgrade/switchProgress.js";
42
43
  const PACKAGE_NAME = "@zq-silk/yui";
43
44
  const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
45
+ function resolveExecutable(command, environmentPath) {
46
+ if (isAbsolute(command))
47
+ return command;
48
+ for (const directory of (environmentPath ?? "").split(delimiter)) {
49
+ if (directory.length === 0)
50
+ continue;
51
+ const candidate = resolve(directory, command);
52
+ try {
53
+ accessSync(candidate, constants.X_OK);
54
+ return candidate;
55
+ }
56
+ catch {
57
+ // Keep walking PATH; callers fail closed when no executable is found.
58
+ }
59
+ }
60
+ return undefined;
61
+ }
62
+ function failedSpawnResult(message) {
63
+ return {
64
+ pid: undefined,
65
+ output: [null, Buffer.alloc(0), Buffer.from(message)],
66
+ stdout: Buffer.alloc(0),
67
+ stderr: Buffer.from(message),
68
+ status: 127,
69
+ signal: null,
70
+ error: new Error(message)
71
+ };
72
+ }
44
73
  /** Build the real ports. `spawn` is injectable so tests avoid real installs. */
45
74
  export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot = tmpdir()) {
75
+ // The production adapter must not let a later PATH change select a
76
+ // different npm during staging, activation, or recovery. Test doubles are
77
+ // intentionally left untouched so deterministic tests can dispatch on the
78
+ // stable command name `npm`.
79
+ const trustedNpm = spawn === spawnSync
80
+ ? resolveExecutable("npm", environment.PATH)
81
+ : "npm";
82
+ const run = (command, args, options) => {
83
+ if (command !== "npm")
84
+ return spawn(command, args, options);
85
+ if (trustedNpm === undefined) {
86
+ return failedSpawnResult("Unable to resolve trusted executable: npm");
87
+ }
88
+ return spawn(trustedNpm, args, options);
89
+ };
46
90
  // Keep the exact verified artifact in memory for this update attempt. This is
47
91
  // deliberately not persisted as a retry or recovery protocol.
48
92
  let verifiedActivatedBinary;
49
93
  let verifiedActivatedVersion;
50
- const stopReplacementController = (home, pid) => (stopReplacementControllerForUpdate(home, pid, environment, spawn));
94
+ const stopReplacementController = (home, pid) => (stopReplacementControllerForUpdate(home, pid, environment, run));
51
95
  return {
52
- stage() {
96
+ stage(version) {
97
+ // A caller that names a version (the release workflow, which freezes the
98
+ // exact version in its plan) installs THAT version — never a moving
99
+ // `latest` that could resolve to a different build than the one the
100
+ // plan authorized. A non-concrete value fails closed rather than being
101
+ // interpolated into an install spec. An omitted version keeps the
102
+ // interactive `yui update` behavior of staging latest.
103
+ const spec = version === undefined
104
+ ? PACKAGE_SPEC
105
+ : isConcreteVersion(version)
106
+ ? `${PACKAGE_NAME}@${version.trim()}`
107
+ : null;
108
+ if (spec === null) {
109
+ throw runtimeError(`Refusing to stage a non-concrete version (${String(version)}): only an exact `
110
+ + "major.minor.patch version can be pinned for an update.");
111
+ }
53
112
  const stagingPath = mkdtempSync(join(stagingRoot, "yui-update-stage-"));
54
113
  let ownsStaging = true;
55
114
  try {
56
- const result = spawn("npm", ["install", "--global", "--prefix", stagingPath, PACKAGE_SPEC], { cwd: process.cwd(), env: environment, shell: false, stdio: "inherit" });
115
+ const result = run("npm", ["install", "--global", "--prefix", stagingPath, spec], { cwd: process.cwd(), env: environment, shell: false, stdio: "inherit" });
57
116
  assertSpawnOk(result, "stage the new package");
58
117
  const binaryPath = join(stagingPath, "bin", "yui");
59
118
  // Resolve the EXACT version that was staged (from the staged install's own
@@ -61,7 +120,7 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
61
120
  // version, FAIL the stage (R2-F1): we must never fall back to a bare
62
121
  // `@latest`, which would let activation promote — and verify wave through —
63
122
  // a different build than the one that passed preflight.
64
- const version = resolveStagedVersion(stagingPath, binaryPath, environment, spawn);
123
+ const version = resolveStagedVersion(stagingPath, binaryPath, environment, run);
65
124
  if (version === null) {
66
125
  throw runtimeError("Failed to resolve the exact staged package version (neither the staged package.json "
67
126
  + "nor `yui --json version` returned a concrete version). Refusing to proceed with a "
@@ -79,11 +138,11 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
79
138
  }
80
139
  },
81
140
  preflight(staged, home) {
82
- const result = spawn(staged.binaryPath, ["--json", "upgrade", "--update-preflight"], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
141
+ const result = run(staged.binaryPath, ["--json", "upgrade", "--update-preflight"], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
83
142
  return interpretPreflight(result);
84
143
  },
85
144
  activateStorage(staged, home) {
86
- const result = spawn(staged.binaryPath, ["--json", "upgrade"], {
145
+ const result = run(staged.binaryPath, ["--json", "upgrade"], {
87
146
  cwd: process.cwd(),
88
147
  // The parent update process captures/stops/drains the old Controller
89
148
  // before invoking the staged child. Mark this internal call so the
@@ -105,12 +164,12 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
105
164
  // `@latest` (R2-F1: `stage` guarantees `staged.version` is a concrete
106
165
  // version, so there is no `latest` sentinel to fall back to).
107
166
  const spec = `${PACKAGE_NAME}@${staged.version}`;
108
- const result = spawn("npm", ["install", "--global", spec], { cwd: process.cwd(), env: environment, shell: false, stdio: "inherit" });
167
+ const result = run("npm", ["install", "--global", spec], { cwd: process.cwd(), env: environment, shell: false, stdio: "inherit" });
109
168
  assertSpawnOk(result, "activate the new binary");
110
169
  },
111
170
  verify(staged, home) {
112
171
  // Verify the ACTUALLY-ACTIVATED global binary, not the staging path (P1-3).
113
- const activeBinary = resolveGlobalBinary(environment, spawn);
172
+ const activeBinary = resolveGlobalBinary(environment, run);
114
173
  if (activeBinary === null || !existsSync(activeBinary)) {
115
174
  throw runtimeError("Post-update health check failed: could not locate the activated global `yui` binary.");
116
175
  }
@@ -122,7 +181,7 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
122
181
  // "exited with status 5". We therefore parse+validate the structured storage
123
182
  // health first: only a valid success envelope with every expected storage
124
183
  // check present-and-ok, no blocking checks, AND exit 0 is healthy.
125
- const doctor = spawn(activeBinary, ["--json", "doctor"], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
184
+ const doctor = run(activeBinary, ["--json", "doctor"], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
126
185
  assertDoctorStorageHealthy(doctor);
127
186
  // 2) Confirm the activated binary's identity matches the staged artifact.
128
187
  // `staged.version` is always a concrete version (R2-F1), so we REQUIRE the
@@ -624,8 +683,12 @@ function assertActivatedControllerIdentity(identity, activatedBinary, activatedV
624
683
  + "runtime/entrypoint; refusing readiness.");
625
684
  }
626
685
  }
627
- /** Resolve the Controller entrypoint beside the activated package's CLI. */
628
- function activatedControllerEntrypoint(activatedBinary) {
686
+ /**
687
+ * Resolve the Controller entrypoint beside the activated package's CLI.
688
+ * Exported so the release workflow's recovery query can apply the exact same
689
+ * entrypoint derivation as the production startup identity check (P1-1, rr23).
690
+ */
691
+ export function activatedControllerEntrypoint(activatedBinary) {
629
692
  let resolvedBinary;
630
693
  try {
631
694
  resolvedBinary = realpathSync(activatedBinary);
@@ -934,15 +997,6 @@ function resolveStagedVersion(stagingPath, binaryPath, environment, spawn) {
934
997
  // with a successful, concrete version.
935
998
  return resolveBinaryVersion(binaryPath, environment, spawn);
936
999
  }
937
- /**
938
- * True for a concrete, pinnable package version — a semver-shaped `X.Y.Z` with an
939
- * optional pre-release/build suffix. Rejects dist-tag sentinels (`latest`, `next`,
940
- * …), empty/whitespace, and anything not anchored to a numeric `major.minor.patch`
941
- * so a sentinel can never be spliced into an activation spec (R3-F1).
942
- */
943
- function isConcreteVersion(value) {
944
- return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(value.trim());
945
- }
946
1000
  /** Read a CONCRETE `version` from a package.json, or `null` when absent/non-concrete. */
947
1001
  function readVersionFromPackageJson(path) {
948
1002
  try {