@zq-silk/yui 0.16.0 → 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +3 -0
  4. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  5. package/dist/agentRun/agentRun.js +7 -17
  6. package/dist/cli/agentConfigurationPicker.js +24 -21
  7. package/dist/cli/commandCatalog.js +18 -10
  8. package/dist/cli/roleWizard.js +41 -30
  9. package/dist/cli/updateCommand.js +35 -13
  10. package/dist/cli/updateOrchestrator.js +73 -33
  11. package/dist/cli/updatePorts.js +51 -20
  12. package/dist/cli.js +35 -26
  13. package/dist/commands/controllerCommands.js +1 -1
  14. package/dist/commands/executionAuditCommands.js +2 -2
  15. package/dist/commands/projectCommands.js +1 -1
  16. package/dist/commands/releaseCommands.js +9 -38
  17. package/dist/commands/taskCommands.js +84 -81
  18. package/dist/commands/taskCompletionGate.js +0 -117
  19. package/dist/commands/taskRoleRuntimeStatus.js +1 -1
  20. package/dist/commands/taskUpstreamCommands.js +93 -54
  21. package/dist/context/runContextPack.js +30 -54
  22. package/dist/context/runInputContract.js +9 -0
  23. package/dist/context/sessionBootstrapManifest.js +11 -19
  24. package/dist/controller/agentHostObservation.js +4 -2
  25. package/dist/controller/controller.js +8 -6
  26. package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
  27. package/dist/controller/globalInputDelivery.js +28 -7
  28. package/dist/controller/globalRuntimeAttention.js +34 -0
  29. package/dist/controller/operatorNotification.js +31 -0
  30. package/dist/controller/providerRetryAdmission.js +3 -1
  31. package/dist/controller/providerRetryDelivery.js +10 -3
  32. package/dist/controller/runtime.js +1 -27
  33. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  34. package/dist/controller/structuredProviderObservation.js +27 -0
  35. package/dist/controller/updateReconciliation.js +72 -20
  36. package/dist/doctor/doctor.js +2 -1
  37. package/dist/executor/agentAdapter.js +28 -96
  38. package/dist/executor/agentConfigurationCatalog.js +13 -68
  39. package/dist/executor/agentConfigurationFields.js +120 -0
  40. package/dist/executor/agentConfigurationProbe.js +55 -80
  41. package/dist/executor/agentExecutor.js +5 -2
  42. package/dist/executor/executorRegistry.js +0 -9
  43. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  44. package/dist/interaction/operatorPresentation.js +2 -1
  45. package/dist/message/messageContinuation.js +8 -9
  46. package/dist/observability/executionAudit.js +1 -10
  47. package/dist/observability/runtimeIdentity.js +0 -23
  48. package/dist/output/agentConfigurationPresentation.js +4 -2
  49. package/dist/release/releaseHandover.js +2 -2
  50. package/dist/release/releaseIdempotencyStore.js +0 -23
  51. package/dist/release/releaseWorkflowPorts.js +8 -4
  52. package/dist/repository/gitWorkspace.js +66 -19
  53. package/dist/repository/taskWorkspacePreparer.js +75 -68
  54. package/dist/resources/resourceRegistry.js +90 -44
  55. package/dist/resources/sqliteResourceRegistry.js +3 -5
  56. package/dist/runtime/acpSession.js +21 -5
  57. package/dist/runtime/agentEndpoint.js +4 -1
  58. package/dist/runtime/agentHost.js +22 -62
  59. package/dist/runtime/agentHostCleanup.js +85 -0
  60. package/dist/runtime/agentHostProtocol.js +1 -1
  61. package/dist/runtime/builtinAgentDrivers.js +46 -119
  62. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  63. package/dist/runtime/codexAppServerRuntime.js +55 -89
  64. package/dist/runtime/jsonLineChannel.js +35 -7
  65. package/dist/runtime/providerContinuationReconciliationService.js +42 -3
  66. package/dist/runtime/providerRetry.js +15 -0
  67. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  68. package/dist/runtime/structuredProviderHost.js +106 -81
  69. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  70. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  71. package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
  72. package/dist/task/nextAction.js +33 -23
  73. package/dist/task/task.js +2 -1
  74. package/dist/web/assets/assetManifest.js +2 -0
  75. package/dist/web/assets/client/app.js +72 -3
  76. package/dist/web/assets/client/components.js +2 -1
  77. package/dist/web/assets/client/i18n.js +2 -0
  78. package/dist/web/assets/client/taskSummary.js +345 -0
  79. package/dist/web/assets/client/taskSurface.js +44 -26
  80. package/dist/web/assets/client/view.js +33 -0
  81. package/dist/web/assets/shell.js +1 -0
  82. package/dist/web/assets/styles/cards.js +21 -0
  83. package/dist/web/webServer.js +39 -5
  84. package/dist/web/webSessions.js +165 -0
  85. package/dist/web/webSnapshot.js +23 -0
  86. package/dist/web/webTaskSurface.js +23 -0
  87. package/docs/managed-turn-and-session-runtime.md +31 -0
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
  89. package/docs/observability/README.md +47 -0
  90. package/docs/observability/README.zh-CN.md +37 -0
  91. package/docs/project-refresh.md +9 -0
  92. package/docs/project-refresh.zh-CN.md +8 -0
  93. package/docs/provider-retry.md +35 -0
  94. package/docs/release-workflow.md +128 -20
  95. package/docs/release-workflow.zh-CN.md +107 -14
  96. package/docs/roles-and-configuration.md +30 -0
  97. package/docs/roles-and-configuration.zh-CN.md +20 -0
  98. package/docs/task-delivery.md +109 -0
  99. package/docs/task-delivery.zh-CN.md +80 -0
  100. package/docs/task-discovery.md +9 -0
  101. package/docs/task-discovery.zh-CN.md +6 -0
  102. package/docs/testing/verification-levels.md +33 -12
  103. package/docs/testing/verification-levels.zh-CN.md +28 -8
  104. package/i18n/README.zh-CN.md +2 -0
  105. package/package.json +1 -1
  106. package/skills/yui-leader/SKILL.md +11 -0
  107. package/skills/yui-leader/references/execution.md +22 -0
  108. package/skills/yui-leader/references/planning.md +6 -2
  109. package/skills/yui-operator/SKILL.md +45 -13
  110. package/skills/yui-operator/references/task-delivery.md +99 -0
  111. package/skills/yui-runtime/SKILL.md +5 -3
  112. package/skills/yui-runtime/references/publication.md +56 -5
  113. package/skills/yui-runtime/references/recovery.md +12 -2
  114. package/dist/runtime/agentHostCompatibility.js +0 -127
