@zq-silk/yui 0.6.13 → 0.6.14

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 (90) hide show
  1. package/README.md +34 -5
  2. package/dist/cli/commandCatalog.js +173 -57
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli.js +99 -11
  5. package/dist/commands/configCommands.js +521 -171
  6. package/dist/commands/deliveryGuardPreflight.js +2 -2
  7. package/dist/commands/executionAuditCommands.js +56 -3
  8. package/dist/commands/projectCommands.js +504 -2
  9. package/dist/commands/releaseCommands.js +0 -1
  10. package/dist/commands/taskActor.js +17 -0
  11. package/dist/commands/taskBaseCommands.js +29 -0
  12. package/dist/commands/taskCommands.js +577 -126
  13. package/dist/commands/taskContextCommand.js +36 -2
  14. package/dist/commands/taskNextActionCommand.js +48 -3
  15. package/dist/commands/taskPublicationCommands.js +319 -0
  16. package/dist/commands/taskRoleRuntimeStatus.js +83 -59
  17. package/dist/commands/telemetryCommands.js +14 -13
  18. package/dist/config/yuiConfig.js +161 -8
  19. package/dist/context/sessionContextBudget.js +68 -0
  20. package/dist/context/wakeNotification.js +65 -0
  21. package/dist/controller/clientRuntime.js +2 -1
  22. package/dist/controller/ephemeralResourceReaper.js +2 -1
  23. package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
  24. package/dist/controller/jobSupervisor.js +5 -4
  25. package/dist/controller/resourceCleanupLinux.js +6 -6
  26. package/dist/controller/resourceInventoryLinux.js +3 -3
  27. package/dist/controller/runtime.js +88 -10
  28. package/dist/controller/updateReconciliation.js +4 -3
  29. package/dist/doctor/doctor.js +26 -7
  30. package/dist/executor/agentConfigurationCatalog.js +18 -0
  31. package/dist/executor/fileRoleLaunchPlanner.js +3 -3
  32. package/dist/lifecycle/contextBudgetRollover.js +81 -0
  33. package/dist/lifecycle/exactRunTerminalization.js +11 -1
  34. package/dist/lifecycle/providerErrorClass.js +33 -12
  35. package/dist/observability/executionAudit.js +214 -6
  36. package/dist/output/table.js +18 -0
  37. package/dist/repository/gitWorkspace.js +92 -0
  38. package/dist/repository/project.js +218 -4
  39. package/dist/repository/taskBaseFreshness.js +318 -0
  40. package/dist/repository/taskWorkspacePreparer.js +16 -2
  41. package/dist/review/deltaRecheck.js +232 -0
  42. package/dist/review/reviewConfig.js +31 -0
  43. package/dist/review/reviewFindingLedger.js +5 -1
  44. package/dist/review/reviewRound.js +156 -1
  45. package/dist/run/providerRetry.js +21 -3
  46. package/dist/run/providerRetryConfig.js +13 -60
  47. package/dist/run/recoveryProjection.js +199 -0
  48. package/dist/runtime/builtinAgentDrivers.js +3 -0
  49. package/dist/runtime/builtinTranscriptUsage.js +76 -32
  50. package/dist/runtime/continuationManager.js +17 -0
  51. package/dist/runtime/index.js +2 -0
  52. package/dist/runtime/launchDiagnostics.js +154 -0
  53. package/dist/runtime/lifecycleReservation.js +13 -0
  54. package/dist/runtime/providerContinuation.js +38 -0
  55. package/dist/runtime/providerContinuationReconciliationService.js +1 -0
  56. package/dist/runtime/providerErrorCodes.js +278 -0
  57. package/dist/runtime/runtimeHealthPolicy.js +20 -0
  58. package/dist/runtime/runtimeObservation.js +1 -0
  59. package/dist/runtime/runtimeProjection.js +115 -23
  60. package/dist/runtime/tmuxAdapters.js +242 -48
  61. package/dist/scheduler/activeRoleRunDelivery.js +139 -3
  62. package/dist/scheduler/activeTaskProgress.js +4 -3
  63. package/dist/scheduler/leaderWakeupProcessor.js +105 -15
  64. package/dist/scheduler/roleRunLiveness.js +2 -1
  65. package/dist/scheduler/roleRunStall.js +3 -2
  66. package/dist/scheduler/taskWake.js +72 -0
  67. package/dist/scheduler/wakeReason.js +64 -0
  68. package/dist/scheduler/wakeupQueue.js +2 -1
  69. package/dist/setup/setupCommand.js +1 -1
  70. package/dist/storage/migration/productionRegistry.js +325 -1
  71. package/dist/storage/sqliteSchema.js +61 -2
  72. package/dist/storage/sqliteStore.js +129 -2
  73. package/dist/storage/storeRpc.js +1 -0
  74. package/dist/storage/taskStore.js +262 -5
  75. package/dist/storage/upgrade/recordVersions.js +6 -1
  76. package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
  77. package/dist/task/completionReadiness.js +282 -0
  78. package/dist/task/publicationReference.js +123 -0
  79. package/dist/task/taskRecordReference.js +3 -1
  80. package/dist/telemetry/telemetryConfig.js +23 -18
  81. package/dist/telemetry/telemetryWiring.js +8 -8
  82. package/dist/tmux/tmuxManager.js +50 -9
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/view.js +18 -0
  85. package/dist/web/webSnapshot.js +100 -10
  86. package/i18n/README.zh-CN.md +5 -5
  87. package/package.json +1 -1
  88. package/skills/yui-leader/SKILL.md +49 -10
  89. package/skills/yui-operator/SKILL.md +13 -3
  90. package/skills/yui-worker/SKILL.md +8 -0
