@zq-silk/yui 0.8.1 → 0.8.3

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 (52) hide show
  1. package/ARCHITECTURE.md +27 -28
  2. package/README.md +57 -46
  3. package/dist/cli/commandCatalog.js +57 -17
  4. package/dist/cli/interactionPolicy.js +4 -10
  5. package/dist/cli/invocationRouter.js +2 -1
  6. package/dist/cli.js +106 -27
  7. package/dist/commands/taskCommands.js +458 -77
  8. package/dist/commands/taskCompletionGate.js +152 -0
  9. package/dist/context/runContextPack.js +19 -4
  10. package/dist/context/sessionBootstrapManifest.js +1 -1
  11. package/dist/controller/fileSchedulerStoreAdapter.js +389 -70
  12. package/dist/controller/resourceInventory.js +9 -5
  13. package/dist/controller/runtime.js +80 -7
  14. package/dist/controller/runtimeLaunchCoordinator.js +18 -78
  15. package/dist/controller/structuredProviderObservation.js +273 -0
  16. package/dist/executor/agentAdapter.js +40 -0
  17. package/dist/executor/agentExecutor.js +31 -7
  18. package/dist/executor/executorRegistry.js +11 -49
  19. package/dist/executor/fileRoleLaunchPlanner.js +115 -37
  20. package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
  21. package/dist/repository/gitWorkspace.js +7 -4
  22. package/dist/repository/taskBaseFreshness.js +4 -2
  23. package/dist/run/agentRun.js +4 -4
  24. package/dist/runtime/agentHost.js +767 -158
  25. package/dist/runtime/builtinAgentDrivers.js +1 -5
  26. package/dist/runtime/codexAppServerRuntime.js +67 -60
  27. package/dist/runtime/exactControlPlane.js +7 -2
  28. package/dist/runtime/index.js +6 -2
  29. package/dist/runtime/launchBroker.js +30 -8
  30. package/dist/runtime/providerAuthorityFence.js +24 -0
  31. package/dist/runtime/providerControl.js +63 -0
  32. package/dist/runtime/providerRecoveryDecision.js +55 -0
  33. package/dist/runtime/providerRuntimeIdentity.js +269 -19
  34. package/dist/runtime/runtimeBinding.js +20 -11
  35. package/dist/runtime/structuredProviderHost.js +476 -0
  36. package/dist/runtime/tmuxAdapters.js +143 -42
  37. package/dist/scheduler/activeRoleRunDelivery.js +206 -120
  38. package/dist/scheduler/leaderWakeupProcessor.js +141 -16
  39. package/dist/scheduler/wakeReason.js +1 -0
  40. package/dist/storage/migration/productionRegistry.js +111 -0
  41. package/dist/storage/sqliteStore.js +2 -0
  42. package/dist/storage/taskStore.js +3 -1
  43. package/dist/task/completionReadiness.js +43 -0
  44. package/dist/task/nextAction.js +6 -4
  45. package/dist/task/publicationReference.js +1 -0
  46. package/dist/tmux/tmuxManager.js +1 -1
  47. package/dist/workItem/workItem.js +12 -0
  48. package/dist/workspace/workItemChangeSetManager.js +2 -1
  49. package/i18n/README.zh-CN.md +11 -8
  50. package/package.json +1 -1
  51. package/skills/yui-leader/SKILL.md +8 -3
  52. package/skills/yui-runtime/SKILL.md +7 -2
package/ARCHITECTURE.md CHANGED
@@ -201,34 +201,33 @@ context.
201
201
 
202
202
  ## Runtime ownership
203
203
 