package/ARCHITECTURE.md CHANGED
@@ -67,6 +67,18 @@ AgentHost serializes submission through AgentEndpoint. The Provider binding
67
67
  records actual acceptance and native correlation. A notification can settle
68
68
  on acceptance without requiring a final execution report.
69
69
 
70
+ Operator attention has a separate, local handoff boundary: one transaction
71
+ saves the exact batch as a Global system Message and consumes only that mailbox
72
+ claim. `queued` (with the Message id) means durable queue ownership, not Provider
73
+ acceptance or Agent processing. Later events form a new batch without repeating
74
+ the handed-off refs. Global Messages alone own subsequent native delivery,
75
+ including rejection, unknown acceptance and Session-target mismatch; none causes
76
+ automatic reissuance under a different key. A pass can mark multiple definitively
77
+ unsent stale-target entries undelivered before reaching successor input, but never
78
+ bypass an uncertain attempt or an interrupt-then reservation. Original Messages
79
+ and source Task/Input/Event records remain inspectable. Notifications ask Agents
80
+ to reread facts; they do not authorize replaying prior operations.
81
+
70
82
  Busy with proven non-acceptance preserves the input for a subsequent attempt.
71
83
  Transport submission alone does not prove acceptance. Unknown effects remain
72
84
  visible and fenced: no blind resend or inferred success. Explicit replacement
@@ -53,6 +53,14 @@ AgentRun 记录一次明确请求的执行,带有冻结的 Context 和生效
53
53
  mailbox 批次;AgentHost 通过 AgentEndpoint 串行化提交。Provider 绑定记录实际接受和原生
54
54
  关联。一条通知可以在接受时结算,而不要求最终执行报告。
55
55
 
56
+ Operator attention 有独立的本地交接边界:同一事务把精确批次保存为 Global 系统 Message,
57
+ 并且只消费该 mailbox claim。`queued`(带 Message id)表示持久队列已接管,不代表 Provider
58
+ 接受或 Agent 已处理。后到事件形成新批次,不重复已经交出的 refs。后续原生投递仅由
59
+ Global Message 负责,包括拒绝、接受结果未知和 Session 目标失效;这些结果都不会自动换键
60
+ 重发。一轮可将多个已明确未提交且目标失效的条目标为未投递,再处理后继输入,但不能越过
61
+ 不确定尝试或 interrupt-then 预约。原始 Message 和来源 Task/Input/Event 仍可查阅。通知
62
+ 只要求 Agent 重读事实,不授予重放先前操作的权限。
63
+
56
64
  忙碌且已证明未接受会为后续尝试保留输入。仅有传输提交不证明接受。未知效果保持可见且被
57
65
  围栏隔离:不盲目重发,也不推断成功。显式替换先解决实际的原生执行,再丢弃其工程占用。
58
66
  在一个已认领批次期间到达的输入,留待下一个批次。读取 Context 不消费投递。
package/README.md CHANGED
@@ -140,6 +140,9 @@ For a visual overview, run `yui web` in another terminal. The local Web view
140
140
  shows the same tasks and pending questions and lets you send messages, answer
141
141
  questions and explicitly queue, steer or interrupt Task input. These authenticated
142
142
  Task controls use the same operations as the CLI; Web is not a separate task system.
