@zq-silk/yui 0.8.9 → 0.9.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 (35) hide show
  1. package/ARCHITECTURE.md +48 -46
  2. package/README.md +33 -31
  3. package/dist/cli/commandCatalog.js +7 -7
  4. package/dist/cli.js +1 -31
  5. package/dist/commands/executionAuditCommands.js +2 -2
  6. package/dist/commands/taskCommands.js +110 -307
  7. package/dist/commands/taskCompletionGate.js +15 -12
  8. package/dist/commands/taskContextCommand.js +18 -11
  9. package/dist/commands/taskNextActionCommand.js +4 -5
  10. package/dist/commands/taskWorkspaceCommands.js +2 -2
  11. package/dist/execution/executionGroup.js +0 -3
  12. package/dist/executor/agentExecutor.js +4 -2
  13. package/dist/executor/effectiveLaunch.js +33 -3
  14. package/dist/integration/gitIntegrationService.js +1 -1
  15. package/dist/lifecycle/exactRunTerminalization.js +12 -8
  16. package/dist/observability/orchestrationMetrics.js +5 -19
  17. package/dist/profile/agentProfile.js +1 -1
  18. package/dist/repository/taskWorkspaceCoordinator.js +2 -0
  19. package/dist/repository/taskWorkspacePreparer.js +173 -26
  20. package/dist/review/reviewRound.js +41 -24
  21. package/dist/storage/migration/productionRegistry.js +138 -0
  22. package/dist/storage/sqliteStore.js +1 -1
  23. package/dist/storage/taskStore.js +26 -27
  24. package/dist/task/completionReadiness.js +24 -22
  25. package/dist/task/nextAction.js +47 -61
  26. package/dist/task/task.js +12 -19
  27. package/dist/web/assets/client/components.js +3 -1
  28. package/dist/web/assets/client/i18n.js +6 -0
  29. package/dist/web/webSnapshot.js +0 -3
  30. package/i18n/README.zh-CN.md +18 -19
  31. package/package.json +1 -1
  32. package/skills/yui-leader/SKILL.md +47 -43
  33. package/skills/yui-operator/SKILL.md +24 -33
  34. package/skills/yui-reviewer/SKILL.md +18 -12
  35. package/skills/yui-worker/SKILL.md +5 -3
@@ -2,14 +2,13 @@ import { usageError } from "../errors/cliError.js";
2
2
  import { GitIntegrationService } from "../integration/gitIntegrationService.js";
3
3
  import { createIntegrationAttempt } from "../integration/integrationAttempt.js";
4
4
  import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
5
- import { taskDeliveryPath } from "../task/task.js";
6
5
  import { publicationExternalKey } from "../task/publicationReference.js";
7
6
  import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
8
7
  import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
9
8
  /**
10
9
  * Verify the one supported ancestry waiver before `task complete` mutates any
11
10
  * durable state. The explicit Publication must be the current verified merged
12
- * record, bind the exact physical Task head (and, for integrated delivery, its
11
+ * record, bind the exact physical Task head (and, when WorkItems exist, its
13
12
  * completed final Review), and name an ancestry-divergent commit with the
14
13
  * exact same Git tree.
15
14
  */
@@ -30,12 +29,14 @@ export async function verifyTaskCompletionPublishedTree(taskId, publicationId, s
30
29
  throw usageError(`Publication ${publication.id} must record exact local and remote commits.`);
31
30
  }
32
31
  const workspace = requireTaskWorkspace(store, task);