204
- tmux owns Agent process lifetimes and observable output. The Controller owns
205
- mailbox delivery, wakeups, Role liveness, and reconciliation. Task attachment
206
- surfaces only attach to an existing pane and cannot create, resume, wake, or
207
- deliver to a managed runtime. Global interactive entry remains an explicit
208
- session-lifecycle operation.
209
-
210
- Task observation is read-only by default. Explicit write access publishes a
211
- Role-scoped tmux lease before revalidating durable Run state. The managed host
212
- also checks that lease before planning or process creation, so either the Run
213
- claim or the writer lease wins and they never share a pane. Writer contention
214
- is transient backpressure rather than a delivery failure: it does not consume
215
- bounded delivery retries, and lease release signals only existing durable work.
216
- Global interactive entry uses the same mechanism at tmux-host scope and
217
- automatically falls back to read-only when another writer already exists.
218
-
219
- Managed Task Claude execution is process-per-Run: the exact Run input is a
220
- stream-json stdin frame submitted at process launch, while native session IDs
221
- carry conversation continuity across processes. The lifecycle binding records
222
- the exact Run submitted at launch. A Controller restart may recover only the
223
- same reserved launch/Run as uncertain until its Provider Hook arrives; a newly
224
- reserved Run cannot reuse an older live Role pane, so its provisional launch
225
- is released, the old owner is fenced through the durable cleanup lane, and the
226
- same Run is retried only after cleanup; pending cleanup prevents a successor
227
- generation from starting early.
228
- Terminal key injection is therefore an interactive compatibility mechanism,
229
- not a managed Claude delivery protocol. Operator and Leader Sessions remain
230
- fixed Task/global Roles; Task Worker Sessions are selected through Role Agent
231
- bindings.
204
+ tmux owns the persistent Agent Host lifetime and observable output. The
205
+ Controller owns mailbox delivery, wakeups, Role liveness, recovery decisions,
206
+ and a durable single-writer authority epoch. The Agent Host owns one structured
207
+ Provider child process and mirrors its output to the pane. Managed prompts are
208
+ never terminal bytes: both Controller and human takeover input become fenced
209
+ Provider-native Turn requests.
210
+
211
+ Run, Conversation, Activation, and Turn identities are independent. A
212
+ Conversation can span Runs and Provider processes; one Activation identifies
213
+ one live process; one Turn identifies one pre-recorded input attempt. A live
214
+ Activation is retained when a later Run reuses the Conversation. Process exit
215
+ ends that Activation, and a resumed process receives a new Activation and a
216
+ higher authority epoch.
217
+
218
+ Task observation uses `task role view`. Explicit takeover requires a live
219
+ managed Run, atomically transfers authority to a human holder, synchronizes the
220
+ same fence to the Host, and only then exposes the PTY input gateway. Detach
221
+ releases authority; `task role release` is an idempotent repair path even after
222
+ the Run has ended. Global interactive entry remains a native session-lifecycle
223
+ operation outside this managed Provider contract.
224
+
225
+ Codex uses a persistent App Server JSON-RPC transport. Claude uses a persistent
226
+ stream-json transport with exact user-message replay acknowledgement. In both
227
+ cases, Yui records Turn intent before writing, accepts only exact Provider
228
+ evidence, and maps an uncertain write to `delivery-unknown` without automatic
229
+ resubmission. Conversation replacement requires exact missing evidence and no
230
+ unsettled input, Turn, Activation, or writer authority.
232
231
 
233
232
  Role desired revisions and Run/Session effective snapshots keep configuration
234
233
  history explicit. Resume compares the complete effective snapshot and
package/README.md CHANGED
@@ -659,55 +659,68 @@ yui task complete <task-id> --summary-file delivery.txt --refresh-remote
659
659
  yui task reopen <task-id>