@@ -97,6 +97,8 @@ export class TmuxManager {
97
97
  ";",
98
98
  "set-option", "-g", "history-limit", String(this.#historyLimit),
99
99
  ";",
100
+ "set-option", "-g", "remain-on-exit", "on",
101
+ ";",
100
102
  "new-session", "-d",
101
103
  "-x", String(this.#initialColumns),
102
104
  "-y", String(this.#initialRows),
@@ -109,6 +111,7 @@ export class TmuxManager {
109
111
  }
110
112
  else {
111
113
  this.configureServerHistory();
114
+ this.run(["set-option", "-g", "remain-on-exit", "on"]);
112
115
  this.run([
113
116
  "new-window",
114
117
  "-t", this.sessionName(taskId),
@@ -141,6 +144,8 @@ export class TmuxManager {
141
144
  ";",
142
145
  "set-option", "-g", "history-limit", String(this.#historyLimit),
143
146
  ";",
147
+ "set-option", "-g", "remain-on-exit", "on",
148
+ ";",
144
149
  "new-session", "-d",
145
150
  "-x", String(this.#initialColumns),
146
151
  "-y", String(this.#initialRows),
@@ -153,6 +158,7 @@ export class TmuxManager {
153
158
  }
154
159
  else {
155
160
  await this.configureServerHistoryAsync();
161
+ await this.runAsync(["set-option", "-g", "remain-on-exit", "on"]);
156
162
  await this.runAsync([
157
163
  "new-window",
158
164
  "-t", this.sessionName(taskId),
@@ -324,6 +330,9 @@ export class TmuxManager {
324
330
  "capture-pane", "-p", "-t", this.target(taskId, roleName), "-S", `-${lines}`
325
331
  ]);
326
332
  }
333
+ captureRolePane(taskId, roleName, lines = 80) {
334
+ return this.captureRole(taskId, roleName, lines);
335
+ }
327
336
  dispatchRole(taskId, role, launch, input, options) {
328
337
  if (options.replaceExisting === true) {
329
338
  try {
@@ -356,24 +365,33 @@ export class TmuxManager {
356
365
  const target = this.target(taskId, roleName);
357
366
  const output = this.run([
358
367
  "display-message", "-p", "-t", target,
359
- "#{pane_dead}|#{pane_pid}|#{pane_current_command}"
368
+ [
369
+ "#{pane_dead}",
370
+ "#{pane_pid}",
371
+ "#{pane_current_command}",
372
+ "#{pane_dead_status}"
373
+ ].join("|")
360
374
  ]).trim();
361
375
  const separator = output.indexOf("|");
362
376
  const secondSeparator = separator < 0 ? -1 : output.indexOf("|", separator + 1);
363
- if (separator < 0 || secondSeparator < 0) {
377
+ const lastSeparator = secondSeparator < 0 ? -1 : output.lastIndexOf("|");
378
+ if (separator < 0 || secondSeparator < 0 || lastSeparator <= secondSeparator) {
364
379
  throw runtimeError(`Tmux returned an invalid pane state for ${roleName}.`);
365
380
  }
366
381
  const dead = output.slice(0, separator);
367
382
  const pidText = output.slice(separator + 1, secondSeparator);
368
- const currentCommand = output.slice(secondSeparator + 1);
383
+ const currentCommand = output.slice(secondSeparator + 1, lastSeparator);
384
+ const exitStatusText = output.slice(lastSeparator + 1);
369
385
  const pid = Number(pidText);
386
+ const exitStatus = nonNegativeNumber(exitStatusText);
370
387
  return {
371
388
  taskId,
372
389
  roleName,
373
390
  target,
374
391
  dead: dead === "1",
375
392
  ...(Number.isSafeInteger(pid) && pid > 0 ? { pid } : {}),
376
- currentCommand
393
+ currentCommand,
394
+ ...(dead === "1" && exitStatus !== undefined ? { exitStatus } : {})
377
395
  };
378
396
  }
379
397
  async inspectPaneAsync(taskId, roleName) {
@@ -396,14 +414,33 @@ export class TmuxManager {
396
414
  "#{cursor_y}",
397
415
  "#{history_size}",
398
416
  "#{pane_current_command}",
417
+ "#{pane_dead_status}",
399
418
  receiptFormat
400
419
  ].join("|")
401
420
  ]);
402
- const state = output.trimEnd().split("|");
403
- if (state.length !== 8 || state[0] !== PANE_STATE_MARKER) {
421
+ const normalizedOutput = output.trimEnd();
422
+ const fieldSeparators = [...normalizedOutput.matchAll(/\|/g)].map((match) => match.index);
423
+ const lastSeparator = fieldSeparators.at(-1);
424
+ const exitSeparator = fieldSeparators.at(-2);
425
+ if (fieldSeparators.length < 8
426
+ || lastSeparator === undefined
427
+ || exitSeparator === undefined
428
+ || !normalizedOutput.startsWith(PANE_STATE_MARKER)) {
429
+ throw runtimeError(`Tmux returned an invalid pane state for ${roleName}.`);
430
+ }
431
+ const [markerEnd, deadEnd, pidEnd, cursorXEnd, cursorYEnd, historyEnd] = fieldSeparators;
432
+ const receiptSeparator = lastSeparator;
433
+ const deadText = normalizedOutput.slice(markerEnd + 1, deadEnd);
434
+ const pidText = normalizedOutput.slice(deadEnd + 1, pidEnd);
435
+ const cursorXText = normalizedOutput.slice(pidEnd + 1, cursorXEnd);
436
+ const cursorYText = normalizedOutput.slice(cursorXEnd + 1, cursorYEnd);
437
+ const historySizeText = normalizedOutput.slice(cursorYEnd + 1, historyEnd);
438
+ const currentCommand = normalizedOutput.slice(historyEnd + 1, exitSeparator);
439
+ const exitStatusText = normalizedOutput.slice(exitSeparator + 1, receiptSeparator);
440
+ const receiptText = normalizedOutput.slice(receiptSeparator + 1);
441
+ if (!normalizedOutput.slice(0, markerEnd).startsWith(PANE_STATE_MARKER)) {
404
442
  throw runtimeError(`Tmux returned an invalid pane state for ${roleName}.`);
405
443
  }
406
- const [, deadText, pidText, cursorXText, cursorYText, historySizeText, currentCommand, receiptText] = state;
407
444
  if ((deadText !== "0" && deadText !== "1")
408
445
  || currentCommand === undefined
409
446
  || receiptText === undefined
@@ -414,6 +451,7 @@ export class TmuxManager {
414
451
  const cursorX = nonNegativeNumber(cursorXText);
415
452
  const cursorY = nonNegativeNumber(cursorYText);
416
453
  const historySize = nonNegativeNumber(historySizeText);
454
+ const exitStatus = nonNegativeNumber(exitStatusText);
417
455
  if (cursorX === undefined || cursorY === undefined || historySize === undefined) {
418
456
  throw runtimeError(`Tmux returned an invalid pane state for ${roleName}.`);
419
457
  }
@@ -425,6 +463,7 @@ export class TmuxManager {
425
463
  dead: deadText === "1",
426
464
  ...(pid === undefined ? {} : { pid }),
427
465
  currentCommand,
466
+ ...(deadText === "1" && exitStatus !== undefined ? { exitStatus } : {}),
428
467
  cursorX,
429
468
  cursorY,
430
469
  historySize
@@ -775,7 +814,8 @@ export class TmuxManager {
775
814
  ...(pane.pid === undefined ? {} : { pid: pane.pid }),
776
815
  target: pane.target,
777
816
  dead: pane.dead,
778
- currentCommand: pane.currentCommand
817
+ currentCommand: pane.currentCommand,
818
+ ...(pane.exitStatus === undefined ? {} : { exitStatus: pane.exitStatus })
779
819
  };
780
820
  }
781
821
  async inspectRolePaneAsync(taskId, roleName) {
@@ -784,7 +824,8 @@ export class TmuxManager {
784
824
  ...(pane.pid === undefined ? {} : { pid: pane.pid }),
785
825
  target: pane.target,
786
826
  dead: pane.dead,
787
- currentCommand: pane.currentCommand
827
+ currentCommand: pane.currentCommand,
828
+ ...(pane.exitStatus === undefined ? {} : { exitStatus: pane.exitStatus })
788
829
  };
789
830
  }
790
831
  stopTask(taskId) {
@@ -66,6 +66,8 @@ const messages = {
66
66
  "detail.focus": "Current focus",
67
67
  "detail.runtimeHealth": "Runtime health",
68
68
  "detail.lastProgress": "Last semantic progress",
69
+ "detail.recoveryFence": "Canonical recovery fence (Yui durable)",
70
+ "detail.recoveryProviderObserved": "Provider observation (evidence only)",
69
71
  "input.freeText": "Type your answer",
70
72
  "input.answered": "Input answered.",
71
73
  "input.new": "A new input needs your attention.",
@@ -364,6 +366,8 @@ const messages = {
364
366
  "detail.focus": "当前重点",
365
367
  "detail.runtimeHealth": "运行健康",
366
368
  "detail.lastProgress": "最近语义进展",
369
+ "detail.recoveryFence": "规范恢复 fence(Yui 持久)",
370
+ "detail.recoveryProviderObserved": "Provider 观测(仅证据)",
367
371
  "input.freeText": "输入回答",
368
372
  "input.answered": "输入已回答。",
369
373
  "input.new": "有新的输入请求需要处理。",
@@ -340,6 +340,24 @@ export function renderTaskDetail(detail, data, t, locale, actions) {
340
340
  node("p", "record-copy", (run.kind || "workflow-not-progressing") + " · " + (run.classification || "truly-stalled")),
341
341
  node("small", "", t("detail.lastProgress") + " · " + formatDateTime(run.progressAt, locale))
342
342
  );
343
+ // Issue 08: the same canonical recovery projection as the CLI. The
344
+ // durable fence and the Provider observation stay clearly separated;
345
+ // the exact recovery actions live in "yui task run show".
346
+ if (run.recovery) {
347
+ if (run.recovery.canonicalProgressAt) {
348
+ card.append(node("small", "",
349
+ t("detail.recoveryFence") + " · " + formatDateTime(run.recovery.canonicalProgressAt, locale)));
350
+ }
351
+ if (run.recovery.provider && run.recovery.provider.observedAt) {
352
+ card.append(node("small", "",
353
+ t("detail.recoveryProviderObserved") + " · " + run.recovery.provider.observationKind
354
+ + " " + formatDateTime(run.recovery.provider.observedAt, locale)));
355
+ }
356
+ if (run.recovery.recoverable) {
357
+ card.append(node("small", "record-copy",
358
+ "yui task run show " + task.id + "/" + run.runId));
359
+ }
360
+ }
343
361
  runtimeHealthBody.append(card);
344
362
  });
345
363
  scaffold.append(anchorSection(
@@ -1,7 +1,11 @@
1
- import { isRoleRunStalled } from "../scheduler/roleRunStall.js";
1
+ import { isRoleRunStalled, latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
2
2
  import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
3
3
  import { summarizeExecutionGroup } from "../execution/executionGroup.js";
4
4
  import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
5
+ import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
6
+ import { classifyRuntimeHealth, projectRuntimeTaskEvents } from "../runtime/runtimeProjection.js";
7
+ import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
8
+ import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
5
9
  export function buildWebDashboardSnapshot(store, now = new Date()) {
6
10
  return store.transaction((reader) => {
7
11
  const statusCounts = {
@@ -52,7 +56,7 @@ export function buildWebDashboardSnapshot(store, now = new Date()) {
52
56
  };
53
57
  });
54
58
  }
55
- export function buildWebTaskDetail(store, taskId) {
59
+ export function buildWebTaskDetail(store, taskId, now = new Date()) {
56
60
  return store.transaction((reader) => {
57
61
  const task = reader.getTask(taskId);
58
62
  if (task === null)
@@ -67,16 +71,24 @@ export function buildWebTaskDetail(store, taskId) {
67
71
  const events = reader.listEvents?.(taskId) ?? [];
68
72
  const needsAttentionRuns = runs
69
73
  .filter((run) => run.status === "active" && isRoleRunStalled(events, run.id))
70
- .map((run) => ({
71
- runId: run.id,
72
- roleName: run.roleName,
73
- progressAt: latestStallProgress(events, run.id),
74
- kind: latestStallField(events, run.id, "kind") ?? "workflow-not-progressing",
75
- classification: latestStallField(events, run.id, "classification") ?? "truly-stalled"
76
- }));
74
+ .map((run) => {
75
+ const facts = readRunRecoveryFacts(reader, taskId, run.id);
76
+ return {
77
+ runId: run.id,
78
+ roleName: run.roleName,
79
+ progressAt: latestStallProgress(events, run.id),
80
+ kind: latestStallField(events, run.id, "kind") ?? "workflow-not-progressing",
81
+ classification: latestStallField(events, run.id, "classification") ?? "truly-stalled",
82
+ // Issue 08: the same canonical recovery projection the CLI exposes.
83
+ ...(facts === null ? {} : { recovery: projectRunRecovery(facts) })
84
+ };
85
+ });
77
86
  const activeRuns = new Map(runs
78
87
  .filter((run) => run.status === "active")
79
88
  .map((run) => [run.roleName, run]));
89
+ const activeRunHealth = runs
90
+ .filter((run) => run.status === "active")
91
+ .map((run) => projectWebRunRuntimeHealth(reader, taskId, run, events, now));
80
92
  const roles = reader.listRoles(taskId).map((role) => {
81
93
  const activeRun = activeRuns.get(role.name);
82
94
  const sessions = reader.getTaskRoleSessionSet(taskId, role.name);
@@ -105,7 +117,7 @@ export function buildWebTaskDetail(store, taskId) {
105
117
  };
106
118
  }),
107
119
  runs,
108
- runtimeHealth: { needsAttentionRuns },
120
+ runtimeHealth: { needsAttentionRuns, activeRuns: activeRunHealth },
109
121
  reviewRounds: reader.listReviewRounds(taskId),
110
122
  openInputs: inputs.filter((request) => request.status === "open"),
111
123
  messages: reader.listMessages(taskId),
@@ -117,6 +129,84 @@ export function buildWebTaskDetail(store, taskId) {
117
129
  function latestStallProgress(events, runId) {
118
130
  return latestStallField(events, runId, "progressAt");
119
131
  }
132
+ /**
133
+ * Layered runtime health for one active Run, computed from the same stored
134
+ * observations and durable semantic fold as the CLI status projection. The
135
+ * Web snapshot has no live tmux pane, so host state stays "unknown"; the
136
+ * classifier still surfaces session/turn/operation/observer layers and the
137
+ * scheduler's durable `run.stalled` episode is surfaced as
138
+ * `stalled-candidate`.
139
+ */
140
+ function projectWebRunRuntimeHealth(reader, taskId, run, events, now) {
141
+ const stalled = isRoleRunStalled(events, run.id);
142
+ const sessions = reader.getTaskRoleSessionSet(taskId, run.roleName);
143
+ const session = sessions?.sessions[run.effective.agentId];
144
+ const stallReason = "the live active Run has no durable progress in the configured stall window";
145
+ if (run.deliveredAt === undefined || session?.launchId === undefined) {
146
+ return {
147
+ runId: run.id,
148
+ roleName: run.roleName,
149
+ layer: stalled ? "stalled-candidate" : "awaiting-provider-acceptance",
150
+ reason: stalled ? stallReason : "the pushed active Run is awaiting provider acceptance",
151
+ stalled,
152
+ lastSemanticProgressAt: run.createdAt
153
+ };
154
+ }
155
+ let driverId;
156
+ try {
157
+ driverId = builtinDriverIdForAdapter(run.effective.adapterId);
158
+ }
159
+ catch {
160
+ return {
161
+ runId: run.id,
162
+ roleName: run.roleName,
163
+ layer: stalled ? "stalled-candidate" : "runtime-unobservable",
164
+ reason: stalled ? stallReason : "the Agent Driver is not a built-in driver",
165
+ stalled,
166
+ lastSemanticProgressAt: run.deliveredAt
167
+ };
168
+ }
169
+ const fence = {
170
+ taskId,
171
+ roleName: run.roleName,
172
+ runId: run.id,
173
+ agentId: run.effective.agentId,
174
+ driverId,
175
+ launchId: session.launchId,
176
+ sessionGenerationId: session.launchId,
177
+ nativeSessionId: session.nativeSessionId,
178
+ nativeTurnId: run.id,
179
+ receiptId: formatAgentRunReceiptId(taskId, run.id)
180
+ };
181
+ const projection = projectRuntimeTaskEvents(fence, run.createdAt, events);
182
+ const view = {
183
+ getAgentRun: (taskId, runId) => reader.listAgentRuns(taskId).find((candidate) => candidate.id === runId) ?? null,
184
+ listEvents: () => events,
185
+ getWorkItem: (workItemTaskId, workItemId) => reader.listWorkItems(workItemTaskId).find((item) => item.id === workItemId) ?? null,
186
+ listReviewRounds: (taskId) => reader.listReviewRounds(taskId),
187
+ listChangeSets: (taskId) => reader.listChangeSets(taskId),
188
+ listIntegrationAttempts: (taskId) => reader.listIntegrationAttempts(taskId),
189
+ listInputRequests: (taskId) => reader.listInputRequests(taskId)
190
+ };
191
+ const semanticProgress = latestRunDurableProgressAt(view, taskId, run.roleName, run.id)
192
+ ?? { progressAt: run.deliveredAt };
193
+ const classification = classifyRuntimeHealth({
194
+ projection,
195
+ semanticProgressAt: semanticProgress.progressAt,
196
+ now
197
+ });
198
+ return {
199
+ runId: run.id,
200
+ roleName: run.roleName,
201
+ layer: stalled ? "stalled-candidate" : classification.layer,
202
+ reason: stalled ? stallReason : classification.reason,
203
+ stalled,
204
+ ...(classification.lastRuntimeActivityAt === undefined
205
+ ? {}
206
+ : { lastRuntimeActivityAt: classification.lastRuntimeActivityAt }),
207
+ lastSemanticProgressAt: classification.lastSemanticProgressAt
208
+ };
209
+ }
120
210
  function latestStallField(events, runId, field) {
121
211
  const stalled = events
122
212
  .filter((event) => event.type === "run.stalled" && event.payload.runId === runId)
@@ -116,16 +116,16 @@ yui task activate <task-id>
116
116
 
117
117
  ```sh
118
118
  yui config show
119
- yui config set --time-zone Europe/London
119
+ yui config set time-zone Europe/London
120
120
  ```
121
121
 
122
122
  WorkItem 审查只有一条可选的全局规则,并直接复用已有 Global Role 的
123
123
  Agent、model、权限、prompt 和 Skills:
124
124
 
125
125
  ```sh
126
- yui config review set --role reviewer --trigger always
127
- yui config review show
128
- yui config review clear
126
+ yui config set review --role reviewer --trigger always
127
+ yui config show
128
+ yui config clear review
129
129
  ```
130
130
 
131
131
  对带 Project 的软件交付,可使用 `--trigger final`:WorkItem 验收与
@@ -133,7 +133,7 @@ Integration 保持独立,在 Task 完成前只对所有已集成 Project 的
133
133
  一次 Task 级 ReviewRound:
134
134
 
135
135
  ```sh
136
- yui config review set --role reviewer --trigger final
136
+ yui config set review --role reviewer --trigger final
137
137
  ```
138
138
 
139
139
  每个进入 Leader 验收阶段的结果,都会成为原 WorkItem 上一个明确的候选。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.6.13",
3
+ "version": "0.6.14",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -96,6 +96,19 @@ children through adapter metadata, and routes a later result reference through
96
96
  the durable inbox. Do not poll, send a waiting Message, rewrite a checkpoint,
97
97
  or yield merely to preserve that native wait.
98
98
 
99
+ Native child results have an explicit durability boundary. A native subagent is
100
+ best-effort by default: its result returns through the parent Conversation, and
101
+ if the parent Session is lost before Yui externalizes the result, rerun the
102
+ child — Yui does not claim it was durably received. When Yui persists a
103
+ continuation report with result content, the child becomes durable-result:
104
+ `yui task continuation list <task>` shows its mode, content digest, and the
105
+ Task event holding the full result. After a parent crash, read durable-result
106
+ children by their event reference instead of rerunning them; rerun only
107
+ best-effort children whose result was never externalized. Critical,
108
+ non-repeatable, or independently verifiable work must use a Yui
109
+ WorkItem/ExecutionGroup, not a native subagent, because only a managed Lane
110
+ owns an independent Run, receipt, and workspace.
111
+
99
112
  For a managed Task Role or Reviewer Run, persist a necessary changed checkpoint,
100
113
  yield the active Leader Run, and stop the turn. Its durable mailbox result or an
101
114
  attention event wakes a later Leader Run. An unchanged healthy managed wait is
@@ -191,8 +204,17 @@ character budget: choose the smallest useful abstraction for the recipient.
191
204
 
192
205
  ## Recover and persist Task context
193
206
 
194
- A launch or wake message is a pointer, not the full context. Start with the
195
- complete Task projection, then follow its Project Policy references:
207
+ A launch or wake message is a pointer, not the full context. A wake carries a
208
+ minimal envelope: the wake id, the aggregated reason tags, and the delta
209
+ window. It never embeds context content. Read the delta on demand:
210
+
211
+ ```sh
212
+ yui task wake show <task-id> <wake-id>
213
+ ```
214
+
215
+ For a fresh generation (no native history), or when the envelope indicates a
216
+ major change, start with the complete Task projection, then follow its Project
217
+ Policy references:
196
218
 
197
219
  ```sh
198
220
  yui task context <task-id>
@@ -200,10 +222,18 @@ yui project show <project>
200
222
  yui project knowledge list <project>
201
223
  ```
202
224
 
203
- Inspect `task work`, `task role`, `task integration`, `task input`, and other
204
- narrower records only when the projection identifies a specific record that
205
- needs closer evidence. Use exact IDs returned by Yui. Never edit `state.json`,
206
- managed refs, worktrees, Sessions, or provider IDs directly.
225
+ The `wake show` delta lists the exact events, messages, and Runs that arrived
226
+ in the window. Inspect `task work`, `task role`, `task integration`, `task
227
+ input`, and other narrower records only when the projection or delta
228
+ identifies a specific record that needs closer evidence. Use exact IDs
229
+ returned by Yui. Never edit `state.json`, managed refs, worktrees, Sessions,
230
+ or provider IDs directly.
231
+
232
+ For a `role-run-stalled` or runtime-health wake, diagnose from the exact
233
+ Run/Event/Session and related WorkItem/Review/Integration records. Preserve
234
+ the current fence and write a Task Message only for a new root cause, impact,
235
+ recovery action, acceptance decision, or user-relevant conclusion; an
236
+ unchanged healthy wait is zero Message.
207
237
 
208
238
  Maintain durable context throughout a long-running Task:
209
239
 
@@ -220,8 +250,11 @@ Maintain durable context throughout a long-running Task:
220
250
  work. Supersede it explicitly when the choice changes.
221
251
  - Add a Milestone for a phase result that can be independently reported or
222
252
  resumed.
223
- - Add or update Project Knowledge only for stable facts useful across Tasks.
224
- Do not use it as a Task log, transcript, or scratchpad.
253
+ - Propose Project Knowledge promotion for stable conclusions useful across
254
+ Tasks. Project Knowledge is an Operator authority: a Leader proposes a
255
+ candidate (with its source Task/Decision/Milestone evidence) and an Operator
256
+ reviews and accepts it; the Leader cannot write the authoritative Knowledge
257
+ list directly. Do not use Knowledge as a Task log, transcript, or scratchpad.
225
258
  - Before requesting user input, persist the current focus, known evidence, and
226
259
  exact blocker.
227
260
 
@@ -236,10 +269,16 @@ yui task decision record <task-id> \
236
269
  --title "<material choice>" --rationale "<reason and consequences>"
237
270
  yui task milestone add <task-id> \
238
271
  --title "<phase>" --summary "<delivered result and evidence>"
239
- yui project knowledge add <project> "<stable fact>" --body "<reusable knowledge>"
272
+ yui project knowledge propose <project> \
273
+ --title "<stable conclusion>" --body "<self-contained project-level knowledge>" \
274
+ --task <task-id> [--decision <id>] [--milestone <id>]
240
275
  ```
241
276
 
242
- Update an existing Knowledge record instead of creating duplicates.
277
+ Resubmitting the same candidate is deduplicated to the existing proposal. A
278
+ proposal that conflicts with an existing Knowledge entry is never silently
279
+ overwritten: the Operator chooses an explicit update, supersede, or reject.
280
+ Pending proposals appear in `yui task next-action <task-id>` as a non-blocking
281
+ advisory; they do not block Task completion.
243
282
 
244
283
  ## Choose the execution path
245
284
 
@@ -139,15 +139,25 @@ yui project discover [name]
139
139
  yui project show <project>
140
140
  yui project knowledge list <project>
141
141
  yui project knowledge show <project> <knowledge-id>
142
+ yui project knowledge proposals list <project>
143
+ yui project knowledge proposals show <project> <proposal-id>
144
+ yui project knowledge accept <project> <proposal-id>
145
+ yui project knowledge reject <project> <proposal-id> --reason "<text>"
142
146
  yui task create "<title>" \
143
147
  --project <project-a> --project <project-b> \
144
148
  --base <project-a>=<ref> --base <project-b>=<ref>
145
149
  yui task activate <task-id>
146
150
  ```
147
151
 
148
- Keep catalog metadata current with `project update`. Update current Knowledge
149
- and retire obsolete Knowledge without deleting its history. If discovery finds
150
- an existing stable checkout, bind it with `project add`. If only a remote is
152
+ Keep catalog metadata current with `project update`. Project Knowledge is an
153
+ Operator authority: a Leader proposes promotion candidates (with source
154
+ Task/Decision/Milestone evidence) and the Operator reviews and accepts or
155
+ rejects them. Acceptance writes the Knowledge entry with its provenance; a
156
+ candidate that duplicates an existing entry is deduplicated, and one that
157
+ conflicts with an existing title fails closed so the Operator must choose an
158
+ explicit `--update`, supersede, or reject. Update current Knowledge and retire
159
+ obsolete Knowledge without deleting its history. If discovery finds an
160
+ existing stable checkout, bind it with `project add`. If only a remote is
151
161
  known, explain the clone destination and impact, obtain confirmation, then run
152
162
  `project clone`; do not send the user mechanical clone steps.
153
163
 
@@ -114,6 +114,14 @@ child Session or Run record. The Leader reviews
114
114
  the result and records the actual Profile revision, runtime model/effort,
115
115
  round, result, and checks in the WorkItem summary.
116
116
 
117
+ A native child result is best-effort until Yui externalizes it: the result
118
+ returns through the parent Conversation, and if that Session is lost before
119
+ the Leader consumes it, the child may need to rerun. Do not claim Yui
120
+ durability for a native result you only emitted in the provider transcript.
121
+ When the child brief requires a durable, independently recoverable result,
122
+ the Leader must dispatch the work as a Yui WorkItem/ExecutionGroup instead —
123
+ native subagents never own a Yui Run, receipt, or workspace.
124
+
117
125
  ## Task Role AgentRun
118
126
 
119
127
  The managed input names the current Run ID. Before ending a managed Codex or