33
- const integrated = taskDeliveryPath(task) === "integrated";
34
- const latestReview = integrated ? latestTaskFinalReview(store, task.id) : undefined;
35
- if (integrated && (latestReview === undefined
36
- || !isSemanticReviewRound(latestReview, store)
37
- || latestReview.taskCandidate === undefined)) {
38
- throw usageError(`Task ${task.id} requires a latest completed Task-final Review before accepting a published tree.`);
32
+ const latestRecordedReview = latestTaskFinalReview(store, task.id);
33
+ const latestReview = latestRecordedReview !== undefined
34
+ && isSemanticReviewRound(latestRecordedReview, store)
35
+ && latestRecordedReview.taskCandidate !== undefined
36
+ ? latestRecordedReview
37
+ : undefined;
38
+ if (latestRecordedReview !== undefined && latestReview === undefined) {
39
+ throw usageError(`Task ${task.id} has an unsettled Task-final Review obligation before accepting a published tree.`);
39
40
  }
40
41
  const git = options.git ?? new NodeGitWorkspace();
41
42
  const actualHeads = new Map();
@@ -122,7 +123,7 @@ export function assertTaskCompletionPublishedTreeProof(store, task, publicationI
122
123
  || publication.remoteCommit !== proof.remoteCommit) {
123
124
  throw usageError(`Publication evidence changed before Task completion: ${publication.id}.`);
124
125
  }
125
- if (taskDeliveryPath(task) === "integrated") {
126
+ if (proof.reviewRoundId !== undefined) {
126
127
  const latestReview = latestTaskFinalReview(store, task.id);
127
128
  if (latestReview === undefined
128
129
  || latestReview.id !== proof.reviewRoundId
@@ -132,8 +133,8 @@ export function assertTaskCompletionPublishedTreeProof(store, task, publicationI
132
133
  throw usageError(`Task-final Review evidence changed before published-tree completion: ${task.id}.`);
133
134
  }
134
135
  }
135
- else if (proof.reviewRoundId !== undefined) {
136
- throw usageError(`Direct published-tree proof unexpectedly binds a final Review: ${task.id}.`);
136
+ else if (latestTaskFinalReview(store, task.id) !== undefined) {
137
+ throw usageError(`Published-tree proof omitted the Task-final Review: ${task.id}.`);
137
138
  }
138
139
  const actualCommit = actualCandidate.projects.find(({ projectId }) => (projectId === proof.projectId))?.commit;
139
140
  if (actualCommit !== proof.localCommit) {
@@ -183,8 +184,10 @@ export async function reconcileTaskRemoteBaselines(taskId, store, home, options
183
184
  // The delivery contract opts Project-backed Tasks into committed
184
185
  // Integration evidence. Metadata-only Tasks retain their existing local
185
186
  // completion semantics and have no remote baseline to reconcile here.
186
- if (task.projectBindings.length === 0 || task.requireIntegration !== true)
187
+ if (task.projectBindings.length === 0
188
+ || !store.listIntegrationAttempts(task.id).some(({ status }) => status === "committed")) {
187
189
  return [];
190
+ }
188
191
  const workspace = requireTaskWorkspace(store, task);
189
192
  const git = options.git ?? new NodeGitWorkspace();
190
193
  if (git.fetchRemoteHeadIntoWorktree === undefined
@@ -4,7 +4,6 @@ import { formatTimestamp } from "../output/timePresentation.js";
4
4
  import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunStall.js";
5
5
  import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
6
6
  import { projectNextAction } from "../task/nextAction.js";
7
- import { taskDeliveryPath } from "../task/task.js";
8
7
  import { inspectTaskRoleSessionRecovery } from "./taskRoleRuntimeStatus.js";
9
8
  import { summarizeExecutionGroup } from "../execution/executionGroup.js";
10
9
  import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
@@ -54,7 +53,6 @@ export function runTaskContextCommand(args, store) {
54
53
  }
55
54
  return {
56
55
  task,
57
- deliveryPath: taskDeliveryPath(task),
58
56
  execution,
59
57
  reviewConfig: reader.getReviewConfig(),
60
58
  brief: reader.getTaskBrief(task.id),
@@ -141,12 +139,15 @@ export function runTaskContextCommand(args, store) {
141
139
  ...(managedWorkspaces.length === 0
142
140
  ? [" None."]
143
141
  : managedWorkspaces.map((workspace) => (` ${managedWorkspaceLabel(workspace)}: ${workspace.root} (${workspace.entries.filter(({ access }) => access === "write").length} writable / ${workspace.entries.length} Projects)`))),
144
- `Delivery: ${taskDeliveryPath(task)}`,
145
- `Completion evidence: ${task.requireIntegration
146
- ? "WorkItem, ChangeSet, and committed Integration required"
147
- : task.projectBindings.length > 0
142
+ `Type: ${task.type ?? "unspecified"}`,
143
+ `Execution topology: ${workItems.length === 0
144
+ ? "Leader-owned Task main"
145
+ : `${workItems.length} independently owned WorkItem(s), integrated on Task main`}`,
146
+ `Completion evidence: ${task.projectBindings.length === 0
147
+ ? "no Project evidence required"
148
+ : workItems.length === 0
148
149
  ? "clean committed Task main required"
149
- : "no Project evidence required"}`,
150
+ : "each delivered WorkItem requires a ChangeSet and committed Integration"}`,
150
151
  `Global review: ${reviewConfig === null
151
152
  ? "disabled"
152
153
  : `${reviewConfig.roleName} (${reviewConfig.trigger})`}`,
@@ -259,10 +260,13 @@ export function runTaskContextCommand(args, store) {
259
260
  ? []
260
261
  : [` Summary: ${compactText(latestRun.summary)}`])
261
262
  ]),
262
- ...renderWorkItemReviews(reviewRounds.filter((round) => round.workItemId === item.id))
263
+ ...renderReviewRounds(reviewRounds.filter((round) => round.workItemId === item.id))
263
264
  ];
264
265
  })),
265
266
  "",
267
+ "Task-final reviews:",
268
+ ...renderReviewRounds(reviewRounds.filter((round) => (round.scope ?? "work-item") === "task")),
269
+ "",
266
270
  ...recentSection("AgentRuns", agentRuns, (run) => [
267
271
  ` ${run.id} [${run.status}/${run.purpose}] ${run.roleName} via ${run.effective.agentId}/${run.effective.adapterId}`,
268
272
  ` Effective: r${run.effective.sourceDesiredRevision}; Profile intent: ${run.effective.profileAccess}; permission: ${run.effective.permission.strategy}; model: ${run.effective.model ?? "default"}; effort: ${run.effective.effort ?? "default"}`,
@@ -358,16 +362,19 @@ function latestStallKind(events, runId) {
358
362
  .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
359
363
  return event?.payload.kind ?? "workflow-not-progressing";
360
364
  }
361
- function renderWorkItemReviews(rounds) {
365
+ function renderReviewRounds(rounds) {
362
366
  const latest = rounds.at(-1);
363
367
  if (latest === undefined)
364
368
  return [" ReviewRounds: none."];
369
+ const target = (latest.scope ?? "work-item") === "task"
370
+ ? "frozen Task candidate"
371
+ : `Candidate ${latest.candidateId ?? "unavailable"}`;
365
372
  return [
366
- ` ReviewRounds: ${rounds.length}; latest ${latest.id} [${latest.status}] ${latest.scope === "task" ? "Task-final" : "WorkItem"} for ${latest.candidateId} via ${latest.reviewerRoleName} (${latest.requestedBy})`,
373
+ ` ReviewRounds: ${rounds.length}; latest ${latest.id} [${latest.status}] ${latest.scope === "task" ? "Task-final" : "WorkItem"} for ${target} via ${latest.reviewerRoleName} (${latest.requestedBy})`,
367
374
  ` Review base: ${latest.reviewBaseCommit}`,
368
375
  ...(latest.scope === "task"
369
376
  ? [
370
- ` Frozen integrated Task heads: ${latest.taskCandidate?.projects
377
+ ` Frozen Task heads: ${latest.taskCandidate?.projects
371
378
  .map(({ projectId, commit }) => `${projectId}@${commit}`).join(", ") ?? "unavailable"}`,
372
379
  ` Task-final contract: ${latest.taskFinalReviewContract?.digest ?? "global policy"}`,
373
380
  ...(latest.deltaRecheck === undefined
@@ -2,7 +2,6 @@ import { taskNotFound, usageError } from "../errors/cliError.js";
2
2
  import { projectNextAction } from "../task/nextAction.js";
3
3
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
4
4
  import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
5
- import { taskDeliveryPath } from "../task/task.js";
6
5
  import { projectTaskOrchestration } from "../observability/orchestrationMetrics.js";
7
6
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
8
7
  /**
@@ -75,7 +74,7 @@ export function runTaskNextActionCommand(args, store) {
75
74
  managedWorkspaces: reader.listManagedWorkspaces(taskId)
76
75
  });
77
76
  return {
78
- deliveryPath: taskDeliveryPath(facts.task),
77
+ taskType: facts.task.type ?? null,
79
78
  action,
80
79
  repairWave,
81
80
  completionReadiness,
@@ -92,7 +91,7 @@ export function runTaskNextActionCommand(args, store) {
92
91
  }
93
92
  return {
94
93
  kind: "output",
95
- output: renderNextAction(data.action, data.deliveryPath, data.repairWave, data.completionReadiness, data.knowledgeProposals, data.orchestration.advisories),
94
+ output: renderNextAction(data.action, data.taskType, data.repairWave, data.completionReadiness, data.knowledgeProposals, data.orchestration.advisories),
96
95
  data
97
96
  };
98
97
  }
@@ -110,10 +109,10 @@ function repairWaveFor(action, facts) {
110
109
  return null;
111
110
  return planRepairWave(round.id, findings);
112
111
  }
113
- function renderNextAction(action, deliveryPath, repairWave, completionReadiness, knowledgeProposals, orchestrationAdvisories) {
112
+ function renderNextAction(action, taskType, repairWave, completionReadiness, knowledgeProposals, orchestrationAdvisories) {
114
113
  const lines = [
115
114
  `Task: ${action.taskId}`,
116
- `Delivery: ${deliveryPath}`,
115
+ `Type: ${taskType ?? "unspecified"}`,
117
116
  `Next action: ${action.kind}`,
118
117
  `Reason: ${action.reason}`,
119
118
  ...(action.refs.length === 0
@@ -119,8 +119,8 @@ function replaceTaskCommand(args, store, options) {
119
119
  createArgs.push("--project", binding.projectId);
120
120
  createArgs.push("--base", `${binding.projectId}=${binding.baseRef}`);
121
121
  }
122
- if (old.requireIntegration)
123
- createArgs.push("--require-integration");
122
+ if (old.type !== undefined)
123
+ createArgs.push("--type", old.type);
124
124
  const created = runTaskCommand(createArgs, store);
125
125
  if (created.kind !== "output") {
126
126
  throw new Error(`Task replacement could not be created for ${old.id}.`);
@@ -435,9 +435,6 @@ export function validateExecutionTarget(target, taskId) {
435
435
  if (target.kind === "work-item" && target.workItemId === undefined) {
436
436
  throw new Error("WorkItem ExecutionTarget requires a Work Item id.");
437
437
  }
438
- if (target.kind === "task-final-review" && target.candidateId === undefined) {
439
- throw new Error("Task-final ExecutionTarget requires a Candidate id.");
440
- }
441
438
  if (target.workItemId !== undefined)
442
439
  requireIdentity(target.workItemId, "Work Item id");
443
440
  if (target.candidateId !== undefined)
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { hasRecentTurnId, rememberRecentTurnId, validatePendingTurnCompletion, validateRecentTurnIds } from "./turnCompletion.js";
3
- import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
3
+ import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchSnapshotsCompatibleForTaskReview, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
4
4
  import { rebindProviderRuntimeRun, validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
5
5
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
6
6
  export function createRoleSessionSet(owner, activeAgentId, now) {
@@ -67,7 +67,9 @@ export function recordRoleAgentSession(set, input, now) {
67
67
  throw new Error(`Role Agent session effective identity is inconsistent: ${agentId}.`);
68
68
  }
69
69
  if (existing !== undefined && existing.nativeSessionId === nativeSessionId
70
- && !effectiveLaunchSnapshotsCompatible(existing.effective, effective)) {
70
+ && !effectiveLaunchSnapshotsCompatible(existing.effective, effective)
71
+ && !(set.owner.scope === "task"
72
+ && effectiveLaunchSnapshotsCompatibleForTaskReview(existing.effective, effective))) {
71
73
  throw new Error(`Role Agent session effective launch cannot change: ${agentId}.`);
72
74
  }
73
75
  if (existing !== undefined && existing.nativeSessionId !== nativeSessionId
@@ -84,7 +84,9 @@ export function effectiveLaunchSnapshotsCompatible(existing, desired) {
84
84
  * configuration with which it started. A later Run may resume that exact
85
85
  * Session only when the current durable Task workspace proves that every
86
86
  * non-commit workspace identity and every other launch field is unchanged.
87
- * WorkItem, ReviewRound and ExecutionLane workspaces remain strict.
87
+ * A Task-final Reviewer workspace is also mutable between semantic Rounds: it
88
+ * keeps one Role-owned physical identity while its frozen commits and Round
89
+ * identity advance. WorkItem and ExecutionLane workspaces remain strict.
88
90
  */
89
91
  export function effectiveLaunchSnapshotsCompatibleForTaskMain(existing, desired, workspace) {
90
92
  if (effectiveLaunchSnapshotsCompatible(existing, desired))
@@ -94,16 +96,34 @@ export function effectiveLaunchSnapshotsCompatibleForTaskMain(existing, desired,
94
96
  if (workspace === null || workspace === undefined)
95
97
  return false;
96
98
  validateManagedWorkspace(workspace);
97
- if (workspace.owner.type !== "task")
98
- return false;
99
99
  const durableWorkspace = {
100
100
  root: workspace.root,
101
101
  entries: workspace.entries.map((entry) => ({ ...entry }))
102
102
  };
103
103
  if (!isDeepStrictEqual(desired.workspace, durableWorkspace))
104
104
  return false;
105
+ if (workspace.owner.type === "review-round") {
106
+ return effectiveLaunchSnapshotsCompatibleForTaskReview(existing, desired);
107
+ }
108
+ if (workspace.owner.type !== "task")
109
+ return false;
105
110
  return isDeepStrictEqual(taskMainCompatibleSnapshot(existing), taskMainCompatibleSnapshot(desired));
106
111
  }
112
+ /**
113
+ * Same Task Reviewer Role, same stable physical workspace and launch policy,
114
+ * but a new semantic ReviewRound and frozen head. The native Session keeps its
115
+ * conversation; each AgentRun still records the new exact Round snapshot.
116
+ */
117
+ export function effectiveLaunchSnapshotsCompatibleForTaskReview(existing, desired) {
118
+ validateEffectiveLaunchSnapshot(existing);
119
+ validateEffectiveLaunchSnapshot(desired);
120
+ if (existing.reviewRoundId === undefined
121
+ || existing.reviewBaseCommit === undefined
122
+ || desired.reviewRoundId === undefined
123
+ || desired.reviewBaseCommit === undefined)
124
+ return false;
125
+ return isDeepStrictEqual(taskReviewCompatibleSnapshot(existing), taskReviewCompatibleSnapshot(desired));
126
+ }
107
127
  /** Preserves a fixed Session's launch configuration while freezing fresh Task-main Git facts. */
108
128
  export function effectiveLaunchWithTaskMainWorkspace(existing, workspace) {
109
129
  validateEffectiveLaunchSnapshot(existing);
@@ -129,6 +149,16 @@ function taskMainCompatibleSnapshot(snapshot) {
129
149
  }
130
150
  };
131
151
  }
152
+ function taskReviewCompatibleSnapshot(snapshot) {
153
+ const { sourceDesiredRevision: _sourceDesiredRevision, reviewRoundId: _reviewRoundId, reviewBaseCommit: _reviewBaseCommit, workspace, ...launch } = snapshot;
154
+ return {
155
+ ...launch,
156
+ workspace: {
157
+ root: workspace.root,
158
+ entries: workspace.entries.map(({ baseCommit: _baseCommit, baseRef: _baseRef, ...entry }) => entry)
159
+ }
160
+ };
161
+ }
132
162
  export function validateEffectiveLaunchSnapshot(snapshot) {
133
163
  if (snapshot.schemaVersion !== 2) {
134
164
  throw new Error("Effective launch snapshot must use schemaVersion 2.");
@@ -675,7 +675,7 @@ async function integrationCommitPlan(store, taskId, repositoryPath, changeSetIds
675
675
  `${changeSet.baseCommit}..${changeSet.headCommit}`
676
676
  ])).trim().split("\n").filter(Boolean);
677
677
  for (const commit of commits) {
678
- // A direct Task-main recovery may capture commits after they are already
678
+ // A Task-main recovery may capture commits after they are already
679
679
  // present on the exact target. Treat those commits as applied rather
680
680
  // than attempting an empty cherry-pick; the later checks and CAS still
681
681
  // fence the committed Integration to expectedHead.
@@ -83,19 +83,23 @@ export function validateExactRunReviewRound(store, run, options = {}) {
83
83
  return { disposition: "obsolete", round, reason: "review-lane-workspace-lineage-mismatch" };
84
84
  }
85
85
  }
86
- const item = store.getWorkItem(run.taskId, round.workItemId);
87
- if (item === null) {
86
+ const task = store.getTask(run.taskId);
87
+ const taskScope = (round.scope ?? "work-item") === "task";
88
+ const item = taskScope || round.workItemId === undefined
89
+ ? null
90
+ : store.getWorkItem(run.taskId, round.workItemId);
91
+ if (!taskScope && item === null) {
88
92
  return { disposition: "obsolete", round, reason: "review-work-item-missing" };
89
93
  }
90
- const candidate = item.candidates.find(({ id }) => id === round.candidateId);
91
- if (candidate === undefined) {
94
+ const candidate = taskScope
95
+ ? undefined
96
+ : item.candidates.find(({ id }) => id === round.candidateId);
97
+ if (!taskScope && candidate === undefined) {
92
98
  return { disposition: "obsolete", round, reason: "review-candidate-missing" };
93
99
  }
94
- const task = store.getTask(run.taskId);
95
- const taskScope = (round.scope ?? "work-item") === "task";
96
100
  const frozenProjects = taskScope
97
101
  ? round.taskCandidate?.projects
98
- : candidate.gitSnapshot?.projects;
102
+ : candidate?.gitSnapshot?.projects;
99
103
  if (taskScope) {
100
104
  if (task === null || round.taskCandidate === undefined) {
101
105
  return { disposition: "obsolete", round, reason: "review-task-candidate-missing" };
@@ -107,7 +111,7 @@ export function validateExactRunReviewRound(store, run, options = {}) {
107
111
  return { disposition: "obsolete", round, reason: "review-frozen-project-scope-drift" };
108
112
  }
109
113
  }
110
- else if (candidate.gitSnapshot !== undefined
114
+ else if (candidate?.gitSnapshot !== undefined
111
115
  && candidate.gitSnapshot.reviewBaseCommit !== round.reviewBaseCommit) {
112
116
  return { disposition: "obsolete", round, reason: "review-candidate-snapshot-drift" };
113
117
  }
@@ -1,6 +1,5 @@
1
1
  import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
2
2
  import { projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
3
- import { taskDeliveryPath } from "../task/task.js";
4
3
  /** One Task's orchestration cost and advisory projection, with no writes. */
5
4
  export function projectTaskOrchestration(facts) {
6
5
  const evidence = {
@@ -53,7 +52,7 @@ export function projectTaskOrchestration(facts) {
53
52
  .at(-1);
54
53
  return Object.freeze({
55
54
  taskId: facts.task.id,
56
- deliveryPath: taskDeliveryPath(facts.task),
55
+ taskType: facts.task.type ?? null,
57
56
  timeToFirstProjectCommitMs: firstCommitAt === undefined
58
57
  ? null
59
58
  : Math.max(0, Date.parse(firstCommitAt) - Date.parse(facts.task.createdAt)),
@@ -89,24 +88,11 @@ export function projectTaskOrchestration(facts) {
89
88
  }
90
89
  function projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, stopLoss) {
91
90
  const result = [];
92
- if (taskDeliveryPath(facts.task) === "direct"
93
- && (facts.workItems.length > 0 || facts.reviewRounds.length > 0 || facts.integrations.length > 0)) {
91
+ if (facts.task.type === "bugfix" && facts.workItems.length > 0) {
94
92
  result.push({
95
- code: "direct-protocol-overhead",
96
- reason: "Direct delivery accumulated WorkItem, Review, or Integration protocol overhead; keep the fix Leader-direct or explicitly promote it to integrated delivery.",
97
- refs: [
98
- ...facts.workItems.map(({ id }) => `work-item:${id}`),
99
- ...facts.reviewRounds.map(({ id }) => `review-round:${id}`),
100
- ...facts.integrations.map(({ id }) => `integration-attempt:${id}`)
101
- ]
102
- });
103
- }
104
- const initial = facts.workItems.filter(({ dependsOn }) => dependsOn.length === 0);
105
- if (taskDeliveryPath(facts.task) === "integrated" && initial.length > 1) {
106
- result.push({
107
- code: "guarded-workitem-fanout",
108
- reason: `${initial.length} initial WorkItems were created for an integrated Task; start with one bounded fix unless independence is explicit.`,
109
- refs: initial.map(({ id }) => `work-item:${id}`)
93
+ code: "bugfix-workitem-overhead",
94
+ reason: `Bugfix ${facts.task.id} created ${facts.workItems.length} WorkItem(s); bugfixes are Leader-owned, so reclassify expanding scope as a feature before delegating independent delivery units.`,
95
+ refs: facts.workItems.map(({ id }) => `work-item:${id}`)
110
96
  });
111
97
  }
112
98
  const repairItems = facts.workItems.filter((item) => (item.acceptance.some((line) => line.startsWith("review-finding:"))));
@@ -75,7 +75,7 @@ export function builtinAgentProfileInputs() {
75
75
  {
76
76
  id: "reviewer",
77
77
  description: "Review one candidate against the user's core outcome, supported behavior, and direct evidence.",
78
- instructions: "Start from user intent and acceptance criteria. Inspect the complete relevant change and report only reachable, material, actionable problems with direct evidence. Separate defects from verification gaps, and prefer the smallest sufficient correction. Follow the bound Project's Policy and Knowledge for build, test, migration, release, and review expectations; do not import rules from another Project or Task. For normal software delivery, review the frozen integrated Task result as one final ReviewRound rather than inventing a per-WorkItem protocol unless the Project Policy explicitly requires one. Do not turn speculative or extreme edge cases into new state, retries, fallbacks, or protocol. In a ReviewRound-owned workspace you may edit source or tests, run local checks, and optionally commit diagnostic evidence. Never push, integrate, mutate Task state, touch another workspace or stable checkout, or write the real Yui control-plane home. Report complete findings, checks actually run, uncertainty, and bounded next actions through the exact Review yield; Yui preserves the full free-form report. Expose evidence and options to the Leader, who decides.",
78
+ instructions: "Start from user intent and acceptance criteria. Inspect the complete relevant change and report only reachable, material, actionable problems with direct evidence. Separate defects from verification gaps, and prefer the smallest sufficient correction. Follow the bound Project's Policy and Knowledge for build, test, migration, release, and review expectations; do not import rules from another Project or Task. For normal software delivery, review the frozen Task result as one final ReviewRound rather than inventing a per-WorkItem protocol unless the Project Policy explicitly requires one. A Task-final Round has no synthetic WorkItem anchor, and a compatible Reviewer Session may continue across changed-head Rounds without reusing an earlier verdict. Do not turn speculative or extreme edge cases into new state, retries, fallbacks, or protocol. In a ReviewRound-owned workspace you may edit source or tests, run local checks, and optionally commit diagnostic evidence. Never push, integrate, mutate Task state, touch another workspace or stable checkout, or write the real Yui control-plane home. Report complete findings, checks actually run, uncertainty, and bounded next actions through the exact Review yield; Yui preserves the full free-form report. Expose evidence and options to the Leader, who decides.",
79
79
  defaultAccess: "write"
80
80
  }
81
81
  ];
@@ -123,6 +123,8 @@ export class TaskWorkspaceCoordinator {
123
123
  if (round.status !== "completed" && round.status !== "failed") {
124
124
  throw new Error(`ReviewRound must be terminal before cleanup: ${round.id}.`);
125
125
  }
126
+ if (round.workspaceDisposition?.kind === "reassigned")
127
+ return "missing";
126
128
  // Hold the per-Project maintenance fence so a concurrent migrate/rebuild/
127
129
  // archive cannot interleave with worktree removal.
128
130
  const workspace = this.store.getReviewRoundWorkspace(taskId, reviewRoundId);