660
660
  ```
661
661
 
662
- Completed Tasks reject messages, dispatch, enter, retry, and late yields until explicitly reopened, while retaining Task main for inspection or integration. Every isolated WorkItem worktree must be explicitly cleaned as integrated or abandoned before archive; that cleanup also removes its managed branch. Archive requires `--integrated` or `--abandon` to state the Task main outcome and is allowed only after Task main is clean. It removes managed worktrees but retains Task and WorkItem records. The Task main branch is retained as a recovery artifact instead of being silently deleted.
662
+ When a verified squash-merge Publication records a remote commit that is
663
+ ancestry-divergent from the unchanged, frozen Task-final Review head, a user or
664
+ global Operator may explicitly authorize completion against its identical Git
665
+ tree:
666
+
667
+ ```sh
668
+ yui task complete <task-id> --summary-file delivery.txt \
669
+ --accept-published-tree <publication-id>
670
+ ```
671
+
672
+ This does not weaken normal freshness checks. Yui requires the current,
673
+ unsuperseded Publication to be merged and verified, its local commit to equal
674
+ the reviewed physical Task head, its remote commit to be ancestry-divergent,
675
+ and both commits to resolve to the same exact tree. `--refresh-remote` fetches
676
+ the remote object graph before resolving that Publication commit. For a Task
677
+ governed by a durable exact final-review contract, the user/Operator command
678
+ persists the exact authorization tuple and wakes the Task Leader; only that
679
+ contract-capable Leader may consume it and complete the Task. Tasks without
680
+ that contract retain the one-step explicit completion path. The Task event
681
+ audit records the authorization and, on completion, the accepted Project,
682
+ Publication, ReviewRound, both commits, and tree.
683
+
684
+ Completed Tasks reject messages, dispatch, Provider authority changes, retry, and late yields until explicitly reopened, while retaining Task main for inspection or integration. Every isolated WorkItem worktree must be explicitly cleaned as integrated or abandoned before archive; that cleanup also removes its managed branch. Archive requires `--integrated` or `--abandon` to state the Task main outcome and is allowed only after Task main is clean. It removes managed worktrees but retains Task and WorkItem records. The Task main branch is retained as a recovery artifact instead of being silently deleted.
663
685
  Task lifecycle completion/selection only suggests valid source states: Draft for activate, active for complete, and completed for reopen.
664
686
 
665
687
  ## Sessions and tmux
666
688
 
667
- tmux owns Agent process lifetimes and their observable output. Global Operator
668
- and global Role sessions remain native interactive CLIs. A managed Task Claude
669
- Run instead starts one finite Claude process with `--print`, stream-json input
670
- and stream-json output. Yui writes the exact Run prompt as one newline-delimited
671
- JSON user frame on stdin, drains output concurrently, and carries native
672
- continuity with Claude's session ID. Startup and delivery therefore never
673
- depend on a TUI composer, readiness glyph, paste delay, or a synthetic Enter
674
- key. Codex keeps its adapter-native launch-prompt and structured callback path.
675
-
676
- `task enter` and `task role enter` are pure attachments to an existing Task
677
- Role pane. They do not start the Controller, prepare a workspace, create or
678
- resume an Agent, wake a Role, or deliver input. Task attachments default to
679
- `--read-only`; `--read-write` is explicit and is rejected while that Role owns
680
- an active managed Run, a managed Claude process is still exiting, or another
681
- writer owns the same pane. A read-write attach first publishes a Role-scoped
682
- tmux writer lease and then revalidates durable Run state, closing the race with
683
- Controller launch. While the lease exists, managed delivery for that Role is
684
- paused without consuming its bounded delivery retries; detach releases the
685
- lease and signals only already-durable Role work for reconsideration. Other
686
- Roles in the same Task continue independently. Before any attach Yui closes
687
- readline, leaves raw mode, pauses its stdin, and synchronously hands the terminal
688
- to tmux. The attach uses the real outer terminal capabilities and a clean alternate
689
- screen; mouse scrolling stays in the Agent pane's
690
- 100,000-line tmux history instead of mixing with earlier shell or IDE terminal
691
- history. A read-write attachment exposes whatever native interaction the
692
- existing pane supports, but it is never part of managed startup or delivery.
693
-
694
- tmux fixes a pane's history capacity when that pane is created. Roles created
695
- before this limit was configured keep their earlier capacity; Yui warns on
696
- Terminal attach and in Web so the user can exit and re-enter that Role once to
697
- create a 100,000-line pane while retaining the native Agent conversation.
698
-
699
- Global interactive entry remains writable when no writer exists and
700
- automatically downgrades to read-only when another writer is present; global
701
- Web keeps one writer per tmux session. Task Web is always read-only. Task CLI
702
- entry is read-only unless `--read-write` is requested, preventing observation
703
- from changing Agent execution.
689
+ Managed Task Agents use the [hybrid Provider runtime](docs/provider-runtime.md).
690
+ The Controller and Agent Host send Provider-native structured requests; tmux
691
+ and PTY keep the Host alive, show output, and provide an explicit human input
692
+ gateway. Managed prompts are never delivered as terminal bytes.
693
+
694
+ Codex uses a persistent App Server JSON-RPC process. Claude Code uses a
695
+ persistent stream-json process with exact user-message replay acknowledgement.
696
+ Both follow the same Conversation, Activation, Turn, and authority-epoch
697
+ contract. A timeout or uncertain write becomes `delivery-unknown` and is never
698
+ automatically retried.
699
+
700
+ Task Role observation and takeover are explicit:
701
+
702
+ ```sh
703
+ yui task role view <task-id> <role>
704
+ yui task role takeover <task-id> <role>
705
+ yui task role release <task-id> <role>
706
+ ```
707
+
708
+ `view` is read-only. `takeover` transfers the durable writer authority to the
709
+ human before enabling the Host PTY gateway and requires a live managed Run;
710
+ detach returns it to the Controller. `release` remains available without a live
711
+ Run and idempotently repairs a stranded or partially synchronized takeover.
712
+ Authority cannot transfer while a Provider Turn or input delivery is unsettled.
713
+
714
+ Global Operator and global Role sessions remain native interactive CLIs:
704
715
 
705
716
  ```sh
706
717
  yui session enter <global-role>