143
+ The detail prioritizes user input, native Session activity, Task progress and key
144
+ conclusions; fixed file results and delivery evidence open on demand. See
145
+ [attention and progress](docs/observability/README.md#web-attention-and-progress).
143
146
  See [Web permissions](docs/architecture/capabilities-and-resources.md#cli-and-web)
144
147
  and [input timing](docs/managed-turn-and-session-runtime.md#input-timing-queue-steer-and-interrupt).
145
148
 
@@ -19,10 +19,6 @@ export const YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES = Object.freeze([
19
19
  "YUI_RUN_ID",
20
20
  "YUI_NATIVE_SESSION_ROOT",
21
21
  "YUI_NATIVE_SESSION_ID",
22
- // Retired: Yui no longer publishes a frozen control-plane descriptor into a
23
- // managed process. Sessions launched by an earlier release still carry it, so
24
- // it stays in this sanitize list.
25
- "YUI_CONTROL_PLANE_DESCRIPTOR",
26
22
  "YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR",
27
23
  "YUI_TASK_RUNTIME_SERVICE_NAMESPACE",
28
24
  "YUI_SESSION_TITLE",
@@ -1,7 +1,7 @@
1
1
  import { validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
2
2
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
3
3
  import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
4
- import { createRunInput, createRunInputEnvelope, validateRunInput } from "../context/runInputContract.js";
4
+ import { createRunInputEnvelope, requireRunContextSnapshotRef, validateRunInput } from "../context/runInputContract.js";
5
5
  import { providerRetryProjection } from "../runtime/providerRetry.js";
6
6
  import { boundedRunFailureDiagnostic, MAX_RUN_FAILURE_DIAGNOSTIC_BYTES, MAX_RUN_RESULT_OUTPUT_BYTES } from "../domain/agentResultTransport.js";
7
7
  export { boundedRunFailureDiagnostic, MAX_RUN_FAILURE_DIAGNOSTIC_BYTES, MAX_RUN_RESULT_OUTPUT_BYTES, transportAgentResult } from "../domain/agentResultTransport.js";
@@ -32,6 +32,9 @@ export function createRun(id, taskId, roleName, mode, input, now, context) {
32
32
  }
33
33
  const timestamp = now.toISOString();
34
34
  const normalizedInput = validateRunInput(input);
35
+ const snapshot = requireRunContextSnapshotRef(normalizedInput);
36
+ if (snapshot.taskId !== taskId)
37
+ throw new Error("AgentRun Context Snapshot belongs to another Task.");
35
38
  return {
36
39
  schemaVersion: 5,
37
40
  id: requireSafeIdentity(id, "AgentRun id"),
@@ -85,22 +88,6 @@ export function runPurposeAdmitsTaskState(purpose, task) {
85
88
  ? task.status === "draft" || task.status === "active"
86
89
  : task.status === "active";
87
90
  }
88
- /** Binds a freshly created, not-yet-persisted Turn to its frozen Context. */
89
- export function withRunContextSnapshot(run, snapshot, deltaRefIds = []) {
90
- const initial = run.inputs[0];
91
- if (run.status !== "active" || initial.input.contextSnapshotRef !== undefined) {
92
- throw new Error(`Cannot replace the AgentRun Context Snapshot: ${run.id}.`);
93
- }
94
- const input = createRunInput({
95
- ...initial.input,
96
- contextSnapshotRef: snapshot,
97
- deltaRefIds
98
- });
99
- return validateRun(Object.freeze({
100
- ...run,
101
- inputs: [runInputRecord(input, 1, initial.submittedAt), ...run.inputs.slice(1)]
102
- }));
103
- }
104
91
  export function appendRunInput(run, input, now) {
105
92
  validateRun(run);
106
93
  if (run.status !== "active")
@@ -118,6 +105,7 @@ export function appendRunInput(run, input, now) {
118
105
  /** Derives Provider-visible identity from the AgentRun, the sole semantic owner. */
119
106
  export function runInputEnvelope(run, sequence = 1) {
120
107
  validateRun(run);
108
+ requireRunContextSnapshotRef(run.inputs[0].input);
121
109
  const record = run.inputs[sequence - 1];
122
110
  if (record === undefined)
123
111
  throw new Error(`AgentRun input does not exist: ${run.id}/${sequence}.`);
@@ -295,6 +283,8 @@ export function validateRun(run) {
295
283
  throw new Error("Execution AgentRun cannot carry Review effective provenance.");
296
284
  }
297
285
  for (const record of run.inputs) {
286
+ // Structural validation keeps historical records and observed subsequent
287
+ // inputs readable. Creation/submission require the initial frozen Snapshot.
298
288
  createRunInputEnvelope(runEnvelopeContext(run), record.input);
299
289
  }
300
290
  if (!["active", "completed", "failed"].includes(run.status)) {
@@ -30,12 +30,11 @@ export async function selectAgentEffort(resolved, io, input) {
30
30
  }
31
31
  /** Select provider-native permission settings in the adapter's canonical shape. */
32
32
  export async function selectAgentPermission(resolved, io, current) {
33
+ renderResolutionNotice(resolved, io);
33
34
  const strategyField = configurationField(resolved.catalog, "permission.strategy");
34
- const strategyChoices = uniqueChoices(strategyField?.choices ?? [
35
- { value: "default", label: "default" },
36
- { value: "bypass", label: "bypass" },
37
- { value: "configured", label: "configured" }
38
- ], current.strategy);
35
+ if (strategyField?.reason !== undefined)
36
+ io.write(`${strategyField.reason}\n`);
37
+ const strategyChoices = uniqueChoices(strategyField?.available === false ? [] : strategyField?.choices ?? [], current.strategy);
39
38
  const strategy = await choose("Select permission strategy", strategyChoices.map(({ value, label, description }) => ({
40
39
  value,
41
40
  label,
@@ -67,6 +66,10 @@ export async function selectAgentPermission(resolved, io, current) {
67
66
  delete permission.approval;
68
67
  else
69
68
  permission.approval = approval.value;
69
+ if (permission.sandbox === undefined && permission.approval === undefined) {
70
+ io.write("Configured permission requires an explicit native option; configuration unchanged.\n");
71
+ return { kind: "cancelled" };
72
+ }
70
73
  return {
71
74
  kind: "selected",
72
75
  permission: permission
@@ -98,6 +101,11 @@ export async function selectAgentPermission(resolved, io, current) {
98
101
  delete permission.mode;
99
102
  else
100
103
  permission.mode = mode.value;
104
+ if (permission.mode === undefined && permission.allowedTools === undefined
105
+ && permission.disallowedTools === undefined) {
106
+ io.write("Configured permission requires an explicit native option; configuration unchanged.\n");
107
+ return { kind: "cancelled" };
108
+ }
101
109
  return {
102
110
  kind: "selected",
103
111
  permission: permission
@@ -107,12 +115,13 @@ export function renderAgentConfigurationResolutionNotice(resolved) {
107
115
  const lines = [];
108
116
  if (resolved.source === "cache") {
109
117
  lines.push((resolved.failure === undefined ? "" : `! Runtime capability request failed (${resolved.failure.message}). `)
110
- + `Showing cached options from ${resolved.fetchedAt ?? "an earlier request"}; they may be stale.`);
118
+ + `Showing identity-matched cached options from ${resolved.fetchedAt ?? "an earlier request"}; they may be stale.`);
111
119
  }
112
120
  else if (resolved.source === "fallback") {
113
121
  lines.push(`! Runtime capability request failed (${resolved.failure?.message ?? "unknown failure"}). `
114
- + "No matching cache is available; only fallback and custom values can be offered.");
122
+ + "No matching cache is available; only declared static adapter contracts and explicit custom values can be offered.");
115
123
  }
124
+ lines.push(`Metadata source: ${resolved.source}; last probe attempted ${resolved.attemptedAt}${resolved.fetchedAt === undefined ? "" : `; fetched ${resolved.fetchedAt}`}. Source describes the query, not native confirmation of every field.`);
116
125
  lines.push(...resolved.catalog.warnings.map((warning) => `! Agent catalog warning: ${warning}`));
117
126
  return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
118
127
  }
@@ -215,29 +224,23 @@ function observedEfforts(models) {
215
224
  }
216
225
  async function selectPermissionField(resolved, io, key, current, defaultValue) {
217
226
  const field = configurationField(resolved.catalog, key);
218
- const fallback = key === "permission.sandbox"
219
- ? ["read-only", "workspace-write", "danger-full-access"]
220
- : key === "permission.approval"
221
- ? ["untrusted", "on-request", "never"]
222
- : ["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"];
223
- const choices = uniqueChoices(field === undefined
224
- ? fallback.map((value) => ({ value, label: value }))
225
- : field.choices, current);
226
- const includeOmit = defaultValue === OMIT || current !== undefined;
227
+ if (field?.reason !== undefined)
228
+ io.write(`${key}: ${field.reason}\n`);
229
+ if (field === undefined)
230
+ io.write(`${key}: no catalog field was reported; no choices inferred.\n`);
231
+ const choices = uniqueChoices(field?.available === false ? [] : field?.choices ?? [], current);
227
232
  const pickerChoices = [
228
- ...(includeOmit
229
- ? [{ value: OMIT, label: "Omit", detail: "Do not pass this provider option" }]
230
- : []),
233
+ { value: OMIT, label: "Omit", detail: "Do not pass this provider option" },
231
234
  ...choices.map(({ value, label, description }) => ({
232
235
  value,
233
236
  label,
234
237
  detail: description ?? value
235
238
  })),
236
239
  ...(field?.allowCustom === true
237
- ? [{ value: CUSTOM, label: "Custom…", detail: "Enter another provider value" }]
240
+ ? [{ value: CUSTOM, label: "Custom…", detail: "Explicit value; native acceptance unverified" }]
238
241
  : [])
239
242
  ];
240
- const selected = await choose(`Select ${key}`, pickerChoices, io, current ?? defaultValue ?? pickerChoices[0]?.value ?? OMIT, key);
243
+ const selected = await choose(`Select ${key}`, pickerChoices, io, current ?? (choices.some(choice => choice.value === defaultValue) ? defaultValue : OMIT), key);
241
244
  if (selected === undefined)
242
245
  return { kind: "cancelled" };
243
246
  if (selected === OMIT)
@@ -344,7 +344,7 @@ const taskChildren = [
344
344
  },
345
345
  {
346
346
  name: "complete",
347
- summary: "Complete an active Task and stop automatic wakeups.",
347
+ summary: "Complete locally, or prepare/dispatch the configured final Review and remain active. --refresh-remote only fetches freshness observations; it never rebases, changes HEAD or starts Integration checks.",
348
348
  usage: "yui task complete <id> (--summary <text>|--summary-file <path|->) [--artifact-ref <artifact-id|turn:id|url> ...] [--refresh-remote] [--accept-published-tree <publication-id>]",
349
349
  options: ["--summary", "--summary-file", "--artifact-ref", "--refresh-remote", "--accept-published-tree"],
350
350
  fileOptions: ["--summary-file"]
@@ -356,7 +356,7 @@ const taskChildren = [
356
356
  children: [
357
357
  {
358
358
  name: "status",
359
- summary: "Classify Task Project bases against local or refreshed remote refs.",
359
+ summary: "Observe Task Project bases against local refs; --refresh fetches remote objects without changing HEAD, starting checks or creating Integration/Review.",
360
360
  usage: "yui task base status <task> [--refresh]",
361
361
  options: ["--refresh"]
362
362
  }
@@ -402,7 +402,7 @@ const taskChildren = [
402
402
  },
403
403
  {
404
404
  name: "next-action",
405
- summary: "Project the durable Task records into one protocol-level next action.",
405
+ summary: "Read Task facts, mechanical prerequisites and legal alternatives; semantic choices remain with the Leader.",
406
406
  usage: "yui task next-action <task> [--json]",
407
407
  options: ["--json"]
408
408
  },
@@ -432,7 +432,7 @@ const taskChildren = [
432
432
  children: [
433
433
  {
434
434
  name: "integrate",
435
- summary: "Rebase Task changes onto the remote development head through Integration.",
435
+ summary: "Explicitly rebase Task changes onto the remote development head, run configured/additional checks (possibly as Jobs), then advance HEAD through Integration CAS. Returns exact attempts and partial results; does not request Review or complete the Task.",
436
436
  usage: "yui task upstream integrate <task> (--latest|--project <project>) [--check <command> ...] [--rerun-checks]",
437
437
  options: ["--latest", "--project", "--check", "--rerun-checks"]
438
438
  }
@@ -893,7 +893,7 @@ const taskChildren = [
893
893
  {
894
894
  name: "expand",
895
895
  summary: "Expand one authorized AgentRun context reference.",
896
- usage: "yui task run context expand <task>/<run> <ref-id> [--store <store>] [--mode full]",
896
+ usage: "yui task run context expand <task>/<run> <ref-id> --store <store> [--mode full]",
897
897
  options: ["--store", "--mode"]
898
898
  },
899
899
  {
@@ -916,7 +916,7 @@ const taskChildren = [
916
916
  name: "retire",
917
917
  summary: "Retire an incorrect historical AgentRun without deleting its audit record.",
918
918
  usage: "yui task run retire <task>/<run> --reason <text> [--expected-progress-at <timestamp>] [--agent-id <id>] [--adapter-id <id>] [--native-session-id <id>]",
919
- options: ["--reason", "--expected-progress-at", "--progress-at", "--agent-id", "--adapter-id", "--native-session-id"]
919
+ options: ["--reason", "--expected-progress-at", "--agent-id", "--adapter-id", "--native-session-id"]
920
920
  }
921
921
  ]
922
922
  },
@@ -1142,7 +1142,12 @@ export const ROOT_COMMAND = buildNode({
1142
1142
  },
1143
1143
  { name: "help", summary: "Show root or scoped command help.", usage: "yui help [command ...]", commandPathArguments: true },
1144
1144
  { name: "version", summary: "Print the installed Yui version." },
1145
- { name: "update", summary: "Install the latest published Yui package globally." },
1145
+ {
1146
+ name: "update",
1147
+ summary: "Install the latest or an exact published Yui version after storage preflight.",
1148
+ usage: "yui update [--version <exact-version>]",
1149
+ options: ["--version"]
1150
+ },
1146
1151
  {
1147
1152
  name: "upgrade",
1148
1153
  summary: "Plan or apply supported storage migrations for this Home.",
@@ -1379,8 +1384,8 @@ export const ROOT_COMMAND = buildNode({
1379
1384
  {
1380
1385
  name: "clone",
1381
1386
  summary: "Clone and bind a Project after user confirmation.",
1382
- usage: "yui project clone <name> <remote> [--alias <name> ...] [--stable <ref>] [--development <ref>]",
1383
- options: ["--alias", "--stable", "--development"]
1387
+ usage: "yui project clone <name> <remote> [--alias <name> ...] [--stable <ref>] [--development <ref>] [--external]",
1388
+ options: ["--alias", "--stable", "--development", "--external"]
1384
1389
  },
1385
1390
  {
1386
1391
  name: "refresh",
@@ -1722,8 +1727,11 @@ export function validateCommandCatalog(root) {
1722
1727
  }
1723
1728
  const options = new Set();
1724
1729
  for (const option of [...node.options, ...node.hiddenOptions]) {
1725
- if (reservedAliases.has(option))
1730
+ // --version is an alias only as the entire root invocation. Scoped
1731
+ // commands may own a version selector; help aliases remain reserved.
1732
+ if (reservedAliases.has(option) && option !== "--version") {
1726
1733
  throw new Error(`Reserved alias token is not allowed: ${[...node.path, option].join(" ")}`);
1734
+ }
1727
1735
  if (options.has(option) || immediate.has(option))
1728
1736
  throw new Error(`Duplicate command token: ${[...node.path, option].join(" ")}`);
1729
1737
  options.add(option);
@@ -2,7 +2,7 @@ import { renderTable } from "../output/table.js";
2
2
  import { displayExecutionComponent } from "../agent/executionComponents.js";
3
3
  import { isAgentAdapterId } from "../agent/adapterCatalog.js";
4
4
  import { defaultRoleAgentConfig } from "../executor/agentAdapter.js";
5
- import { selectAgentEffort, selectAgentModelAndEffort } from "./agentConfigurationPicker.js";
5
+ import { selectAgentEffort, selectAgentModelAndEffort, renderAgentConfigurationResolutionNotice } from "./agentConfigurationPicker.js";
6
6
  const TEXT_COLUMN = { header: "Selection", minWidth: 12, maxWidth: 34 };
7
7
  export async function resolveRoleWizardArguments(commandArgs, ports, io) {
8
8
  const args = [...commandArgs];
@@ -231,6 +231,8 @@ async function configureNewAgentField(args, agent, ports, io) {
231
231
  args: selection.effort === undefined ? args : [...args, "--effort", selection.effort]
232
232
  };
233
233
  }
234
+ if (resolved !== undefined)
235
+ io.write(renderAgentConfigurationResolutionNotice(resolved));
234
236
  if (selected.value === "permission-strategy") {
235
237
  const strategy = await promptAgentFieldValue(selected, io);
236
238
  if (strategy === undefined || strategy.length === 0)
@@ -407,6 +409,8 @@ async function updateAgentBindingSettings(args, role, binding, ports, io) {
407
409
  ]
408
410
  };
409
411
  }
412
+ if (resolved !== undefined)
413
+ io.write(renderAgentConfigurationResolutionNotice(resolved));
410
414
  if (selectedField.value === "permission-strategy") {
411
415
  const strategy = await promptAgentFieldValue(selectedField, io);
412
416
  if (strategy === undefined || strategy.length === 0)
@@ -446,23 +450,17 @@ function agentFields(binding, catalog) {
446
450
  return [
447
451
  agentField("model", "Model", config.model, "--model", ["--clear-model"]),
448
452
  agentField("effort", "Effort", config.effort, "--effort", ["--clear-effort"]),
449
- agentField("permission-strategy", "Permission strategy", permission.strategy, "--permission-strategy", ["--permission-strategy", "default"], catalogChoices(catalog, "permission.strategy", ["default", "bypass", "configured"])),
453
+ agentField("permission-strategy", "Permission strategy", permission.strategy, "--permission-strategy", ["--permission-strategy", "default"], catalogField(catalog, "permission.strategy")),
450
454
  ...(binding.adapterId === "codex" && permission.strategy === "configured" ? [
451
- agentField("sandbox", "Sandbox", permission.sandbox, "--sandbox", ["--permission-strategy", "default"], catalogChoices(catalog, "permission.sandbox", [
452
- "read-only", "workspace-write", "danger-full-access"
453
- ])),
454
- agentField("approval", "Approval", permission.approval, "--approval", ["--permission-strategy", "default"], catalogChoices(catalog, "permission.approval", [
455
- "untrusted", "on-request", "never"
456
- ]))
455
+ agentField("sandbox", "Sandbox", permission.sandbox, "--sandbox", ["--permission-strategy", "default"], catalogField(catalog, "permission.sandbox")),
456
+ agentField("approval", "Approval", permission.approval, "--approval", ["--permission-strategy", "default"], catalogField(catalog, "permission.approval"))
457
457
  ] : []),
458
458
  ...(binding.adapterId === "codex" ? [
459
- agentField("search", "Web search", config.search, "--search", ["--clear-search"], [
460
- ...catalogChoices(catalog, "search", ["true"])
461
- ])
459
+ agentField("search", "Web search", config.search, "--search", ["--clear-search"], catalogField(catalog, "search"))
462
460
  ] : []),
463
461
  ...((binding.adapterId === "claude" || binding.adapterId === "acp")
464
462
  && permission.strategy === "configured" ? [
465
- agentField("permission-mode", "Permission mode", permission.mode, "--permission-mode", ["--permission-strategy", "default"], catalogChoices(catalog, "permission.mode"))
463
+ agentField("permission-mode", "Permission mode", permission.mode, "--permission-mode", ["--permission-strategy", "default"], catalogField(catalog, "permission.mode"))
466
464
  ] : [])
467
465
  ];
468
466
  }
@@ -479,9 +477,8 @@ async function loadAgentCatalog(ports, binding, cwd) {
479
477
  ? value
480
478
  : undefined;
481
479
  }
482
- function catalogChoices(catalog, key, fallback = []) {
483
- const field = catalog?.fields.find((candidate) => candidate.key === key);
484
- return field === undefined ? [...fallback] : field.choices.map(({ value }) => value);
480
+ function catalogField(catalog, key) {
481
+ return catalog?.fields.find((candidate) => candidate.key === key);
485
482
  }
486
483
  function appendModelEffortPatch(args, selection, update) {
487
484
  return [
@@ -494,8 +491,19 @@ function appendModelEffortPatch(args, selection, update) {
494
491
  : ["--effort", selection.effort])
495
492
  ];
496
493
  }
497
- function agentField(value, label, current, set, clear, choices) {
498
- return { value, label, current: display(current), set, clear, ...(choices === undefined ? {} : { choices }) };
494
+ function agentField(value, label, current, set, clear, capability) {
495
+ const reported = capability?.available === false ? [] : capability?.choices.map(choice => choice.value) ?? [];
496
+ const retained = (typeof current === "string" || typeof current === "boolean")
497
+ && !reported.includes(String(current)) ? [String(current)] : [];
498
+ return {
499
+ value, label, current: display(current), set, clear,
500
+ choices: [...reported, ...retained], allowCustom: capability?.allowCustom === true,
501
+ reason: [
502
+ capability?.reason ?? (capability === undefined
503
+ ? "No catalog field was reported; no choices inferred." : ""),
504
+ ...(retained.length === 0 ? [] : ["Current value is retained but not reported by this catalog."])
505
+ ].filter(Boolean).join(" ")
506
+ };
499
507
  }
500
508
  async function configuredPermissionArgs(binding, catalog, strategy, io) {
501
509
  if (strategy !== "configured")
@@ -509,12 +517,8 @@ async function configuredPermissionArgs(binding, catalog, strategy, io) {
509
517
  return undefined;
510
518
  const option = field === "sandbox" ? "--sandbox" : "--approval";
511
519
  const value = await promptAgentFieldValue(agentField(field, field === "sandbox" ? "Sandbox" : "Approval", undefined, option, ["--permission-strategy", "default"], field === "sandbox"
512
- ? catalogChoices(catalog, "permission.sandbox", [
513
- "read-only", "workspace-write", "danger-full-access"
514
- ])
515
- : catalogChoices(catalog, "permission.approval", [
516
- "untrusted", "on-request", "never"
517
- ])), io);
520
+ ? catalogField(catalog, "permission.sandbox")
521
+ : catalogField(catalog, "permission.approval")), io);
518
522
  return value === undefined
519
523
  ? undefined
520
524
  : ["--permission-strategy", "configured", option, value];
@@ -523,9 +527,9 @@ async function configuredPermissionArgs(binding, catalog, strategy, io) {
523
527
  // An ACP Session's configured permission is one mode the Agent enumerated,
524
528
  // so there is nothing to choose between: ask for the mode directly rather
525
529
  // than offering tool rules the protocol has no place for. The choices come
526
- // from the capability catalog, so a Session that offers no modes presents
527
- // none instead of inviting a value the Agent would reject.
528
- const value = await promptAgentFieldValue(agentField("permission-mode", "Permission mode", undefined, "--permission-mode", ["--permission-strategy", "default"], catalogChoices(catalog, "permission.mode")), io);
530
+ // from the catalog. An explicitly open field permits a custom mode before
531
+ // Session creation; it is checked against the native Session at launch.
532
+ const value = await promptAgentFieldValue(agentField("permission-mode", "Permission mode", undefined, "--permission-mode", ["--permission-strategy", "default"], catalogField(catalog, "permission.mode")), io);
529
533
  return value === undefined || value.length === 0
530
534
  ? undefined
531
535
  : ["--permission-strategy", "configured", "--permission-mode", value];
@@ -541,16 +545,23 @@ async function configuredPermissionArgs(binding, catalog, strategy, io) {
541
545
  ? "--permission-mode"
542
546
  : field === "allowed" ? "--allowed-tool" : "--disallowed-tool";
543
547
  const value = field === "mode"
544
- ? await promptAgentFieldValue(agentField("permission-mode", "Permission mode", undefined, option, ["--permission-strategy", "default"], catalogChoices(catalog, "permission.mode")), io)
548
+ ? await promptAgentFieldValue(agentField("permission-mode", "Permission mode", undefined, option, ["--permission-strategy", "default"], catalogField(catalog, "permission.mode")), io)
545
549
  : (await io.question(`${field === "allowed" ? "Allowed" : "Disallowed"} tool: `))?.trim();
546
550
  return value === undefined || value.length === 0
547
551
  ? undefined
548
552
  : ["--permission-strategy", "configured", option, value];
549
553
  }
550
554
  async function promptAgentFieldValue(field, io) {
551
- return field.choices === undefined
552
- ? (await io.question(`${field.label}: `))?.trim()
553
- : choose(`Set ${field.label}`, field.choices.map((choice) => ({ value: choice, cells: [choice] })), [TEXT_COLUMN], io, field.choices[0], "value");
555
+ if (field.reason)
556
+ io.write(`${field.label}: ${field.reason}\n`);
557
+ if (field.choices === undefined)
558
+ return (await io.question(`${field.label}: `))?.trim();
559
+ const custom = "\0yui:custom";
560
+ const selected = await choose(`Set ${field.label}`, [
561
+ ...field.choices.map(choice => ({ value: choice, cells: [choice] })),
562
+ ...(field.allowCustom ? [{ value: custom, cells: ["Custom value (native acceptance unverified)"] }] : [])
563
+ ], [TEXT_COLUMN], io, field.choices.includes(field.current) ? field.current : field.choices[0], "value");
564
+ return selected === custom ? (await io.question(`Custom ${field.label}: `))?.trim() : selected;
554
565
  }
555
566
  async function selectActiveAgent(role, ports, io) {
556
567
  const configured = await configuredAgents(ports);
@@ -1,6 +1,8 @@
1
1
  import { resolveYuiHome } from "../storage/taskStore.js";
2
2
  import { createUpdatePorts } from "./updatePorts.js";
3
3
  import { runUpdate } from "./updateOrchestrator.js";
4
+ import { isConcreteVersion } from "../domain/validation.js";
5
+ import { usageError } from "../errors/cliError.js";
4
6
  /**
5
7
  * Run `yui update` as a side-by-side, recoverable orchestration.
6
8
  *
@@ -11,15 +13,24 @@ import { runUpdate } from "./updateOrchestrator.js";
11
13
  *
12
14
  * Returns a process exit code: 0 on success or already-current, 5 on abort.
13
15
  */
14
- export function runUpdateCommand(environment = process.env, spawn, write = (text) => process.stdout.write(text), ports) {
16
+ export function runUpdateCommand(args = [], environment = process.env, spawn, write = (text) => process.stdout.write(text), ports) {
17
+ const version = parseUpdateVersion(args);
15
18
  const home = resolveYuiHome(environment);
16
19
  const resolvedPorts = ports ?? createUpdatePorts(environment, spawn);
17
- const result = runUpdate(resolvedPorts, { home });
20
+ const result = runUpdate(resolvedPorts, { home, version });
18
21
  write(`${renderUpdateResult(result)}\n`);
19
22
  if (result.outcome === "aborted")
20
23
  return 5;
21
24
  return 0;
22
25
  }
26
+ function parseUpdateVersion(args) {
27
+ if (args.length === 0)
28
+ return undefined;
29
+ if (args.length === 2 && args[0] === "--version" && isConcreteVersion(args[1])) {
30
+ return args[1].trim();
31
+ }
32
+ throw usageError("Update usage: yui update [--version <exact-version>]");
33
+ }
23
34
  /** Render an {@link UpdateResult} as concise, CLI-style text. */
24
35
  export function renderUpdateResult(result) {
25
36
  const rendered = (() => {
@@ -33,14 +44,18 @@ export function renderUpdateResult(result) {
33
44
  case "aborted":
34
45
  return [
35
46
  `Update aborted during ${result.phase}: ${result.message}`,
36
- result.phase === "migrate-storage"
37
- || (result.phase === "post-verify" && result.recoverable)
38
- ? "The target binary is installed; the Home remains quiesced pending successful verification."
39
- : result.recoverable
40
- ? "The current install and Home remain usable."
41
- : result.phase === "activate-binary"
42
- ? "The Home was unchanged, but binary health is unknown; do not assume the current install is usable."
43
- : "Manual recovery is required (see below).",
47
+ result.controllerOwnershipUnknown === true || result.controllerRestore?.outcome === "unknown"
48
+ || result.controllerReconciliation?.attempts.some(attempt => attempt.outcome === "unknown")
49
+ || result.controllerReconciliation?.observationError !== undefined
50
+ ? "Controller resource effects are uncertain; do not assume the Home is quiesced or usable."
51
+ : result.phase === "migrate-storage"
52
+ || (result.phase === "post-verify" && result.recoverable)
53
+ ? "The target binary is installed; the Home remains quiesced pending successful verification."
54
+ : result.recoverable
55
+ ? "The current install and Home remain usable."
56
+ : result.phase === "activate-binary"
57
+ ? "The Home was unchanged, but binary health is unknown; do not assume the current install is usable."
58
+ : "Manual recovery is required (see below).",
44
59
  ...(result.backupPath === undefined
45
60
  ? []
46
61
  : [`Storage backup: ${result.backupPath}`]),
@@ -48,7 +63,14 @@ export function renderUpdateResult(result) {
48
63
  ].join("\n");
49
64
  }
50
65
  })();
51
- return result.cleanupWarning === undefined
52
- ? rendered
53
- : `${rendered}\nWarning: ${result.cleanupWarning}`;
66
+ return [
67
+ rendered,
68
+ ...(result.controllerReconciliation === undefined ? [] : [
69
+ `Controller reconciliation: ${JSON.stringify(result.controllerReconciliation)}`
70
+ ]),
71
+ ...(result.controllerRestore === undefined ? [] : [
72
+ `Controller restore: ${JSON.stringify(result.controllerRestore)}`
73
+ ]),
74
+ ...(result.cleanupWarning === undefined ? [] : [`Warning: ${result.cleanupWarning}`])
75
+ ].join("\n");
54
76
  }