@zq-silk/yui 0.10.0 → 0.11.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 (46) hide show
  1. package/README.md +53 -0
  2. package/dist/cli/commandCatalog.js +28 -7
  3. package/dist/cli.js +50 -47
  4. package/dist/commands/projectCommands.js +69 -6
  5. package/dist/commands/taskCommands.js +639 -89
  6. package/dist/commands/taskContextCommand.js +78 -27
  7. package/dist/commands/taskNextActionCommand.js +13 -2
  8. package/dist/commands/taskOverviewCommand.js +21 -5
  9. package/dist/commands/taskUpstreamCommands.js +136 -0
  10. package/dist/context/runContextPack.js +184 -17
  11. package/dist/controller/agentRuntimeObserver.js +31 -20
  12. package/dist/controller/fileSchedulerStoreAdapter.js +7 -13
  13. package/dist/execution/candidateConvergence.js +623 -0
  14. package/dist/execution/executionGroup.js +255 -13
  15. package/dist/execution/executionHealth.js +324 -0
  16. package/dist/execution/resourceBroker.js +425 -0
  17. package/dist/executor/fileRoleLaunchPlanner.js +6 -9
  18. package/dist/executor/workspacePreflightClassification.js +117 -0
  19. package/dist/lifecycle/exactRunTerminalization.js +13 -2
  20. package/dist/lifecycle/taskRoleSessionReset.js +4 -2
  21. package/dist/repository/taskBaseFreshness.js +26 -1
  22. package/dist/repository/taskWorkspacePreparer.js +17 -1
  23. package/dist/review/reviewRound.js +27 -6
  24. package/dist/run/agentRun.js +2 -2
  25. package/dist/run/recoveryProjection.js +15 -0
  26. package/dist/runtime/runtimeContinuationProjection.js +7 -0
  27. package/dist/runtime/tmuxAdapters.js +8 -2
  28. package/dist/scheduler/actionability.js +169 -3
  29. package/dist/scheduler/activeTaskProgress.js +15 -10
  30. package/dist/scheduler/leaderWakeupProcessor.js +17 -1
  31. package/dist/scheduler/taskExecutionProjection.js +105 -8
  32. package/dist/scheduler/taskObservabilityProjection.js +282 -0
  33. package/dist/storage/migration/productionRegistry.js +14 -0
  34. package/dist/storage/sqliteStore.js +12 -0
  35. package/dist/storage/taskStore.js +1 -1
  36. package/dist/task/completionReadiness.js +1 -1
  37. package/dist/task/nextAction.js +314 -2
  38. package/dist/web/assets/client/components.js +116 -0
  39. package/dist/web/assets/client/i18n.js +66 -0
  40. package/dist/web/assets/client/view.js +15 -0
  41. package/dist/web/assets/styles/cards.js +23 -0
  42. package/dist/web/assets/styles/responsive.js +2 -0
  43. package/dist/web/webSnapshot.js +8 -2
  44. package/dist/workItem/workItem.js +262 -5
  45. package/i18n/README.zh-CN.md +42 -0
  46. package/package.json +1 -1
