@zq-silk/yui 0.15.11 → 0.15.12

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 (101) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/cli/commandCatalog.js +38 -10
  6. package/dist/cli/interactionPolicy.js +4 -0
  7. package/dist/cli/managedDiagnostics.js +1 -1
  8. package/dist/cli.js +46 -15
  9. package/dist/commands/executionAuditCommands.js +10 -0
  10. package/dist/commands/globalRoleCommands.js +27 -2
  11. package/dist/commands/projectCommands.js +44 -15
  12. package/dist/commands/taskCommands.js +95 -33
  13. package/dist/commands/taskIntegrationCommands.js +3 -1
  14. package/dist/commands/taskOverviewCommand.js +4 -3
  15. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  16. package/dist/commands/taskPublicationCommands.js +11 -2
  17. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  18. package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
  19. package/dist/context/runContextPack.js +3 -0
  20. package/dist/context/taskCatalog.js +187 -0
  21. package/dist/context/taskContext.js +17 -4
  22. package/dist/controller/controller.js +11 -2
  23. package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
  24. package/dist/controller/globalInputDelivery.js +13 -0
  25. package/dist/controller/providerRetryAdmission.js +100 -0
  26. package/dist/controller/providerRetryDelivery.js +218 -0
  27. package/dist/controller/runtime.js +36 -1
  28. package/dist/integration/gitIntegrationService.js +19 -6
  29. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  30. package/dist/message/globalProviderRetry.js +15 -0
  31. package/dist/observability/executionAudit.js +19 -0
  32. package/dist/repository/gitWorkspace.js +371 -105
  33. package/dist/repository/projectMaintenanceLock.js +75 -18
  34. package/dist/repository/taskWorkspaceCoordinator.js +71 -124
  35. package/dist/repository/taskWorkspacePreparer.js +86 -24
  36. package/dist/repository/workspaceCleanupInspection.js +187 -0
  37. package/dist/runtime/agentError.js +5 -3
  38. package/dist/runtime/agentHost.js +28 -11
  39. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  40. package/dist/runtime/codexAppServerRuntime.js +34 -3
  41. package/dist/runtime/providerControl.js +5 -1
  42. package/dist/runtime/providerRetry.js +198 -0
  43. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  44. package/dist/runtime/sessionTokenMetrics.js +15 -5
  45. package/dist/runtime/structuredProviderHost.js +6 -2
  46. package/dist/runtime/taskUsageMetrics.js +275 -0
  47. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  48. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  49. package/dist/scheduler/taskExecutionProjection.js +26 -5
  50. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  51. package/dist/storage/sqliteSchema.js +31 -0
  52. package/dist/storage/sqliteStore.js +17 -0
  53. package/dist/storage/storageVersions.js +1 -1
  54. package/dist/storage/storeRpc.js +1 -0
  55. package/dist/storage/taskCatalog.js +123 -0
  56. package/dist/storage/taskStore.js +2 -0
  57. package/dist/task/archiveDiagnostics.js +1 -0
  58. package/dist/task/archivePreflight.js +124 -0
  59. package/dist/task/publicationAdoption.js +56 -0
  60. package/dist/task/publicationReference.js +10 -0
  61. package/dist/task/remoteDelivery.js +31 -16
  62. package/dist/web/assets/client/app.js +92 -17
  63. package/dist/web/assets/client/components.js +55 -13
  64. package/dist/web/assets/client/i18n.js +72 -4
  65. package/dist/web/assets/client/taskSurface.js +2 -1
  66. package/dist/web/assets/client/view.js +35 -7
  67. package/dist/web/assets/shell.js +6 -0
  68. package/dist/web/assets/styles/layout.js +7 -0
  69. package/dist/web/webServer.js +14 -3
  70. package/dist/web/webSnapshot.js +12 -3
  71. package/dist/workspace/cleanupInspection.js +63 -0
  72. package/dist/workspace/workItemChangeSetManager.js +110 -50
  73. package/docs/agent-result-consumption.md +4 -0
  74. package/docs/agent-result-consumption.zh-CN.md +3 -0
  75. package/docs/agent-runtime-drivers.md +7 -0
  76. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  77. package/docs/architecture/README.md +2 -0
  78. package/docs/architecture/README.zh-CN.md +3 -1
  79. package/docs/architecture/capabilities-and-resources.md +30 -5
  80. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  81. package/docs/managed-turn-and-session-runtime.md +47 -0
  82. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  83. package/docs/observability/README.md +62 -0
  84. package/docs/observability/README.zh-CN.md +47 -0
  85. package/docs/project-refresh.md +77 -0
  86. package/docs/project-refresh.zh-CN.md +59 -0
  87. package/docs/provider-retry.md +70 -0
  88. package/docs/task-delivery.md +133 -13
  89. package/docs/task-delivery.zh-CN.md +99 -10
  90. package/docs/task-discovery.md +102 -0
  91. package/docs/task-discovery.zh-CN.md +86 -0
  92. package/docs/testing/verification-levels.md +16 -0
  93. package/docs/testing/verification-levels.zh-CN.md +13 -1
  94. package/i18n/README.zh-CN.md +13 -7
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +3 -2
  97. package/skills/yui-operator/SKILL.md +13 -2
  98. package/skills/yui-reviewer/SKILL.md +4 -0
  99. package/skills/yui-runtime/SKILL.md +17 -2
  100. package/skills/yui-runtime/references/publication.md +26 -4
  101. package/skills/yui-runtime/references/recovery.md +24 -0
package/ARCHITECTURE.md CHANGED
@@ -169,8 +169,9 @@ Leader management is limited to its Task; executable code still requires exact
169
169
  Operator-issued grants. Plugin validation is not a security certification.