707
- yui task enter <task-id> [role] [--read-only | --read-write]
708
- yui task role enter <task-id> <role> [--read-only | --read-write]
709
718
  ```
710
719
 
720
+ tmux fixes a pane's history capacity when that pane is created. Existing panes
721
+ retain their configured capacity; managed runtime output remains observable in
722
+ the Agent Host pane without becoming lifecycle or acknowledgement evidence.
723
+
711
724
  Each Role, including a Task-bound Worker instance, can bind multiple configured Agents, has one active Agent, and keeps
712
725
  a separate native session per Agent binding. Operator narrows this to at most
713
726
  one Agent per adapter—for example, one Codex and one Claude—so its bindings are
@@ -729,12 +742,10 @@ snapshot instead of applying desired drift as a hot change.
729
742
 
730
743
  Use `yui config role unbind <global-role> <agent-id>` or `yui task role unbind <task-id> <role> <agent-id>` to retire a dormant binding. The active binding and any non-stopped native session are rejected; a stopped session record is removed atomically with the binding.
731
744
 
732
- Claude session IDs are preallocated at launch. Every managed Task Claude Run
733
- uses a new finite process; resume starts a new process against the fixed native
734
- session instead of reusing an interactive pane. Codex discovers its native
735
- thread identity from structured lifecycle events. Managed Task Runs use one
736
- Agent Driver Hook ingress for both CLIs. Global interactive Codex sessions may
737
- still use its structured `notify` callback for conversation presentation.
745
+ Claude session IDs are preallocated at launch. Codex discovers its native
746
+ thread identity from App Server responses. Managed Task Runs use structured
747
+ Provider observations for both CLIs. Global interactive Codex sessions may
748
+ still use its `notify` callback for conversation presentation.
738
749
 
739
750
  Automated lifecycle and delivery decisions use structured Hook payloads,
740
751
  persisted identities, usage snapshots, tmux process state, receipts, and pane
@@ -277,8 +277,8 @@ const taskChildren = [
277
277
  {
278
278
  name: "complete",
279
279
  summary: "Complete an active Task and stop automatic wakeups.",
280
- usage: "yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote]",
281
- options: ["--summary", "--summary-file", "--refresh-remote"],
280
+ usage: "yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote] [--accept-published-tree <publication-id>]",
281
+ options: ["--summary", "--summary-file", "--refresh-remote", "--accept-published-tree"],
282
282
  fileOptions: ["--summary-file"]
283
283
  },
284
284
  {
@@ -513,7 +513,8 @@ const taskChildren = [
513
513
  name: "role",
514
514
  summary: "Manage Roles within a Task.",
515
515
  sections: [{ id: "manage", title: "Commands", entries: [
516
- "add", "list", "status", "show", "update", "remove", "bind", "unbind", "reset", "enter"
516
+ "add", "list", "status", "show", "update", "remove", "bind", "unbind", "reset",
517
+ "view", "takeover", "release"
517
518
  ] }],
518
519
  children: [
519
520
  {
@@ -548,10 +549,19 @@ const taskChildren = [
548
549
  options: ["--reason"]
549
550
  },
550
551
  {
551
- name: "enter",
552
- summary: "Attach to an existing Task Role session without starting it.",
553
- usage: "yui task role enter <task> <role> [--read-only | --read-write]",
554
- options: ["--read-only", "--read-write"]
552
+ name: "view",
553
+ summary: "Attach read-only to a managed Provider presentation surface.",
554
+ usage: "yui task role view <task> <role>"
555
+ },
556
+ {
557
+ name: "takeover",
558
+ summary: "Acquire Provider writer authority and enter the PTY input gateway.",
559
+ usage: "yui task role takeover <task> <role>"
560
+ },
561
+ {
562
+ name: "release",
563
+ summary: "Return stranded human Provider authority to the Controller.",
564
+ usage: "yui task role release <task> <role>"
555
565
  }
556
566
  ]
557
567
  },
@@ -675,7 +685,7 @@ const taskChildren = [
675
685
  {
676
686
  name: "run",
677
687
  summary: "Inspect and control Task Role Agent Runs.",
678
- sections: [{ id: "manage", title: "Commands", entries: ["list", "show", "retry", "settle", "recover", "yield", "checkpoint"] }],
688
+ sections: [{ id: "manage", title: "Commands", entries: ["list", "show", "retry", "settle", "recover", "yield", "context", "checkpoint"] }],
679
689
  children: [
680
690
  { name: "list", summary: "List Runs for a work item.", usage: "yui task run list <task>/<work>" },
681
691
  {
@@ -706,6 +716,28 @@ const taskChildren = [
706
716
  options: ["--summary", "--summary-file"],
707
717
  fileOptions: ["--summary-file"]
708
718
  },
719
+ {
720
+ name: "context",
721
+ summary: "Load the exact authorized Run context.",
722
+ usage: "yui task run context <task>/<run> [--json]",
723
+ executable: true,
724
+ hidden: true,
725
+ sections: [{ id: "load", title: "Commands", entries: ["expand", "delta"] }],
726
+ children: [
727
+ {
728
+ name: "expand",
729
+ summary: "Expand one authorized Run context reference.",
730
+ usage: "yui task run context expand <task>/<run> <ref-id> [--store <store>] [--mode full]",
731
+ options: ["--store", "--mode"]
732
+ },
733
+ {
734
+ name: "delta",
735
+ summary: "Load authorized Run context changes after a cursor.",
736
+ usage: "yui task run context delta <task>/<run> --after <cursor>",
737
+ options: ["--after"]
738
+ }
739
+ ]
740
+ },
709
741
  {
710
742
  name: "checkpoint",
711
743
  summary: "Record durable progress for a long-running Agent Run.",
@@ -719,7 +751,7 @@ const taskChildren = [
719
751
  {
720
752
  name: "review",
721
753
  summary: "Control Task-final ReviewRounds.",
722
- sections: [{ id: "manage", title: "Commands", entries: ["request", "group", "retry", "finding"] }],
754
+ sections: [{ id: "manage", title: "Commands", entries: ["request", "force-fresh", "group", "retry", "finding"] }],
723
755
  children: [
724
756
  {
725
757
  name: "request",
@@ -727,6 +759,11 @@ const taskChildren = [
727
759
  usage: "yui task review request <task> --role <global-role> [--strategy fixed:<count>|adaptive:<max>] [--lane-role <role> ...] [--delta-recheck]",
728
760
  options: ["--role", "--strategy", "--lane-role", "--delta-recheck"]
729
761
  },
762
+ {
763
+ name: "force-fresh",
764
+ summary: "Replace one exact non-semantic failed Task-final Review with a distinct full Round.",
765
+ usage: "yui task review force-fresh <task>/<review-round>"
766
+ },
730
767
  {
731
768
  name: "group",
732
769
  summary: "Resolve a Reviewer ExecutionGroup after its Lanes finish.",
@@ -940,12 +977,6 @@ const taskChildren = [
940
977
  { name: "show", summary: "Show one ChangeSet.", usage: "yui task change-set show <task>/<change-set>" }
941
978
  ]
942
979
  },
943
- {
944
- name: "enter",
945
- summary: "Attach to an existing Task Role, defaulting to Leader and read-only.",
946
- usage: "yui task enter <task> [role] [--read-only | --read-write]",
947
- options: ["--read-only", "--read-write"]
948
- }
949
980
  ];
950
981
  export const ROOT_COMMAND = buildNode({
951
982
  name: "yui",
@@ -1319,7 +1350,7 @@ export const ROOT_COMMAND = buildNode({
1319
1350
  summary: "Manage Tasks, WorkItems, Agent Runs, and integration.",
1320
1351
  sections: [
1321
1352
  { id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "base", "update", "activate", "complete", "reopen", "retire", "list", "show", "context", "next-action", "archive", "rebuild", "history", "replace", "reconcile"] },
1322
- { id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "publication", "work", "run", "review", "integration", "role", "enter", "overlap", "change-set"] },
1353
+ { id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "publication", "work", "run", "review", "integration", "role", "overlap", "change-set"] },
1323
1354
  { id: "knowledge", title: "Task Knowledge", entries: ["brief", "decision", "milestone", "event", "continuation", "wake"] }
1324
1355
  ],
1325
1356
  children: taskChildren
@@ -1359,8 +1390,17 @@ export const ROOT_COMMAND = buildNode({
1359
1390
  name: "internal",
1360
1391
  summary: "Internal Yui callbacks.",
1361
1392
  hidden: true,
1362
- sections: [{ id: "callbacks", title: "Callbacks", entries: ["session-notify", "runtime-hook"] }],
1393
+ sections: [{
1394
+ id: "callbacks",
1395
+ title: "Callbacks",
1396
+ entries: ["session-notify", "runtime-hook", "agent-host"]
1397
+ }],
1363
1398
  children: [
1399
+ {
1400
+ name: "agent-host",
1401
+ summary: "Run the persistent structured Provider host.",
1402
+ usage: "yui internal agent-host <launch-id> <ticket>"
1403
+ },
1364
1404
  {
1365
1405
  name: "session-notify",
1366
1406
  summary: "Record a structured native session notification.",
@@ -361,8 +361,8 @@ export const INTERACTION_POLICIES = Object.freeze([
361
361
  trailingOptions: { "--reason": "value" },
362
362
  confirmation: { action: "Reset Task Role Session", targetArgumentIndex: 4 }
363
363
  },
364
- {
365
- commandPath: ["task", "role", "enter"],
364
+ ...["view", "takeover", "release"].map((command) => ({
365
+ commandPath: ["task", "role", command],
366
366
  selectors: [
367
367
  { argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true },
368
368
  {
@@ -372,9 +372,8 @@ export const INTERACTION_POLICIES = Object.freeze([
372
372
  dependsOn: 3,
373
373
  actionTarget: true
374
374
  }
375
- ],
376
- trailingOptions: { "--read-only": "flag", "--read-write": "flag" }
377
- },
375
+ ]
376
+ })),
378
377
  {
379
378
  commandPath: ["task", "work", "create"],
380
379
  selectors: [
@@ -534,11 +533,6 @@ export const INTERACTION_POLICIES = Object.freeze([
534
533
  ? { trailingOptions: { "--reason": "value" } }
535
534
  : {})
536
535
  })),
537
- {
538
- commandPath: ["task", "enter"],
539
- selectors: [{ argumentIndex: 2, entity: "task", provider: "tasks", actionTarget: true }],
540
- trailingOptions: { "--read-only": "flag", "--read-write": "flag" }
541
- },
542
536
  {
543
537
  commandPath: ["jobs", "retry"],
544
538
  selectors: [{ argumentIndex: 2, entity: "job", provider: "jobs", actionTarget: true }]
@@ -32,7 +32,8 @@ function resolveExecutionPath(args) {
32
32
  const child = findChild(node, args[index] ?? "");
33
33
  const internalExecutable = child !== undefined && ((node === ROOT_COMMAND && child.name === "internal")
34
34
  || (node.path.join(" ") === "yui config completion" && child.name === "candidates")
35
- || (node.path.join(" ") === "yui task run" && child.name === "checkpoint")
35
+ || (node.path.join(" ") === "yui task run"
36
+ && (child.name === "checkpoint" || child.name === "context"))
36
37
  || (node.path.join(" ") === "yui controller"
37
38
  && (child.name === "identity" || child.name === "live-identity")));
38
39
  if (child === undefined || (child.hidden && !internalExecutable)) {
package/dist/cli.js CHANGED
@@ -36,7 +36,7 @@ import { runResourcesCommand } from "./commands/resourcesCommands.js";
36
36
  import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
37
37
  import { runProjectCommand } from "./commands/projectCommands.js";
38
38
  import { runProfileCommand } from "./commands/profileCommands.js";
39
- import { dispatchPreparedReviewRound, failPendingReviewRound, assertTaskRoleWritableAttachAvailable, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, validateTaskArchiveRequest } from "./commands/taskCommands.js";
39
+ import { dispatchPreparedReviewRound, failPendingReviewRound, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, validateTaskArchiveRequest } from "./commands/taskCommands.js";
40
40
  import { taskActor } from "./commands/taskActor.js";
41
41
  import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
42
42
  import { runTaskChangeSetCommand } from "./commands/taskChangeSetCommands.js";
@@ -48,7 +48,7 @@ import { createUpdatePorts } from "./cli/updatePorts.js";
48
48
  import { createReleaseWorkflowPorts } from "./release/releaseWorkflowPorts.js";
49
49
  import { readRuntimeIdentity } from "./release/runtimeRelease.js";
50
50
  import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
51
- import { reconcileTaskRemoteBaselines } from "./commands/taskCompletionGate.js";
51
+ import { reconcileTaskRemoteBaselines, verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
52
52
  import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
53
53
  import { assertTaskBaseFreshnessForCompletion, inspectTaskBaseFreshness } from "./repository/taskBaseFreshness.js";
54
54
  import { FileCompletionManager, resolveCliIdentity } from "./completion/fileCompletionManager.js";
@@ -61,9 +61,9 @@ import { runSessionNotifyCommand } from "./controller/sessionNotify.js";
61
61
  import { openSchedulerTelemetry } from "./telemetry/telemetryWiring.js";
62
62
  import { runRuntimeObservationHookCommand } from "./controller/runtimeObservationHook.js";
63
63
  import { buildDoctorReport, renderDoctor, runDoctorCommand } from "./doctor/doctor.js";
64
- import { agentNotFound, CliError, usageError } from "./errors/cliError.js";
64
+ import { agentNotFound, CliError, runtimeError, usageError } from "./errors/cliError.js";
65
65
  import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
66
- import { runAgentHost } from "./runtime/agentHost.js";
66
+ import { AGENT_HOST_CONTROL_PROTOCOL, runAgentHost, sendAgentHostAuthorityControl } from "./runtime/agentHost.js";
67
67
  import { TaskWorkspaceCoordinator, WorkspaceCleanupBlockedError } from "./repository/taskWorkspaceCoordinator.js";
68
68
  import { FileTaskWorkspacePreparer, ReviewRoundWorkspaceEvidenceError } from "./repository/taskWorkspacePreparer.js";
69
69
  import { inspectStorageSchema } from "./storage/storageSchema.js";
@@ -944,6 +944,7 @@ export async function main() {
944
944
  }
945
945
  }
946
946
  let completionSummary;
947
+ let completionPublishedTreeProof;
947
948
  if (resolved[1] === "base" && resolved[2] === "status") {
948
949
  const result = await runTaskBaseStatusCommand(resolved.slice(3), store);
949
950
  emit(result.output, false, result.data);
@@ -958,12 +959,26 @@ export async function main() {
958
959
  ...(taskFinalReviewContract === undefined
959
960
  ? {}
960
961
  : { taskFinalReviewContract })
961
- });
962
+ }, completionRequest);
962
963
  if (!completion.completed && !completion.activeTaskReview) {
963
- const freshness = await inspectTaskBaseFreshness(resolved[2], store, {
964
- refresh: refreshRemote
965
- });
966
- for (const warning of assertTaskBaseFreshnessForCompletion(freshness)) {
964
+ // An explicit refresh must fetch the remote object graph before the
965
+ // Publication proof resolves its exact commit. Without the flag the
966
+ // command remains offline and preserves the existing proof-first path.
967
+ const refreshedFreshness = refreshRemote
968
+ ? await inspectTaskBaseFreshness(resolved[2], store, { refresh: true })
969
+ : undefined;
970
+ if (completionRequest.acceptedPublishedTreePublicationId !== undefined) {
971
+ completionPublishedTreeProof = await verifyTaskCompletionPublishedTree(completionRequest.taskId, completionRequest.acceptedPublishedTreePublicationId, store);
972
+ }
973
+ const freshness = refreshedFreshness
974
+ ?? await inspectTaskBaseFreshness(resolved[2], store);
975
+ for (const warning of assertTaskBaseFreshnessForCompletion(freshness, {
976
+ ...(completionPublishedTreeProof === undefined
977
+ ? {}
978
+ : {
979
+ acceptedPublishedTreeProjectId: completionPublishedTreeProof.projectId
980
+ })
981
+ })) {
967
982
  process.stderr.write(`Warning: ${warning}\n`);
968
983
  }
969
984
  // Keep completion offline by default. An explicit refresh is the only
@@ -1002,6 +1017,9 @@ export async function main() {
1002
1017
  ? {}
1003
1018
  : { taskFinalReviewContract }),
1004
1019
  ...(completionSummary === undefined ? {} : { completionSummary }),
1020
+ ...(completionPublishedTreeProof === undefined
1021
+ ? {}
1022
+ : { completionPublishedTreeProof }),
1005
1023
  ...(workItemIntegrationProof === undefined ? {} : { workItemIntegrationProof }),
1006
1024
  ...(candidateGitSnapshot === undefined ? {} : { candidateGitSnapshot }),
1007
1025
  ...(candidateMaterialization === undefined
@@ -1148,30 +1166,82 @@ export async function main() {
1148
1166
  : { command: result.data, ...reviewData });
1149
1167
  return;
1150
1168
  }
1151
- if (result.output !== undefined)
1152
- emit(result.output);
1153
- try {
1154
- tmux.attachRole(result.taskId, result.roleName, result.access, {
1155
- ...(result.access === "read-write"
1156
- ? {
1157
- revalidateWritableAttach: () => {
1158
- assertTaskRoleWritableAttachAvailable(store, result.taskId, result.roleName, {
1159
- isManagedProcessRunning: () => (tmux.probeRoleStatus(result.taskId, result.roleName) === "running")
1160
- });
1161
- }
1169
+ if (jsonOutput) {
1170
+ throw usageError("Task Role view/takeover requires an interactive terminal.");
1171
+ }
1172
+ if (result.kind === "view") {
1173
+ if (result.output !== undefined)
1174
+ emit(result.output);
1175
+ tmux.attachRole(result.taskId, result.roleName, "read-only");
1176
+ return;
1177
+ }
1178
+ const syncAuthority = async (authorityResult) => {
1179
+ let control;
1180
+ try {
1181
+ control = await sendAgentHostAuthorityControl({
1182
+ home,
1183
+ scope: "task",
1184
+ taskId: authorityResult.taskId,
1185
+ roleName: authorityResult.roleName,
1186
+ control: {
1187
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
1188
+ type: "set-authority",
1189
+ nativeSessionId: authorityResult.nativeSessionId,
1190
+ authority: authorityResult.authority
1162
1191
  }
1163
- : {})
1192
+ });
1193
+ }
1194
+ catch (error) {
1195
+ throw runtimeError(`Agent Host authority synchronization failed at epoch ${authorityResult.authority.epoch}: `
1196
+ + `${error instanceof Error ? error.message : String(error)}. `
1197
+ + `Durable authority is ${authorityResult.authority.owner}-owned; retry `
1198
+ + `'yui task role release ${authorityResult.taskId} ${authorityResult.roleName}' `
1199
+ + "to reconcile the Host.");
1200
+ }
1201
+ if (control.outcome !== "accepted"
1202
+ || control.snapshot.nativeSessionId !== authorityResult.nativeSessionId
1203
+ || control.snapshot.authorityEpoch !== authorityResult.authority.epoch
1204
+ || control.snapshot.authorityOwner !== authorityResult.authority.owner
1205
+ || control.snapshot.authorityHolderId !== authorityResult.authority.holderId) {
1206
+ throw runtimeError(`Agent Host did not accept Provider authority epoch ${authorityResult.authority.epoch}: `
1207
+ + (control.snapshot.detail ?? control.outcome)
1208
+ + `. Durable authority is ${authorityResult.authority.owner}-owned; `
1209
+ + "retry 'yui task role release "
1210
+ + `${authorityResult.taskId} ${authorityResult.roleName}' to reconcile the Host.`);
1211
+ }
1212
+ return control;
1213
+ };
1214
+ await syncAuthority(result);
1215
+ emit(result.output);
1216
+ if (result.action === "release") {
1217
+ runtime.notifyMailboxChanged({
1218
+ kind: "role",
1219
+ taskId: result.taskId,
1220
+ roleName: result.roleName
1164
1221
  });
1222
+ return;
1223
+ }
1224
+ process.stdout.write("Provider input is now routed through the Agent Host PTY gateway. "
1225
+ + "Use tmux detach (Ctrl-b d) to return authority to the Controller.\n");
1226
+ try {
1227
+ tmux.attachRole(result.taskId, result.roleName, "read-write");
1165
1228
  }