@@ -14,6 +14,8 @@ import {
14
14
  messageCard,
15
15
  metaItem,
16
16
  metricTile,
17
+ observabilityMetricCard,
18
+ dagGraph,
17
19
  overviewRow,
18
20
  pagedList,
19
21
  pathMetaItem,
@@ -290,6 +292,9 @@ export function renderTaskDetail(detail, data, t, locale, actions) {
290
292
  // the attention/blocker facts behind it.
291
293
  const band = executionBand(data.execution, t, locale);
292
294
  if (band) summaryBody.append(band);
295
+ const observability = data.observability || (data.execution && data.execution.observability);
296
+ const metrics = observabilityMetricCard(observability, t);
297
+ if (metrics) summaryBody.append(metrics);
293
298
 
294
299
  if (task.completionSummary) {
295
300
  const conclusion = node("div", "conclusion");
@@ -367,6 +372,16 @@ export function renderTaskDetail(detail, data, t, locale, actions) {
367
372
  ));
368
373
  }
369
374
 
375
+ if (observability && observability.dag) {
376
+ const dagBody = node("div", "section-body");
377
+ dagBody.append(dagGraph(observability.dag, t));
378
+ scaffold.append(anchorSection(
379
+ "detail-dag",
380
+ sectionHead(t("detail.dag"), { count: observability.dag.nodes.length }),
381
+ dagBody
382
+ ));
383
+ }
384
+
370
385
  // 3. Focus (anchor #detail-focus) — brief + technical approach
371
386
  const focusBody = node("div", "section-body");
372
387
  if (data.brief) {
@@ -159,6 +159,29 @@ details.work-item-card>summary.record-head~*{margin-top:0}
159
159
  .lane-role{font-family:var(--font-body);font-weight:700;font-size:12px;color:var(--text)}
160
160
  .lane-status{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
161
161
  .lane-summary{flex:1;min-width:0;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
162
+ .execution-stage-meta,.execution-resource-meta,.work-item-observability{gap:10px;flex-wrap:wrap}
163
+ .work-item-stages{margin-top:6px}
164
+ .execution-stage-meta .chip,.execution-resource-meta .chip{margin-left:auto}
165
+ .observability-metrics{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px;margin-top:12px}
166
+ .observability-metrics .metric{min-height:52px;padding:8px 10px}
167
+ .observability-metrics .metric-value{font-size:18px}
168
+ .observability-context-meta{gap:10px;flex-wrap:wrap;margin-top:7px}
169
+ .dag-graph{display:grid;gap:8px}
170
+ .dag-node{display:grid;gap:6px;padding:9px 11px;background:var(--bg-2);border:1px solid var(--border);border-left:3px solid var(--faint);border-radius:var(--radius);box-shadow:var(--shadow-card)}
171
+ .dag-node.is-ready{border-left-color:var(--accent)}
172
+ .dag-node.is-running{border-left-color:var(--active)}
173
+ .dag-node.is-blocked,.dag-node.is-failed,.dag-node.is-awaiting_acceptance{border-left-color:var(--danger)}
174
+ .dag-node.is-completed{border-left-color:var(--success)}
175
+ .dag-node.is-retired{border-left-color:var(--faint)}
176
+ .dag-node-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
177
+ .dag-node-head .status-dot{margin-top:0}
178
+ .dag-node-title{flex:1;min-width:0;font-size:12px;color:var(--text)}
179
+ .dag-node-deps{display:flex;align-items:center;gap:6px;flex-wrap:wrap;color:var(--muted);font-family:var(--font-mono);font-size:9.5px}
180
+ .dag-node-deps small{margin-right:2px;text-transform:uppercase;letter-spacing:.08em}
181
+ .dag-root-cause{color:var(--warning);font-family:var(--font-mono);font-size:9.5px;letter-spacing:.03em}
182
+ .chip.is-satisfied{color:var(--success);background:var(--success-soft);border-color:transparent}
183
+ .chip.is-active{color:var(--accent);background:var(--accent-soft);border-color:var(--accent-line)}
184
+ .chip.is-failed-open,.chip.is-dead{color:var(--danger);background:var(--danger-soft);border-color:transparent}
162
185
  .exec-resolution{display:flex;gap:9px;align-items:baseline;flex-wrap:wrap;padding:7px 10px;border-radius:var(--radius);background:var(--accent-soft);border:1px solid var(--accent-line)}
163
186
  .exec-resolution-decision{font-family:var(--font-mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}
164
187
  .exec-resolution-summary{flex:1;min-width:0;font-size:12px;color:var(--text)}
@@ -21,6 +21,7 @@ export const RESPONSIVE_STYLES = `
21
21
  body.detail-active .detail-back{display:grid}
22
22
  .detail{overflow:visible}
23
23
  .overview-duo{grid-template-columns:1fr}
24
+ .observability-metrics{grid-template-columns:repeat(3,minmax(0,1fr))}
24
25
  /* Terminal panel overlays full-screen instead of occupying a grid column */
25
26
  .terminal-panel{position:fixed;inset:0;z-index:70;grid-column:auto;height:100vh;border-left:0}
26
27
  }
@@ -34,6 +35,7 @@ export const RESPONSIVE_STYLES = `
34
35
  .topbar{flex-wrap:nowrap;gap:10px}
35
36
  .detail-tabs{mask-image:linear-gradient(90deg,#000 calc(100% - 28px),transparent);-webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 28px),transparent)}
36
37
  .record-cols{grid-template-columns:1fr}
38
+ .observability-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}
37
39
  }
38
40
  @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
39
41
  `;
@@ -33,7 +33,7 @@ export function buildWebDashboardSnapshot(store, now = new Date()) {
33
33
  const needsAttentionCount = reader.listAgentRuns(task.id)
34
34
  .filter((run) => run.status === "active" && isRoleRunStalled(events, run.id))
35
35
  .length;
36
- const execution = buildTaskExecutionProjection(reader, task.id);
36
+ const execution = buildTaskExecutionProjection(reader, task.id, task, now);
37
37
  const names = task.projectBindings.flatMap(({ projectId }) => {
38
38
  const name = projectNames.get(projectId);
39
39
  return name === undefined ? [] : [name];
@@ -105,18 +105,24 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
105
105
  && effectiveLaunch.sourceDesiredRevision !== role.launchRevision
106
106
  };
107
107
  });
108
+ const execution = buildTaskExecutionProjection(reader, taskId, task, now);
109
+ if (execution === null)
110
+ return null;
111
+ const workItemObservability = new Map(execution.observability.workItems.map((item) => [item.workItemId, item]));
108
112
  return {
109
113
  task: {
110
114
  ...task,
111
115
  ...(projectNames.length === 0 ? {} : { projectNames })
112
116
  },
113
- execution: buildTaskExecutionProjection(reader, taskId),
117
+ execution,
118
+ observability: execution.observability,
114
119
  brief: reader.getTaskBrief(taskId),
115
120
  roles,
116
121
  workItems: reader.listWorkItems(taskId).map((item) => {
117
122
  const group = currentWorkItemExecutionGroup(item);
118
123
  return {
119
124
  ...item,
125
+ observability: workItemObservability.get(item.id),
120
126
  ...(group === undefined ? {} : { currentExecution: summarizeExecutionGroup(group) })
121
127
  };
122
128
  }),
@@ -1,9 +1,10 @@
1
- import { normalizedUniqueIdentities, normalizedUniqueText, requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
1
+ import { normalizedUniqueIdentities, normalizedUniqueText, requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
2
+ import { contextContentDigest, validateContextSnapshotRef } from "../context/contextSnapshot.js";
2
3
  import { validateReviewConfig } from "../review/reviewConfig.js";
3
4
  import { taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
4
5
  import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
5
6
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
6
- import { assertExecutionGroupTransition, validateExecutionGroup } from "../execution/executionGroup.js";
7
+ import { assertExecutionGroupTransition, validateExecutionGroup, WORK_ITEM_EXPLORATION_STAGES } from "../execution/executionGroup.js";
7
8
  const TERMINAL_STATUSES = [
8
9
  "completed",
9
10
  "failed",
@@ -12,7 +13,7 @@ const TERMINAL_STATUSES = [
12
13
  export function createWorkItem(id, taskId, input, now) {
13
14
  const timestamp = now.toISOString();
14
15
  return validateWorkItem({
15
- schemaVersion: 9,
16
+ schemaVersion: 12,
16
17
  id: requireIdentity(id, "Work Item id"),
17
18
  taskId: requireIdentity(taskId, "Task id"),
18
19
  title: requireText(input.title, "Work item title"),
@@ -247,8 +248,8 @@ export function recordWorkItemWorkspaceDisposition(workItem, disposition, now) {
247
248
  });
248
249
  }
249
250
  export function validateWorkItem(workItem) {
250
- if (workItem.schemaVersion !== 9)
251
- throw new Error("WorkItem must use schemaVersion 9.");
251
+ if (workItem.schemaVersion !== 12)
252
+ throw new Error("WorkItem must use schemaVersion 12.");
252
253
  validateTaskRecordReference({ taskId: workItem.taskId, localId: workItem.id }, "workItem");
253
254
  requireIdentity(workItem.taskId, "Task id");
254
255
  requireText(workItem.title, "Work item title");
@@ -283,6 +284,7 @@ export function validateWorkItem(workItem) {
283
284
  }
284
285
  groupIds.add(group.id);
285
286
  }
287
+ validateWorkItemExplorationHistory(workItem.executionGroups);
286
288
  if (workItem.currentExecutionGroupId !== undefined) {
287
289
  requireIdentity(workItem.currentExecutionGroupId, "ExecutionGroup id");
288
290
  if (!groupIds.has(workItem.currentExecutionGroupId)) {
@@ -309,6 +311,16 @@ export function validateWorkItem(workItem) {
309
311
  if (candidate.workItemRevision > workItem.revision) {
310
312
  throw new Error("Work Item candidate revision cannot exceed the Work Item revision.");
311
313
  }
314
+ if (candidate.executionGroupId !== undefined) {
315
+ const group = workItem.executionGroups.find(({ id }) => id === candidate.executionGroupId);
316
+ if (group?.stage !== undefined) {
317
+ if (group.stage.stage !== "resolve"
318
+ || group.resolution?.decision !== "accept"
319
+ || !group.resolution.selectedLaneIds.includes(candidate.executionLaneId)) {
320
+ throw new Error("An exploration Candidate must come from the accepted Resolve stage.");
321
+ }
322
+ }
323
+ }
312
324
  });
313
325
  const currentCandidate = currentWorkItemCandidate(workItem);
314
326
  if (workItem.status === "awaiting_acceptance" && currentCandidate === undefined) {
@@ -351,6 +363,130 @@ export function validateWorkItem(workItem) {
351
363
  }
352
364
  return workItem;
353
365
  }
366
+ /**
367
+ * Project the next legal exploration stage from immutable WorkItem Group
368
+ * history. Retry repeats the current stage; retry at Resolve begins the next
369
+ * round. Reject has no continuation, while blocked follows the same bounded
370
+ * retry path after the Leader settles any required InputRequest.
371
+ */
372
+ export function planWorkItemExplorationStage(workItem, input) {
373
+ validateWorkItem(workItem);
374
+ const snapshot = validateContextSnapshotRef(input.contextSnapshotRef);
375
+ const capacity = executionStrategyCapacity(input.strategy);
376
+ const current = currentWorkItemExecutionGroup(workItem);
377
+ if (workItem.executionGroups.length === 0) {
378
+ if (workItem.status !== "pending" && workItem.status !== "running") {
379
+ throw new Error(`Work Item cannot begin exploration from ${workItem.status}: ${workItem.id}.`);
380
+ }
381
+ if (input.mode === undefined) {
382
+ throw new Error("The first exploration stage requires a mode.");
383
+ }
384
+ if (input.convergence === undefined) {
385
+ throw new Error("The first exploration stage requires a candidate convergence policy.");
386
+ }
387
+ const resourceBudget = requireNewStageResourceBudget(input.resourceBudget);
388
+ const resources = requireNewStageResources(input.resources);
389
+ return {
390
+ schemaVersion: 1,
391
+ mode: input.mode,
392
+ stage: "plan",
393
+ round: 1,
394
+ stageAttempt: 1,
395
+ maxRounds: requirePositiveInteger(input.maxRounds ?? 1, "Exploration max rounds"),
396
+ budget: {
397
+ maxLanes: capacity,
398
+ maxAttempts: requirePositiveInteger(input.maxAttempts ?? 1, "Exploration stage max attempts"),
399
+ ...resourceBudget
400
+ },
401
+ resources,
402
+ contextSnapshotRef: snapshot,
403
+ parentResults: [],
404
+ ...(input.convergence === undefined ? {} : { convergence: input.convergence })
405
+ };
406
+ }
407
+ if (current === undefined || current.stage === undefined) {
408
+ throw new Error("A Work Item cannot mix single and exploration ExecutionGroups.");
409
+ }
410
+ if (current.resolution === undefined) {
411
+ throw new Error(`Execution stage is still active: ${current.id}.`);
412
+ }
413
+ if (input.mode !== undefined && input.mode !== current.stage.mode) {
414
+ throw new Error("Work Item exploration mode is immutable.");
415
+ }
416
+ if (input.maxRounds !== undefined && input.maxRounds !== current.stage.maxRounds) {
417
+ throw new Error("Work Item exploration maxRounds is immutable.");
418
+ }
419
+ const parentResults = selectedParentResults(current);
420
+ const base = {
421
+ schemaVersion: 1,
422
+ mode: current.stage.mode,
423
+ maxRounds: current.stage.maxRounds,
424
+ contextSnapshotRef: snapshot,
425
+ parentResults,
426
+ ...(current.stage.convergence === undefined
427
+ ? {}
428
+ : { convergence: current.stage.convergence })
429
+ };
430
+ if (current.resolution.decision === "reject") {
431
+ throw new Error(`Rejected exploration has no continuation: ${current.id}.`);
432
+ }
433
+ if (current.resolution.decision === "accept") {
434
+ if (current.stage.stage === "resolve") {
435
+ throw new Error(`Accepted Resolve stage already completed exploration: ${current.id}.`);
436
+ }
437
+ const resourceBudget = requireNewStageResourceBudget(input.resourceBudget);
438
+ const resources = requireNewStageResources(input.resources);
439
+ return {
440
+ ...base,
441
+ stage: nextExplorationStage(current.stage.stage),
442
+ round: current.stage.round,
443
+ stageAttempt: 1,
444
+ budget: {
445
+ maxLanes: capacity,
446
+ maxAttempts: requirePositiveInteger(input.maxAttempts ?? 1, "Exploration stage max attempts"),
447
+ ...resourceBudget
448
+ },
449
+ resources
450
+ };
451
+ }
452
+ if (current.stage.stage === "resolve" && current.resolution.decision === "retry") {
453
+ if (current.stage.round >= current.stage.maxRounds) {
454
+ throw new Error(`Work Item exploration round budget is exhausted: ${workItem.id}.`);
455
+ }
456
+ const resourceBudget = requireNewStageResourceBudget(input.resourceBudget);
457
+ const resources = requireNewStageResources(input.resources);
458
+ return {
459
+ ...base,
460
+ stage: "plan",
461
+ round: current.stage.round + 1,
462
+ stageAttempt: 1,
463
+ budget: {
464
+ maxLanes: capacity,
465
+ maxAttempts: requirePositiveInteger(input.maxAttempts ?? 1, "Exploration stage max attempts"),
466
+ ...resourceBudget
467
+ },
468
+ resources
469
+ };
470
+ }
471
+ if (current.stage.stageAttempt >= current.stage.budget.maxAttempts) {
472
+ throw new Error(`Work Item exploration stage attempt budget is exhausted: ${workItem.id}.`);
473
+ }
474
+ if (capacity !== current.stage.budget.maxLanes) {
475
+ throw new Error("A retried exploration stage must keep its frozen Lane budget.");
476
+ }
477
+ if (input.maxAttempts !== undefined
478
+ && input.maxAttempts !== current.stage.budget.maxAttempts) {
479
+ throw new Error("A retried exploration stage must keep its frozen attempt budget.");
480
+ }
481
+ return {
482
+ ...base,
483
+ stage: current.stage.stage,
484
+ round: current.stage.round,
485
+ stageAttempt: current.stage.stageAttempt + 1,
486
+ budget: current.stage.budget,
487
+ ...(current.stage.resources === undefined ? {} : { resources: current.stage.resources })
488
+ };
489
+ }
354
490
  /** Resolve the WorkItem's current execution iteration. */
355
491
  export function currentWorkItemExecutionGroup(workItem) {
356
492
  return workItem.currentExecutionGroupId === undefined
@@ -361,6 +497,127 @@ export function currentWorkItemExecutionGroup(workItem) {
361
497
  export function workItemExecutionGroupById(workItem, executionGroupId) {
362
498
  return workItem.executionGroups.find(({ id }) => id === executionGroupId);
363
499
  }
500
+ /**
501
+ * True when a Run failure belongs to the WorkItem's current unresolved Lane.
502
+ * Such a failure is Lane-bounded: the WorkItem remains running so the Leader
503
+ * can reuse completed siblings and retry only this failed attempt.
504
+ */
505
+ export function workItemOwnsUnresolvedExecutionLane(workItem, executionGroupId, executionLaneId) {
506
+ if (executionGroupId === undefined
507
+ || executionLaneId === undefined
508
+ || workItem.currentExecutionGroupId !== executionGroupId)
509
+ return false;
510
+ const group = workItem.executionGroups.find(({ id }) => id === executionGroupId);
511
+ return group !== undefined
512
+ && group.resolution === undefined
513
+ && group.lanes.some(({ id, status }) => (id === executionLaneId && status === "failed"));
514
+ }
515
+ function validateWorkItemExplorationHistory(groups) {
516
+ const staged = groups.filter(({ stage }) => stage !== undefined);
517
+ if (staged.length === 0)
518
+ return;
519
+ if (staged.length !== groups.length) {
520
+ throw new Error("A Work Item cannot mix single and exploration ExecutionGroups.");
521
+ }
522
+ const first = groups[0].stage;
523
+ if (first.stage !== "plan"
524
+ || first.round !== 1
525
+ || first.stageAttempt !== 1
526
+ || first.parentResults.length !== 0) {
527
+ throw new Error("Work Item exploration must begin at Plan round 1 attempt 1.");
528
+ }
529
+ for (let index = 1; index < groups.length; index += 1) {
530
+ const previous = groups[index - 1];
531
+ const current = groups[index];
532
+ const before = previous.stage;
533
+ const after = current.stage;
534
+ if (previous.resolution === undefined) {
535
+ throw new Error(`Exploration stage must resolve before its successor: ${previous.id}.`);
536
+ }
537
+ if (previous.resolution.decision === "reject") {
538
+ throw new Error(`Rejected exploration cannot have a successor: ${previous.id}.`);
539
+ }
540
+ if (after.mode !== before.mode
541
+ || after.maxRounds !== before.maxRounds
542
+ || JSON.stringify(after.convergence) !== JSON.stringify(before.convergence)) {
543
+ throw new Error("Work Item exploration mode, maxRounds and convergence policy are immutable.");
544
+ }
545
+ const parents = selectedParentResults(previous);
546
+ if (JSON.stringify(after.parentResults) !== JSON.stringify(parents)) {
547
+ throw new Error(`Exploration parentResults do not match ${previous.id}.`);
548
+ }
549
+ if (previous.resolution.decision === "accept") {
550
+ if (before.stage === "resolve"
551
+ || after.stage !== nextExplorationStage(before.stage)
552
+ || after.round !== before.round
553
+ || after.stageAttempt !== 1) {
554
+ throw new Error(`Exploration stage transition is invalid: ${previous.id}/${current.id}.`);
555
+ }
556
+ continue;
557
+ }
558
+ if (before.stage === "resolve" && previous.resolution.decision === "retry") {
559
+ if (before.round >= before.maxRounds
560
+ || after.stage !== "plan"
561
+ || after.round !== before.round + 1
562
+ || after.stageAttempt !== 1) {
563
+ throw new Error(`Exploration round transition is invalid: ${previous.id}/${current.id}.`);
564
+ }
565
+ continue;
566
+ }
567
+ if (after.stage !== before.stage
568
+ || after.round !== before.round
569
+ || after.stageAttempt !== before.stageAttempt + 1
570
+ || after.stageAttempt > before.budget.maxAttempts
571
+ || JSON.stringify(after.budget) !== JSON.stringify(before.budget)
572
+ || JSON.stringify(after.resources) !== JSON.stringify(before.resources)) {
573
+ throw new Error(`Exploration retry transition is invalid: ${previous.id}/${current.id}.`);
574
+ }
575
+ }
576
+ }
577
+ function requireNewStageResourceBudget(value) {
578
+ if (value?.maxTokens === undefined
579
+ || value.maxToolCalls === undefined
580
+ || value.maxWallClockSeconds === undefined) {
581
+ throw new Error("A new exploration stage requires token, tool-call and wall-clock budgets.");
582
+ }
583
+ return {
584
+ maxTokens: requirePositiveInteger(value.maxTokens, "Exploration stage max tokens"),
585
+ maxToolCalls: requirePositiveInteger(value.maxToolCalls, "Exploration stage max tool calls"),
586
+ maxWallClockSeconds: requirePositiveInteger(value.maxWallClockSeconds, "Exploration stage max wall-clock seconds")
587
+ };
588
+ }
589
+ function requireNewStageResources(value) {
590
+ if (value === undefined) {
591
+ throw new Error("A new exploration stage requires resource completion policy.");
592
+ }
593
+ return value;
594
+ }
595
+ function selectedParentResults(group) {
596
+ if (group.resolution === undefined) {
597
+ throw new Error(`ExecutionGroup is unresolved: ${group.id}.`);
598
+ }
599
+ return group.resolution.selectedLaneIds.map((laneId) => {
600
+ const lane = group.lanes.find(({ id }) => id === laneId);
601
+ if (lane?.result === undefined) {
602
+ throw new Error(`Selected ExecutionLane result is missing: ${group.id}/${laneId}.`);
603
+ }
604
+ return {
605
+ executionGroupId: group.id,
606
+ executionLaneId: lane.id,
607
+ resultDigest: contextContentDigest(lane.result)
608
+ };
609
+ });
610
+ }
611
+ function nextExplorationStage(stage) {
612
+ const index = WORK_ITEM_EXPLORATION_STAGES.indexOf(stage);
613
+ const next = WORK_ITEM_EXPLORATION_STAGES[index + 1];
614
+ if (next === undefined)
615
+ throw new Error("Resolve is the final exploration stage.");
616
+ return next;
617
+ }
618
+ function executionStrategyCapacity(strategy) {
619
+ return requirePositiveInteger(strategy.mode === "fixed" ? strategy.count : strategy.max, "Execution strategy capacity");
620
+ }
364
621
  function validateWorkItemExecutionGroup(group, taskId, workItemId) {
365
622
  validateExecutionGroup(group);
366
623
  if (group.taskId !== taskId || group.purpose !== "execution") {
@@ -288,6 +288,48 @@ yui task work isolate <task-id>/<work-item-id>
288
288
  yui task work dispatch <task-id>/<work-item-id> --input "完成实现并运行聚焦测试"
289
289
  ```
290
290
 
291
+ 派发默认仍为 `single`。Leader 可在全新 WorkItem 上显式开启有界多路探索;每个
292
+ 接受的阶段按 `Plan → Generate → Compare → Synthesize → Verify → Resolve`
293
+ 推进,只有接受后的 Resolve 阶段会物化既有的单一 Candidate:
294
+
295
+ ```sh
296
+ yui task work dispatch <task-id>/<work-item-id> \
297
+ --mode parallel-diverse --max-rounds 2 --stage-max-attempts 2 \
298
+ --strategy fixed:2 --lane-role critic \
299
+ --stage-max-tokens 240000 --stage-max-tool-calls 200 \
300
+ --stage-max-seconds 1800 --stage-quorum 2
301
+ yui task work group resolve <task-id>/<work-item-id> \
302
+ --decision accept --summary "Plan 证据充分"
303
+ ```
304
+
305
+ 每个阶段都是新的不可变 ExecutionGroup;其 ContextSnapshot 与选中的父 Lane
306
+ 结果都以持久引用衔接。`retry` 在阶段尝试预算内重做当前阶段,而 Resolve 上的
307
+ `retry` 才进入下一轮,并受最大轮次约束。
308
+
309
+ 每个新阶段还会冻结一份 Resource Broker 契约:token、工具调用和墙钟预算,
310
+ quorum 与 deadline,straggler 窗口,以及继续增加 Lane 所需的最低边际价值。
311
+ 省略这些参数时复用现有 context budget 与 runtime-health 时间窗;同一阶段的 retry
312
+ 累计原有花费并共享绝对 deadline。执行、Lane retry 和 Reviewer panel 准入统一核算
313
+ Home、Task、WorkItem、Group、Provider、Agent 和模型层级的活动 Lane;容量不足的
314
+ Lane 会耐久保留为 pending,不会把整个 Group 判失败。容量释放或 deadline 到达会沿
315
+ 既有 actionability 路径唤醒 Leader,重跑同一 dispatch 即按冻结输入继续。释放的
316
+ 容量优先留给最早且当前可准入的等待 Lane;受 Provider 或 Agent 限制的队首
317
+ 不会阻塞独立资源域继续推进。Provider 限流仍沿用既有的原地重试窗口,不会扩散成
318
+ 兄弟 Lane 失败。
319
+
320
+ Leader 可在接受 Group 时增加 `--early-stop <0-100>`。只有 quorum 已满足且 T5 的
321
+ Verify/Resolve 证据证明验收充分时,Yui 才允许提前终止;它只跳过从未启动的 Lane,
322
+ 运行中的 straggler 会被报告并保留,不会因为省费被自动杀死。证据不足时,预算或
323
+ deadline 耗尽会把阶段留给 Leader 决议,绝不会把薄证据转换成成功。
324
+
325
+ 新建探索历史还会冻结结构化候选收敛契约。Yui 将当前阶段的精确 JSON 形状追加到
326
+ 每个 Lane assignment,并在 Leader 推进前校验入选报告:Compare 必须显式划分
327
+ 重复簇,并以直接来源、可执行检查或冻结产物支持每条入选路线;Synthesize 对调研
328
+ 使用 claim/evidence 表、对架构使用决策矩阵、对代码只选择一个冻结 Git snapshot。
329
+ Verify 必须使用与入选 Synthesize 作者不同的 Role。只有逐条验收证据完整时才能得到
330
+ `passed` 并由 Resolve 接受 Candidate;显式 gap 只能形成 `next-round`,再通过有界的
331
+ Resolve `retry` 继续。票数和衍生分析可以作为报告上下文,但不能替代直接证据。
332
+
291
333
  每个 Agent binding 只有一套 adapter-specific 权限枚举配置:`default` 遵循
292
334
  provider 默认行为;`bypass` 编译 provider 支持的 bypass flag;`configured`
293
335
  保留其中显式设置的原生选项。Codex 选项是 `sandbox` 和 `approval`;Claude 选项是
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,