170
170
 
171
171
  CLI contributions and controlled Web panels are projections of the Registry.
172
- Web is loopback-only and Controller-owned; browser credentials do not become
173
- Operator authority. See [Plugin SDK](docs/plugin-sdk.md) and
172
+ Web is loopback-only and Controller-owned. Its authenticated local-user controls
173
+ share CLI domain operations; query panels remain read-only and cannot borrow
174
+ that user authority for mutations or plugin management. See [Plugin SDK](docs/plugin-sdk.md) and
174
175
  [capabilities and resources](docs/architecture/capabilities-and-resources.md).
175
176
 
176
177
  ## Persistence, completion and operation
@@ -181,8 +182,11 @@ Homes; malformed state is diagnosed, not automatically repaired.
181
182
 
182
183
  Completion freezes the delivery result and checks applicable acceptance,
183
184
  integration and review contracts. It is distinct from publication, verified
184
- remote merge, physical quiescence and archive. Archive requires settled work
185
- and clean removable managed resources, preserves Task history, and cannot reopen.
185
+ remote merge, physical quiescence and archive. Ordinary archive requires settled
186
+ work and clean removable managed resources. Explicitly authorized force archive
187
+ can retain unresolved evidence and unsafe resources without claiming delivery or
188
+ quiescence. Both preserve Task history and cannot reopen.
189
+ See [Task delivery and archive](docs/task-delivery.md#archive).
186
190
 
187
191
  Runtime health and cost are observations, not semantic verdicts. The Agent reads
188
192
  exact faults and current intent to choose retry, repair or abandonment. Yui does
@@ -132,7 +132,8 @@ Registry 暴露 context、消息、artifact、环境、插件以及部分 Task/J
132
132
  由 Operator 签发的 grant。插件验证不是安全认证。
133
133
 
134
134
  CLI 贡献和受控的 Web 面板是 Registry 的投影。Web 仅本地回环、由 Controller 拥有;浏览器
135
- 凭据不会变成 Operator 权限。参见[插件 SDK](docs/plugin-sdk.zh-CN.md)和
135
+ 凭据认证本地用户控制,这些控制复用 CLI 领域操作;查询面板仍只读,不能借用该用户权限
136
+ 执行修改或插件管理。参见[插件 SDK](docs/plugin-sdk.zh-CN.md)和
136
137
  [能力与资源](docs/architecture/capabilities-and-resources.zh-CN.md)。
137
138
 
138
139
  ## 持久化、完成与运维
@@ -141,7 +142,9 @@ Home 有一条只追加的存储迁移链。普通运行时只接受当前记录
141
142
  有效 Home;畸形状态被诊断,而不是自动修复。
142
143
 
143
144
  完成会冻结交付结果,并检查适用的验收、集成和审查合同。它区别于发布、已验证的远程合并、
144
- 物理静止和归档。归档要求工作已了结、受管资源干净可移除,保留 Task 历史,且不可重开。
145
+ 物理静止和归档。普通归档要求工作已了结、受管资源干净可移除。明确授权的 force 归档
146
+ 可以保留未解决证据与不安全资源,但不宣称交付或物理静止。两者都保留 Task 历史且不可
147
+ 重开。参见[Task 交付与归档](docs/task-delivery.zh-CN.md#归档)。
145
148
 
146
149
  运行时健康度和成本是观察,而不是语义判定。Agent 读取精确的故障和当前意图,以选择重试、
147
150
  修复或放弃。Yui 不会自动创建救援 Worker 或选择另一个模型。
package/README.md CHANGED
@@ -30,7 +30,7 @@ not from terminal windows you juggle or details you have to remember.
30
30
  - **Bring your own Agent** — Codex CLI, Claude Code CLI and ACP peers run behind
31
31
  one boundary and stay replaceable without losing the Task.
32
32
  - **Local-first and private** — everything runs on your machine for one trusted
33
- user; the Web view is loopback and read-only.
33
+ user; Web is loopback-only, with read-only views and authenticated user controls.
34
34
  - **Isolated by default** — repository work happens in managed Git worktrees;
35
35
  the stable checkout stays read-only.
36
36
 
@@ -137,7 +137,11 @@ when necessary. A failed process does not erase the Task, and an uncertain
137
137
  submission is not silently repeated.
138
138
 
139
139
  For a visual overview, run `yui web` in another terminal. The local Web view
140
- shows the same tasks and pending questions; it is not a separate task system.
140
+ shows the same tasks and pending questions and lets you send messages, answer
141
+ questions and explicitly queue, steer or interrupt Task input. These authenticated
142
+ Task controls use the same operations as the CLI; Web is not a separate task system.
143
+ See [Web permissions](docs/architecture/capabilities-and-resources.md#cli-and-web)
144
+ and [input timing](docs/managed-turn-and-session-runtime.md#input-timing-queue-steer-and-interrupt).
141
145
 
142
146
  ## Architecture
143
147
 
@@ -243,7 +247,7 @@ the Controller are what touch the store:
243
247
  it moves work and records facts — it never judges an answer
244
248
 
245
249
  Agents work in Projects: read-only checkout + isolated worktrees.
246
- Web view (yui web): a loopback, read-only projection of the store.
250
+ Web (yui web): loopback views + authenticated user controls.
247
251
  ```
248
252
 
249
253
  ### Layered design
@@ -253,7 +257,7 @@ never a fixed workflow:
253
257
 
254
258
  ```text
255
259
  Experience — how you interact
256
- CLI (Operator) · Web (loopback, read-only) · native Agent sessions
260
+ CLI (Operator) · Web (loopback, authenticated) · native Agent sessions
257
261
  collect input · show facts · confirm actions · invoke capabilities
258
262
 
259
263
  Intelligence — who decides
@@ -290,9 +294,13 @@ extra states:
290
294
  WorkItem open ─▶ accepted ─▶ retired
291
295
 
292
296
  Draft holds planning only; activation adopts a delivery workspace.
293
- Archive needs settled work and clean worktrees; it cannot reopen.
297
+ Ordinary archive needs settled work and clean worktrees; it cannot reopen.
294
298
  ```
295
299
 
300
+ Explicitly authorized force archive can retain unresolved evidence and unsafe
301
+ resources; it does not prove delivery or permission to delete them.
302
+ See the [archive contract](docs/task-delivery.md#archive).
303
+
296
304
  ## Design principles
297
305
 
298
306
  ### Agents make decisions; Yui makes work durable
@@ -2,6 +2,7 @@ import { validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js"
2
2
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
3
3
  import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
4
4
  import { createRunInput, createRunInputEnvelope, validateRunInput } from "../context/runInputContract.js";
5
+ import { providerRetryProjection } from "../runtime/providerRetry.js";
5
6
  import { boundedRunFailureDiagnostic, MAX_RUN_FAILURE_DIAGNOSTIC_BYTES, MAX_RUN_RESULT_OUTPUT_BYTES } from "../domain/agentResultTransport.js";
6
7
  export { boundedRunFailureDiagnostic, MAX_RUN_FAILURE_DIAGNOSTIC_BYTES, MAX_RUN_RESULT_OUTPUT_BYTES, transportAgentResult } from "../domain/agentResultTransport.js";
7
8
  /** Record lifecycle and observed delivery are separate read-only facts. */
@@ -17,6 +18,8 @@ export function runExecutionObservation(run, binding, events = []) {
17
18
  return {
18
19
  recordStatus: run.status,
19
20
  delivery,
21
+ retry: binding?.retry?.previousRunId === run.id || binding?.retry?.successorRunId === run.id
22
+ ? providerRetryProjection(binding) : null,
20
23
  ...(current === undefined ? {} : {
21
24
  attemptId: current.attemptId, observedAt: current.updatedAt,
22
25
  ...(current.nativeTurnId === undefined ? {} : { nativeTurnId: current.nativeTurnId })
@@ -215,6 +215,11 @@ const globalSessionChildren = [
215
215
  summary: "Reconcile durable Session owners with native sessions.",
216
216
  usage: "yui session reconcile [--report] [--cleanup]",
217
217
  options: ["--report", "--cleanup"]
218
+ },
219
+ {
220
+ name: "retry",
221
+ summary: "Inspect or control bounded Provider recovery for this Global Session.",
222
+ usage: "yui session retry <role> [show|cancel|disable|enable]"
218
223
  }
219
224
  ];
220
225
  const profileChildren = [
@@ -373,9 +378,9 @@ const taskChildren = [
373
378
  },
374
379
  {
375
380
  name: "list",
376
- summary: "List unarchived Task overviews.",
377
- usage: "yui task list [--all] [--verbose]",
378
- options: ["--all", "--verbose"]
381
+ summary: "List Task overviews or a bounded discovery catalog.",
382
+ usage: "yui task list [--all] [--verbose] | --view compact [--all] [--status <status>] [--project <id>] [--search <text>] [--attention <category>] [--limit <1..100>] [--cursor <cursor>]",
383
+ options: ["--all", "--verbose", "--view", "--status", "--project", "--search", "--attention", "--limit", "--cursor"]
379
384
  },
380
385
  { name: "show", summary: "Show a Task.", usage: "yui task show <id>" },
381
386
  {
@@ -406,6 +411,12 @@ const taskChildren = [
406
411
  usage: "yui task remote-delivery <task> [--json]",
407
412
  options: ["--json"]
408
413
  },
414
+ {
415
+ name: "archive-preflight",
416
+ summary: "Inspect current archive and exact-owner cleanup blockers without changing state or authorizing removal.",
417
+ usage: "yui task archive-preflight <id> (--integrated|--abandon) [--force] [--json]",
418
+ options: ["--integrated", "--abandon", "--force", "--json"]
419
+ },
409
420
  {
410
421
  name: "archive",
411
422
  summary: "Archive a terminal Task; explicit --force commits despite delivery/cleanup warnings, retaining unsafe resources.",
@@ -606,17 +617,29 @@ const taskChildren = [
606
617
  {
607
618
  name: "publication",
608
619
  summary: "Create or update external PR/MR publication evidence for a Task.",
609
- sections: [{ id: "manage", title: "Commands", entries: ["upsert", "verify", "list", "show"] }],
620
+ sections: [{ id: "manage", title: "Commands", entries: ["upsert", "diff", "adopt", "verify", "list", "show"] }],
610
621
  children: [
611
622
  {
612
623
  name: "upsert",
613
624
  summary: "Create or immutably update an external PR/MR and its publication state.",
614
- usage: "yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--merged-at <iso-timestamp>]",
615
- options: ["--project", "--provider", "--repository", "--kind", "--id", "--url", "--title", "--source-branch", "--target-branch", "--local-commit", "--remote-commit", "--state", "--reported", "--verified", "--evidence", "--merged-at"]
625
+ usage: "yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--head-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--merged-at <iso-timestamp>]",
626
+ options: ["--project", "--provider", "--repository", "--kind", "--id", "--url", "--title", "--source-branch", "--target-branch", "--local-commit", "--head-commit", "--remote-commit", "--state", "--reported", "--verified", "--evidence", "--merged-at"]
627
+ },
628
+ {
629
+ name: "diff",
630
+ summary: "Read a completed Task's fixed acceptance-to-publication candidate diff using local Git only.",
631
+ usage: "yui task publication diff <task>/<publication> [--integration <id>]",
632
+ options: ["--integration"]
633
+ },
634
+ {
635
+ name: "adopt",
636
+ summary: "Explicitly accept the reviewed publication candidate as covering the original completion.",
637
+ usage: "yui task publication adopt <task>/<publication> --reviewed-diff <sha256> --acceptance <text> [--integration <id>]",
638
+ options: ["--reviewed-diff", "--acceptance", "--integration"]
616
639
  },
617
640
  {
618
641
  name: "verify",
619
- summary: "Verify one current GitHub PR against the exact Task delivery head through gh.",
642
+ summary: "Observe the current PR/MR head and merge through its provider; record verification separately from Task coverage.",
620
643
  usage: "yui task publication verify (<task>/<publication-id> | <task> <publication-id>)"
621
644
  },
622
645
  {
@@ -668,8 +691,13 @@ const taskChildren = [
668
691
  name: "session",
669
692
  summary: "Inspect, stop or explicitly select a new Task Role Session.",
670
693
  executable: true,
671
- sections: [{ id: "manage", title: "Commands", entries: ["inspect", "stop", "new"] }],
694
+ sections: [{ id: "manage", title: "Commands", entries: ["inspect", "retry", "stop", "new"] }],
672
695
  children: [
696
+ {
697
+ name: "retry",
698
+ summary: "Inspect or control Provider recovery without stopping an admitted Turn.",
699
+ usage: "yui task role session retry <task> <role> [show|cancel|disable|enable]"
700
+ },
673
701
  {
674
702
  name: "inspect",
675
703
  summary: "Read the current Session, Host process, and AgentRun facts.",
@@ -1481,7 +1509,7 @@ export const ROOT_COMMAND = buildNode({
1481
1509
  sections: [
1482
1510
  { id: "global", title: "Global Role sessions", entries: ["context", "enter", "record", "replace"] },
1483
1511
  { id: "maintenance", title: "Maintenance", entries: ["stop"] },
1484
- { id: "recovery", title: "Recovery", entries: ["reconcile"] }
1512
+ { id: "recovery", title: "Recovery", entries: ["reconcile", "retry"] }
1485
1513
  ],
1486
1514
  children: globalSessionChildren
1487
1515
  },
@@ -1489,7 +1517,7 @@ export const ROOT_COMMAND = buildNode({
1489
1517
  name: "task",
1490
1518
  summary: "Manage Tasks, WorkItems, AgentRuns, and integration.",
1491
1519
  sections: [
1492
- { id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "base", "update", "activate", "activation", "execution", "complete", "cancel", "reopen", "retire", "list", "show", "context", "next-action", "remote-delivery", "archive", "replace", "reconcile", "upstream", "artifact"] },
1520
+ { id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "base", "update", "activate", "activation", "execution", "complete", "cancel", "reopen", "retire", "list", "show", "context", "next-action", "remote-delivery", "archive-preflight", "archive", "replace", "reconcile", "upstream", "artifact"] },
1493
1521
  { id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "publication", "work", "run", "review", "integration", "role", "overlap", "change-set"] },
1494
1522
  { id: "knowledge", title: "Task Knowledge", entries: ["brief", "decision", "milestone", "event", "continuation", "wake"] }
1495
1523
  ],
@@ -229,6 +229,10 @@ export const INTERACTION_POLICIES = Object.freeze([
229
229
  ...taskTarget("remote-delivery"),
230
230
  trailingOptions: { "--json": "flag" }
231
231
  },
232
+ {
233
+ ...taskTarget("archive-preflight"),
234
+ trailingOptions: { "--integrated": "flag", "--abandon": "flag", "--force": "flag", "--json": "flag" }
235
+ },
232
236
  {
233
237
  ...taskTarget("archive", 2, ["completed", "cancelled"]),
234
238
  trailingOptions: { "--integrated": "flag", "--abandon": "flag", "--force": "flag" },
@@ -3,7 +3,7 @@
3
3
  export function taskDiagnosticTarget(args) {
4
4
  if (args[0] !== "task")
5
5
  return undefined;
6
- if (["show", "context", "next-action"].includes(args[1] ?? "")) {
6
+ if (["show", "context", "next-action", "archive-preflight"].includes(args[1] ?? "")) {
7
7
  return args[1] === "context" && ["inspect", "delta"].includes(args[2] ?? "")
8
8
  ? args[3] : args[2];
9
9
  }
package/dist/cli.js CHANGED
@@ -42,10 +42,13 @@ import { runResourcesCommand } from "./commands/resourcesCommands.js";
42
42
  import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
43
43
  import { runProjectCommand } from "./commands/projectCommands.js";
44
44
  import { previewProfileAgentConfigurationMutation, runProfileCommand } from "./commands/profileCommands.js";
45
- import { assertWorkItemDependenciesCompletedForCommand, requireWorkItemAssignee, dispatchPreparedReviewRound, failPendingReviewRound, preserveReviewRoundWorkspace, parseTaskCompletionRequest, previewTaskRoleAgentConfigurationMutation, preflightTaskCompletion, runTaskCommand, planReplicatedWorkItemLanes, validateTaskArchiveRequest } from "./commands/taskCommands.js";
45
+ import { assertWorkItemDependenciesCompletedForCommand, requireWorkItemAssignee, dispatchPreparedReviewRound, failPendingReviewRound, preserveReviewRoundWorkspace, parseTaskCompletionRequest, previewTaskRoleAgentConfigurationMutation, preflightTaskCompletion, runTaskCommand, planReplicatedWorkItemLanes, validateTaskArchiveRequest, parseTaskArchiveArguments } from "./commands/taskCommands.js";
46
46
  import { assertTaskRemoteDeliveryIntegrated, createTaskRemoteDeliveryProof } from "./commands/taskRemoteDeliveryCommand.js";
47
47
  import { renderArchiveDiagnostics, taskArchiveDiagnostics } from "./task/archiveDiagnostics.js";
48
+ import { inspectTaskArchive, renderTaskArchivePreflight } from "./task/archivePreflight.js";
49
+ import { CleanupInspectionError } from "./workspace/cleanupInspection.js";
48
50
  import { runTaskPublicationVerifyCommand } from "./commands/taskPublicationVerifyCommand.js";
51
+ import { runTaskPublicationAdoptCommand } from "./commands/taskPublicationAdoptCommand.js";
49
52
  import { createGitHubCliPublicationVerifier } from "./external/githubPublicationVerifier.js";
50
53
  import { createGitLabCliPublicationVerifier } from "./external/gitlabPublicationVerifier.js";
51
54
  import { taskLocalActor, assertTaskDeliveryAuthority } from "./commands/taskActor.js";
@@ -115,6 +118,9 @@ const rawArgs = [...taskFinalReviewInvocation.args];
115
118
  const jsonOutput = rawArgs.includes("--json");
116
119
  const args = normalizeAliases(jsonOutput ? rawArgs.filter((argument) => argument !== "--json") : rawArgs);
117
120
  void main().catch((error) => {
121
+ if (error instanceof CleanupInspectionError) {
122
+ error = cleanupCliError(error, error.checks[0]?.resource ?? "workspace");
123
+ }
118
124
  if (error instanceof CliError) {
119
125
  const rendered = jsonOutput
120
126
  ? JSON.stringify({ ok: false, code: error.code, message: error.message, details: error.details })
@@ -913,6 +919,15 @@ export async function main() {
913
919
  return;
914
920
  }
915
921
  if (resolved[0] === "task") {
922
+ if (resolved[1] === "archive-preflight") {
923
+ const request = parseTaskArchiveArguments(resolved.slice(2), "archive-preflight");
924
+ if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== request.taskId) {
925
+ throw usageError("Archive inspection must remain within this Session's Task.");
926
+ }
927
+ const data = await inspectTaskArchive(workspaceCoordinator, request);
928
+ emit(renderTaskArchivePreflight(data), false, data);
929
+ return;
930
+ }
916
931
  if (resolved[1] === "artifact") {
917
932
  // File/directory artifacts live in the Task's local Git repository, so
918
933
  // their save/read/list are asynchronous and handled here rather than in
@@ -1033,6 +1048,13 @@ export async function main() {
1033
1048
  emit(result.output, false, result.data);
1034
1049
  return;
1035
1050
  }
1051
+ if (resolved[1] === "publication" && (resolved[2] === "diff" || resolved[2] === "adopt")) {
1052
+ const result = await runTaskPublicationAdoptCommand(resolved.slice(2), store, {
1053
+ environment: process.env
1054
+ });
1055
+ emit(result.output, false, result.data);
1056
+ return;
1057
+ }
1036
1058
  if (resolved[1] === "publication" && resolved[2] === "verify") {
1037
1059
  const result = await runTaskPublicationVerifyCommand(resolved.slice(3), store, {
1038
1060
  verifiers: {
@@ -1043,12 +1065,6 @@ export async function main() {
1043
1065
  environmentPath: process.env.PATH
1044
1066
  })
1045
1067
  },
1046
- candidateForTask: async (taskId) => {
1047
- const status = store.getTask(taskId)?.status;
1048
- return status === "active" || status === "cancelled"
1049
- ? snapshotActualTaskReviewCandidate(taskId, store, workspacePreparer)
1050
- : null;
1051
- },
1052
1068
  environment: process.env
1053
1069
  });
1054
1070
  emit(result.output, false, result.data);
@@ -1154,7 +1170,7 @@ export async function main() {
1154
1170
  await new WorkItemChangeSetManager(store).assertIntegrated(reference.taskId, reference.localId);
1155
1171
  }
1156
1172
  catch (error) {
1157
- throw usageError(error instanceof Error ? error.message : String(error));
1173
+ throw cleanupCliError(error, `work-item:${qualified}`);
1158
1174
  }
1159
1175
  }
1160
1176
  let removal;
@@ -1245,15 +1261,15 @@ export async function main() {
1245
1261
  await new WorkItemChangeSetManager(store).assertIntegrated(task.id, item.id);
1246
1262
  }
1247
1263
  catch (error) {
1248
- throw usageError(error instanceof Error ? error.message : String(error));
1264
+ throw cleanupCliError(error, `work-item:${task.id}/${item.id}`);
1249
1265
  }
1250
1266
  }
1251
1267
  const cleanup = await workspaceCoordinator.cleanupTaskForArchive(task.id, disposition);
1252
1268
  if (cleanup.status === "retained-dirty") {
1253
- throw usageError(cleanup.error ?? `Task ${task.id} has dirty managed worktrees and remains terminal.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "dirty-worktree", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true));
1269
+ throw usageError(cleanup.error ?? `Task ${task.id} has dirty managed worktrees and remains terminal.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "dirty-worktree", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true, cleanup.checks));
1254
1270
  }
1255
1271
  if (cleanup.status === "failed") {
1256
- throw usageError(`Task ${task.id} worktree cleanup failed: ${cleanup.error ?? "unknown error"}.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "cleanup-failed", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true));
1272
+ throw usageError(`Task ${task.id} worktree cleanup failed: ${cleanup.error ?? "unknown error"}.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "cleanup-failed", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true, cleanup.checks));
1257
1273
  }
1258
1274
  }
1259
1275
  }
@@ -1953,17 +1969,21 @@ function assertManagedSessionManifest(home, scope) {
1953
1969
  return manifest;
1954
1970
  }
1955
1971
  function cleanupCliError(error, fallbackResource) {
1972
+ if (error instanceof CleanupInspectionError) {
1973
+ return usageError(error.message, undefined, cleanupBlockedDetails(error.checks[0]?.reason ?? "cleanup-failed", fallbackResource, true, error.checks));
1974
+ }
1956
1975
  if (error instanceof WorkspaceCleanupBlockedError) {
1957
1976
  return usageError(error.message, undefined, cleanupBlockedDetails(error.reason, error.resource, error.retryable));
1958
1977
  }
1959
1978
  return new CliError("RUNTIME_ERROR", error instanceof Error ? error.message : String(error), undefined, cleanupBlockedDetails("cleanup-failed", fallbackResource, true));
1960
1979
  }
1961
- function cleanupBlockedDetails(reason, resource, retryable) {
1980
+ function cleanupBlockedDetails(reason, resource, retryable, checks) {
1962
1981
  return {
1963
1982
  status: "blocked",
1964
1983
  blockedBy: [{ resource, reason, retryable }],
1965
1984
  remainingResources: [resource],
1966
- retryable
1985
+ retryable,
1986
+ ...(checks === undefined ? {} : { checks })
1967
1987
  };
1968
1988
  }
1969
1989
  function cliWorkItemReference(value, environment) {
@@ -2042,9 +2062,20 @@ async function prepareExecutionLaneWorkspacesForCommand(args, store, preparer, e
2042
2062
  throw usageError(`${item.taskId}/${roleName} already has an active turn.`);
2043
2063
  }
2044
2064
  }
2045
- const held = preparer.acquireTaskProjectMaintenanceLocks(item.taskId);
2065
+ const held = await preparer.acquireTaskProjectMaintenanceLocks(item.taskId);
2046
2066
  const map = new Map();
2047
2067
  try {
2068
+ assertTaskDeliveryAuthority(store, environment, item.taskId);
2069
+ const currentItem = store.getWorkItem(item.taskId, item.id);
2070
+ if (currentItem?.revision !== item.revision
2071
+ || JSON.stringify(workItemDispatchLanePlan(args, store, currentItem)) !== JSON.stringify(plan)) {
2072
+ throw new Error(`Work item dispatch changed while waiting for Project maintenance: ${item.id}.`);
2073
+ }
2074
+ if (held.current.status !== "active" || held.current.executionGate.state !== "enabled"
2075
+ || plan.roles.some(roleName => store.getRole(item.taskId, roleName) === null
2076
+ || store.getActiveRun(item.taskId, roleName) !== null)) {
2077
+ throw new Error(`Task/Role dispatch state changed while waiting for Project maintenance: ${item.taskId}.`);
2078
+ }
2048
2079
  const projectPaths = new Map();
2049
2080
  for (const { projectId } of held.current.projectBindings) {
2050
2081
  const project = store.getProject(projectId);
@@ -2667,7 +2698,7 @@ function selectionCall(store, catalogs, method, params) {
2667
2698
  case "role.list": return store.listGlobalRoles();
2668
2699
  case "role.show": return store.getGlobalRole(String(params.name ?? ""));
2669
2700
  case "project.list": return callOptional(reader, "listProjects");
2670
- case "task.list": return callOptional(reader, "listTasks");
2701
+ case "task.list": return store.listTaskChoices();
2671
2702
  case "task.integration.list": return store.listIntegrationAttempts(String(params.taskId ?? ""));
2672
2703
  case "task.change-set.list": return store.listChangeSets(String(params.taskId ?? ""));
2673
2704
  case "task.role.list": return callOptional(reader, "listRoles", [params.taskId]);
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { usageError } from "../errors/cliError.js";
9
9
  import { defaultTableWidth, renderTable } from "../output/table.js";
10
+ import { formatUsageMetric } from "../runtime/taskUsageMetrics.js";
10
11
  import { runExecutionAudit } from "../observability/executionAudit.js";
11
12
  export function parseExecutionAuditOptions(args) {
12
13
  const options = {};
@@ -246,6 +247,15 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
246
247
  else {
247
248
  lines.push("", ...sectionError("orchestration", report));
248
249
  }
250
+ if (report.usage.status === "ok" && report.usage.data !== undefined) {
251
+ lines.push("", "Observed usage — Task lifetime, observed sources only (audit time window not applied):");
252
+ for (const usage of report.usage.data) {
253
+ lines.push(` ${usage.taskId}: tokens=${formatUsageMetric(usage.tokens)}; tools=${formatUsageMetric(usage.toolCalls)}; elapsed=${formatUsageMetric(usage.elapsedSeconds, "s")}; native execution sum=${formatUsageMetric(usage.executionSeconds, "s")}`);
254
+ }
255
+ }
256
+ else {
257
+ lines.push("", ...sectionError("usage", report));
258
+ }
249
259
  if (report.storage.status === "ok" && report.storage.data !== undefined) {
250
260
  const storage = report.storage.data;
251
261
  lines.push("", `Storage: backend ${storage.backend} · state.json ${formatBytes(storage.stateJsonBytes)} · yui.db ${formatBytes(storage.databaseBytes)}`
@@ -1,4 +1,6 @@
1
1
  import { roleNotFound, usageError } from "../errors/cliError.js";
2
+ import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
3
+ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
2
4
  import { createRoleSessionSet, recordRoleAgentSession } from "../executor/agentExecutor.js";
3
5
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
4
6
  import { managedGlobalRoleWorkspace } from "../storage/homeLayout.js";
@@ -14,9 +16,9 @@ import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./rol
14
16
  import { assertLiveRoleSessionAcknowledged, assertRoleRuntimeMutationAllowed, LIVE_SESSION_ACKNOWLEDGEMENT_OPTION } from "./roleRuntimeGuard.js";
15
17
  export function runGlobalRoleCommand(args, store, options = {}) {
16
18
  const [command, ...rest] = args;
17
- if (command === "message" || command === "interrupt") {
19
+ if (command === "message" || command === "interrupt" || command === "session" && rest[0] === "retry") {
18
20
  const env = options.env ?? process.env;
19
- const target = command === "message" ? rest[1] : rest[0];
21
+ const target = command === "message" || command === "session" ? rest[1] : rest[0];
20
22
  if (env.YUI_SESSION_SCOPE !== undefined && env.YUI_SESSION_SCOPE !== "global"
21
23
  || env.YUI_SESSION_SCOPE === "global" && env.YUI_ROLE !== "operator"
22
24
  && env.YUI_ROLE !== target) {
@@ -126,6 +128,7 @@ function roleContext(args, store, options) {
126
128
  pendingMessages: pendingQueue,
127
129
  messages: store.listGlobalRoleMessages(name),
128
130
  nativeTurn: sessions?.providerBinding?.run ?? null,
131
+ retry: providerRetryProjection(sessions?.providerBinding),
129
132
  interrupts: sessions?.interrupts ?? {},
130
133
  sessionManifestPath: environment.YUI_SESSION_MANIFEST,
131
134
  cliCommand: "yui"
@@ -382,6 +385,28 @@ function enterRole(args, store) {
382
385
  }
383
386
  function roleSession(args, store, options) {
384
387
  const [command, rawName, ...tail] = args;
388
+ if (command === "retry") {
389
+ const name = roleName(rawName);
390
+ const [action = "show", ...extra] = tail;
391
+ if (extra.length > 0 || !["show", "cancel", "disable", "enable"].includes(action)) {
392
+ throw usageError("Usage: yui session retry <role> [show|cancel|disable|enable]");
393
+ }
394
+ return store.transaction(tx => {
395
+ requireRole(name, tx);
396
+ const sessions = tx.getGlobalRoleSessionSet(name);
397
+ let binding = sessions?.providerBinding ?? null;
398
+ if (action !== "show") {
399
+ if (sessions === null || binding === null)
400
+ throw usageError("No current Provider Session.");
401
+ binding = controlProviderRetry(binding, action, Date.now());
402
+ tx.saveGlobalRoleSessionSet({ ...sessions, providerBinding: binding });
403
+ settleGlobalRetryInput(tx, name, binding, new Date());
404
+ }
405
+ return options.jsonOutput ? JSON.stringify({
406
+ roleName: name, retry: providerRetryProjection(binding), disabled: binding?.retryDisabled ?? false
407
+ }) + "\n" : renderProviderRetry(binding) + "\n";
408
+ });
409
+ }
385
410
  if (command !== "record" && command !== "replace") {
386
411
  throw usageError("Session usage: yui session record|replace <role> --native-id <id> [--reason <reason>].");
387
412
  }
@@ -1,10 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { readdir, rename, rm } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
- import { usageError } from "../errors/cliError.js";
4
+ import { CliError, usageError } from "../errors/cliError.js";
5
5
  import { defaultTableWidth, renderTable } from "../output/table.js";
6
6
  import { healCheckoutSwap, restoreCheckoutSwap, swapManagedCheckout } from "../repository/checkoutSwap.js";
7
- import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
7
+ import { GitWorkspaceRefreshError, NodeGitWorkspace } from "../repository/gitWorkspace.js";
8
8
  import { acquireProjectMaintenanceLock } from "../repository/projectMaintenanceLock.js";
9
9
  import { addProjectKnowledge, addKnowledgeProposal, assertProjectActive, decideKnowledgeProposal, createProject, findKnowledgeProposal, findKnowledgeProposalByFingerprint, knowledgeEvidenceDigest, knowledgeProposalFingerprint, managedProjectPath, planKnowledgeAcceptance, retireProject, retireProjectKnowledge, resolveProject, updateProjectKnowledge, updateProjectMetadata, validateProject, validateProjectName } from "../repository/project.js";
10
10
  import { managedWorkspacesRoot } from "../storage/homeLayout.js";
@@ -22,10 +22,14 @@ export async function runProjectCommand(args, store, options = {}) {
22
22
  }
23
23
  if (command === "refresh") {
24
24
  const refreshed = await refreshProject(rest, store, options);
25
+ const head = refreshed.changed
26
+ ? `Refreshed project ${refreshed.project.id}: ${refreshed.fromCommit} -> ${refreshed.toCommit}`
27
+ : `Project ${refreshed.project.id} HEAD is already current at ${refreshed.toCommit}`;
28
+ const tracking = refreshed.tracking;
25
29
  return {
26
- output: refreshed.changed
27
- ? `Refreshed project ${refreshed.project.id}: ${refreshed.fromCommit} -> ${refreshed.toCommit}\n`
28
- : `Project ${refreshed.project.id} is already current at ${refreshed.toCommit}\n`,
30
+ output: `${head}\n` + (tracking.status === "unmanaged"
31
+ ? `Tracking unmanaged: ${tracking.reason}\n`
32
+ : `Tracking ${tracking.status}: ${tracking.ref} at ${tracking.toCommit}\n`),
29
33
  data: refreshed
30
34
  };
31
35
  }
@@ -101,14 +105,23 @@ async function refreshProject(args, store, options) {
101
105
  // RFC Phase 1: hold the per-Project maintenance fence for the whole refresh
102
106
  // so Task workspace preparation and other maintenance cannot interleave
103
107
  // with the canonical branch/working-tree move.
104
- const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
108
+ const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
105
109
  try {
110
+ const current = requireUnchangedMaintenanceProject(store, project, "refresh");
106
111
  const refreshed = await (options.git ?? new NodeGitWorkspace()).refresh({
107
- repositoryPath: project.path,
108
- remoteUrl: project.remoteUrl,
109
- stableRef: project.stableBranch
112
+ repositoryPath: current.path,
113
+ remoteUrl: current.remoteUrl,
114
+ stableRef: current.stableBranch
110
115
  });
111
- return { project, ...refreshed };
116
+ return { project: current, ...refreshed };
117
+ }
118
+ catch (error) {
119
+ if (error instanceof GitWorkspaceRefreshError) {
120
+ throw new CliError("RUNTIME_ERROR", error.message, undefined, {
121
+ projectId: project.id, refresh: error.result
122
+ });
123
+ }
124
+ throw error;
112
125
  }
113
126
  finally {
114
127
  releaseMaintenance();
@@ -131,8 +144,9 @@ async function diagnoseProject(args, store, options) {
131
144
  };
132
145
  }
133
146
  const git = options.git ?? new NodeGitWorkspace();
134
- const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
147
+ const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
135
148
  try {
149
+ requireUnchangedMaintenanceProject(store, project, "diagnose");
136
150
  const current = await git.inspect(project.path, "HEAD");
137
151
  const remote = await git.resolveRemoteBaseline({
138
152
  repositoryPath: project.path,
@@ -307,13 +321,14 @@ async function migrateProject(args, store, options) {
307
321
  // Migration rewrites the Project's Git repository: hold the per-Project
308
322
  // maintenance fence so no rebuild/archive/cleanup (or a second migrate)
309
323
  // interleaves, and the Controller defers worktree preparation meanwhile.
310
- const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
324
+ const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
311
325
  try {
312
326
  // Re-read the Project under the fence. A concurrent migration may have
313
327
  // completed and switched the catalog to the Home-managed repo since the
314
328
  // record was resolved above; the stale snapshot must not drive any Git
315
329
  // effect (in particular it must not delete the now-canonical repo).
316
330
  const current = requireProject(store, project.id);
331
+ assertProjectActive(current, "migrate");
317
332
  if (current.ownership === "managed") {
318
333
  throw usageError(`Project is already Home-managed: ${project.id}.`);
319
334
  }
@@ -588,7 +603,7 @@ async function resetProject(args, store, options) {
588
603
  throw usageError(`Project reset requires matching stable and development branches: ${project.id}.`);
589
604
  }
590
605
  const git = options.git ?? new NodeGitWorkspace();
591
- const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
606
+ const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
592
607
  try {
593
608
  // Re-read under the fence so a concurrent catalog change can never drive
594
609
  // a destructive Git effect from a stale snapshot.
@@ -713,7 +728,7 @@ async function replaceProject(args, store, options) {
713
728
  + "Re-run with --discard-local to acknowledge that the checkout and any uncommitted state will be discarded.");
714
729
  }
715
730
  const git = options.git ?? new NodeGitWorkspace();
716
- const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
731
+ const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
717
732
  try {
718
733
  const current = requireProject(store, project.id);
719
734
  assertProjectActive(current, "replace");
@@ -922,8 +937,9 @@ async function deleteProjectCommand(args, store, options) {
922
937
  // every failure restores it, so the catalog never loses its recoverable
923
938
  // entry while a live checkout (or its failure) is still in play.
924
939
  const tombstone = join(store.rootDirectory(), "projects", `.delete-${project.id}`);
925
- const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
940
+ const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
926
941
  try {
942
+ requireUnchangedMaintenanceProject(store, project, "delete");
927
943
  // Heal a crashed earlier attempt before the prechecks turn.
928
944
  await healCheckoutSwap({ currentPath: project.path, backupPath: tombstone });
929
945
  if (existsSync(project.path)) {
@@ -1289,6 +1305,19 @@ function projectKnowledge(args, store, options) {
1289
1305
  ? "Project knowledge command is required."
1290
1306
  : `Unknown command: project knowledge ${command}`);
1291
1307
  }
1308
+ /** Waiting yields to catalog writers too; never apply stale Git/lifecycle inputs. */
1309
+ function requireUnchangedMaintenanceProject(store, expected, action) {
1310
+ const current = requireProject(store, expected.id);
1311
+ if (current.path !== expected.path
1312
+ || current.remoteUrl !== expected.remoteUrl
1313
+ || current.stableBranch !== expected.stableBranch
1314
+ || current.developmentBranch !== expected.developmentBranch
1315
+ || current.ownership !== expected.ownership
1316
+ || current.status !== expected.status) {
1317
+ throw new Error(`Project changed while waiting to ${action}: ${expected.id}; read current state before retrying.`);
1318
+ }
1319
+ return current;
1320
+ }
1292
1321
  function requireProject(store, reference) {
1293
1322
  const project = resolveProject(store.listProjects(), reference);
1294
1323
  if (project === null)