1166
1229
  finally {
1167
- if (result.access === "read-write") {
1168
- // A Run claimed while the writer lease was visible is intentionally
1169
- // paused. Releasing the lease only signals that existing durable
1170
- // work may be reconsidered; it never creates or wakes a Run.
1230
+ const currentTask = store.getTask(result.taskId);
1231
+ // Completing or retiring the Task from inside the takeover Turn owns
1232
+ // Provider shutdown and clears the live binding. Do not turn that
1233
+ // successful terminal transition into a failing best-effort release.
1234
+ if (currentTask?.status === "active") {
1235
+ const released = runTaskCommand(["role", "release", result.taskId, result.roleName], store, { runtime, environment: process.env, yuiHome: home });
1236
+ if (released.kind !== "authority" || released.action !== "release") {
1237
+ throw runtimeError("Provider authority release returned an invalid result.");
1238
+ }
1239
+ await syncAuthority(released);
1240
+ emit(released.output);
1171
1241
  runtime.notifyMailboxChanged({
1172
1242
  kind: "role",
1173
- taskId: result.taskId,
1174
- roleName: result.roleName
1243
+ taskId: released.taskId,
1244
+ roleName: released.roleName
1175
1245
  });
1176
1246
  }
1177
1247
  }
@@ -1621,6 +1691,15 @@ async function actualTaskReviewCandidateForTaskCommand(args, store, preparer, en
1621
1691
  taskId = reference.taskId;
1622
1692
  }
1623
1693
  }
1694
+ else if (args[1] === "review"
1695
+ && args[2] === "force-fresh"
1696
+ && args[3] !== undefined) {
1697
+ const reference = cliTaskRecordReference(args[3], "reviewRound", environment);
1698
+ const round = store.getReviewRound(reference.taskId, reference.localId);
1699
+ if (round !== null && (round.scope ?? "work-item") === "task") {
1700
+ taskId = reference.taskId;
1701
+ }
1702
+ }
1624
1703
  else if (args[1] === "work"
1625
1704
  && args[2] === "review"
1626
1705
  && args[3] === "retry"