@zq-silk/yui 0.8.9 → 0.10.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 (51) hide show
  1. package/ARCHITECTURE.md +48 -46
  2. package/README.md +72 -42
  3. package/dist/cli/commandCatalog.js +16 -8
  4. package/dist/cli.js +27 -32
  5. package/dist/commands/executionAuditCommands.js +2 -2
  6. package/dist/commands/globalRoleCommands.js +0 -12
  7. package/dist/commands/sessionCommands.js +116 -0
  8. package/dist/commands/taskBaseCommands.js +1 -11
  9. package/dist/commands/taskCommands.js +123 -340
  10. package/dist/commands/taskCompletionGate.js +15 -12
  11. package/dist/commands/taskContextCommand.js +18 -11
  12. package/dist/commands/taskNextActionCommand.js +4 -5
  13. package/dist/commands/taskWorkspaceCommands.js +2 -2
  14. package/dist/controller/clientRuntime.js +65 -0
  15. package/dist/controller/fileSchedulerStoreAdapter.js +2 -49
  16. package/dist/doctor/doctor.js +16 -12
  17. package/dist/execution/executionGroup.js +0 -3
  18. package/dist/executor/agentAdapter.js +39 -42
  19. package/dist/executor/agentExecutor.js +4 -2
  20. package/dist/executor/codexConfigConflict.js +40 -16
  21. package/dist/executor/effectiveLaunch.js +33 -3
  22. package/dist/executor/fileRoleLaunchPlanner.js +15 -24
  23. package/dist/integration/deliveryObligation.js +72 -0
  24. package/dist/integration/gitIntegrationService.js +1 -1
  25. package/dist/lifecycle/exactRunTerminalization.js +12 -8
  26. package/dist/observability/orchestrationMetrics.js +12 -26
  27. package/dist/profile/agentProfile.js +1 -1
  28. package/dist/repository/taskBaseFreshness.js +5 -11
  29. package/dist/repository/taskWorkspaceCoordinator.js +2 -0
  30. package/dist/repository/taskWorkspacePreparer.js +173 -26
  31. package/dist/review/reviewRound.js +41 -24
  32. package/dist/role/role.js +0 -9
  33. package/dist/runtime/{firstProgressStopLoss.js → firstProgressAdvisory.js} +7 -21
  34. package/dist/scheduler/leaderWakeupProcessor.js +0 -25
  35. package/dist/setup/setupCommand.js +0 -5
  36. package/dist/storage/migration/productionRegistry.js +138 -0
  37. package/dist/storage/sqliteStore.js +2 -1
  38. package/dist/storage/taskStore.js +27 -27
  39. package/dist/storage/upgrade/upgradeOrchestrator.js +35 -7
  40. package/dist/task/completionReadiness.js +35 -25
  41. package/dist/task/nextAction.js +70 -78
  42. package/dist/task/task.js +12 -19
  43. package/dist/web/assets/client/components.js +3 -1
  44. package/dist/web/assets/client/i18n.js +6 -0
  45. package/dist/web/webSnapshot.js +0 -3
  46. package/i18n/README.zh-CN.md +33 -24
  47. package/package.json +1 -1
  48. package/skills/yui-leader/SKILL.md +55 -52
  49. package/skills/yui-operator/SKILL.md +31 -40
  50. package/skills/yui-reviewer/SKILL.md +18 -12
  51. package/skills/yui-worker/SKILL.md +5 -3
@@ -4,15 +4,14 @@ import { homedir } from "node:os";
4
4
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
5
5
  import { parse } from "smol-toml";
6
6
  /**
7
- * Inspects the local, file-backed Codex configuration layers supported by Yui
8
- * that can conflict with launch-owned settings. Remote and platform-managed
9
- * layers are outside this compatibility boundary. Project files are considered
10
- * only when the directory, project root, or project root is trusted.
7
+ * Reports the local, file-backed Codex configuration layers supported by Yui.
8
+ * Yui's invocation-local launch settings take precedence over ordinary local
9
+ * layers without mutating those files. Higher-precedence managed policy is
10
+ * reported separately because an invocation cannot override it. Remote
11
+ * managed layers remain outside this reporting boundary. Project files are
12
+ * considered only when the directory, project root, or repository root is
13
+ * trusted.
11
14
  */
12
- export function inspectCodexDeveloperInstructions(input) {
13
- return inspectCodexConfigKeys(input, ["developer_instructions"])
14
- .developerInstructions;
15
- }
16
15
  export function inspectCodexLaunchConfig(input) {
17
16
  return inspectCodexConfigKeys(input, ["developer_instructions", "notify"]);
18
17
  }
@@ -49,12 +48,17 @@ function inspectCodexConfigKeys(input, keys) {
49
48
  ? withExactWorkspaceTrust(discovery, input.workspace)
50
49
  : discovery);
51
50
  const candidates = [
52
- ...discoveryPaths.map((path) => ({ path, keys })),
51
+ ...discoveryPaths.map((path) => ({
52
+ path,
53
+ keys,
54
+ precedence: "session-overridable"
55
+ })),
53
56
  ...projectPaths.map((path) => ({
54
57
  path,
55
- keys: keys.filter((key) => key === "developer_instructions")
58
+ keys: keys.filter((key) => key === "developer_instructions"),
59
+ precedence: "session-overridable"
56
60
  })),
57
- { path: managedPath, keys }
61
+ { path: managedPath, keys, precedence: "managed" }
58
62
  ];
59
63
  let developerInstructions = { status: "absent" };
60
64
  let notify = { status: "absent" };
@@ -71,16 +75,36 @@ function inspectCodexConfigKeys(input, keys) {
71
75
  catch (error) {
72
76
  throw unreliableInspection(candidate.path, error);
73
77
  }
74
- if (developerInstructions.status === "absent"
75
- && configured.has("developer_instructions")) {
76
- developerInstructions = { status: "configured", source: candidate.path };
78
+ if (configured.has("developer_instructions")) {
79
+ developerInstructions = {
80
+ status: "configured",
81
+ source: candidate.path,
82
+ precedence: candidate.precedence
83
+ };
77
84
  }
78
- if (notify.status === "absent" && configured.has("notify")) {
79
- notify = { status: "configured", source: candidate.path };
85
+ if (configured.has("notify")) {
86
+ notify = {
87
+ status: "configured",
88
+ source: candidate.path,
89
+ precedence: candidate.precedence
90
+ };
80
91
  }
81
92
  }
82
93
  return { developerInstructions, notify };
83
94
  }
95
+ export function assertCodexLaunchOverridesAvailable(inspection, keys) {
96
+ for (const key of keys) {
97
+ const configured = inspection[key];
98
+ if (configured.status !== "configured" || configured.precedence !== "managed") {
99
+ continue;
100
+ }
101
+ const nativeKey = key === "developerInstructions"
102
+ ? "developer_instructions"
103
+ : "notify";
104
+ throw new Error(`Codex ${nativeKey} is controlled by higher-precedence managed configuration at `
105
+ + `${configured.source}; Yui cannot apply its invocation-local ${nativeKey} value.`);
106
+ }
107
+ }
84
108
  function withExactWorkspaceTrust(discovery, workspace) {
85
109
  return {
86
110
  ...discovery,
@@ -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.");
@@ -13,7 +13,6 @@ import { serializeRunBootstrapEnvelope, serializeRunHostRecoveryEnvelope } from
13
13
  import { serializeProviderRetryEnvelope } from "../run/providerRetry.js";
14
14
  import { prefixYuiTitleInput } from "../run/runIdentity.js";
15
15
  import { resolveAgentAdapter } from "./agentAdapter.js";
16
- import { inspectCodexLaunchConfig } from "./codexConfigConflict.js";
17
16
  import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
18
17
  import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
19
18
  import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
@@ -26,6 +25,7 @@ import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
26
25
  import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
27
26
  import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
28
27
  import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
28
+ import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from "./codexConfigConflict.js";
29
29
  /** Builds managed native Agent launches from the authoritative Task records. */
30
30
  export class FileRoleLaunchPlanner {
31
31
  home;
@@ -274,6 +274,19 @@ export class FileRoleLaunchPlanner {
274
274
  const adapter = resolveAgentAdapter(binding.adapterId);
275
275
  const effectiveWorkspace = effective.workspace.root;
276
276
  const agentWorkspace = nativeAgentWorkspace(effective.workspace);
277
+ if (adapter.id === "codex") {
278
+ const codexConfig = inspectCodexLaunchConfig({
279
+ environment: launchEnvironment,
280
+ workspace: agentWorkspace,
281
+ profile: binding.config.adapterId === "codex"
282
+ ? binding.config.profile
283
+ : undefined,
284
+ trustWorkspace: true
285
+ });
286
+ assertCodexLaunchOverridesAvailable(codexConfig, owner.scope !== "task" || input.runId === undefined
287
+ ? ["developerInstructions", "notify"]
288
+ : ["developerInstructions"]);
289
+ }
277
290
  const runtimeIsolation = input.runtimeIsolation === undefined
278
291
  ? undefined
279
292
  : parseTaskRuntimeIsolationDescriptor(JSON.stringify(input.runtimeIsolation));
@@ -309,25 +322,6 @@ export class FileRoleLaunchPlanner {
309
322
  sessionManifestDigest: bootstrap.manifest.digest,
310
323
  sessionCliPath: bootstrap.sessionCliPath
311
324
  };
312
- const codexConfig = binding.config.adapterId === "codex"
313
- ? inspectCodexLaunchConfig({
314
- environment: {
315
- ...operationalSourceEnvironment,
316
- ...agentSourceEnvironment,
317
- ...launchEnvironment
318
- },
319
- workspace: agentWorkspace,
320
- profile: binding.config.profile,
321
- trustWorkspace: true
322
- })
323
- : undefined;
324
- if (codexConfig?.notify.status === "configured"
325
- && (owner.scope !== "task" || input.runId === undefined)) {
326
- throw new Error("Codex notify is already configured by "
327
- + `${codexConfig.notify.source}; this interactive Yui Session requires exclusive `
328
- + "ownership of the structured notify callback and refuses to replace or be replaced "
329
- + "by native configuration.");
330
- }
331
325
  const managedRun = owner.scope === "task" && input.runId !== undefined
332
326
  ? this.store.getAgentRun(owner.taskId, input.runId)
333
327
  : null;
@@ -353,10 +347,7 @@ export class FileRoleLaunchPlanner {
353
347
  config: effectiveConfig,
354
348
  workspace: agentWorkspace,
355
349
  ...(sessionTitle === undefined ? {} : { sessionTitle }),
356
- ...sessionContext,
357
- ...(codexConfig === undefined
358
- ? {}
359
- : { codexDeveloperInstructions: codexConfig.developerInstructions })
350
+ ...sessionContext
360
351
  };
361
352
  if (input.mode === "resume"
362
353
  && knownNativeSessionId !== undefined
@@ -0,0 +1,72 @@
1
+ import { governingWorkItemCandidate } from "../workItem/workItem.js";
2
+ /**
3
+ * Delivery obligations follow the Candidate that currently governs each
4
+ * WorkItem. Older Candidates and their ChangeSets remain audit evidence, but
5
+ * they do not keep a Task open after a replacement Candidate is accepted.
6
+ */
7
+ export function governingChangeSets(workItems, changeSets) {
8
+ const selected = new Map();
9
+ for (const item of workItems) {
10
+ const candidate = governingWorkItemCandidate(item);
11
+ if (candidate === undefined)
12
+ continue;
13
+ for (const changeSet of changeSets) {
14
+ if (changeSet.workItemId !== item.id)
15
+ continue;
16
+ const expectedHead = candidateProjectHead(candidate, changeSet.projectId);
17
+ if (expectedHead !== undefined && changeSet.headCommit !== expectedHead)
18
+ continue;
19
+ const key = `${item.id}\0${changeSet.projectId}`;
20
+ const current = selected.get(key);
21
+ if (current === undefined || compareChangeSets(current, changeSet) < 0) {
22
+ selected.set(key, changeSet);
23
+ }
24
+ }
25
+ }
26
+ return [...selected.values()].sort(compareChangeSets);
27
+ }
28
+ export function changeSetDeliverySettled(changeSet, integrations, queueEntries = []) {
29
+ if (integrations.some((attempt) => (attempt.status === "committed" && attempt.changeSetIds.includes(changeSet.id))))
30
+ return true;
31
+ const latestQueueEntry = queueEntries
32
+ .filter((entry) => entry.changeSetId === changeSet.id)
33
+ .sort(compareQueueEntries)
34
+ .at(-1);
35
+ return latestQueueEntry?.status === "superseded";
36
+ }
37
+ export function latestGoverningQueueEntries(changeSets, queueEntries) {
38
+ const governingIds = new Set(changeSets.map(({ id }) => id));
39
+ const latest = new Map();
40
+ for (const entry of queueEntries) {
41
+ if (!governingIds.has(entry.changeSetId))
42
+ continue;
43
+ const current = latest.get(entry.changeSetId);
44
+ if (current === undefined || compareQueueEntries(current, entry) < 0) {
45
+ latest.set(entry.changeSetId, entry);
46
+ }
47
+ }
48
+ return [...latest.values()].sort(compareQueueEntries);
49
+ }
50
+ /**
51
+ * Historical blocked attempts are audit evidence once every ChangeSet they
52
+ * reference has been replaced by a newer governing Candidate. Attempts that
53
+ * may still be writing remain blockers regardless of Candidate history.
54
+ */
55
+ export function integrationAttemptRequiresSettlement(attempt, changeSets) {
56
+ if (attempt.status === "running" || attempt.status === "validating")
57
+ return true;
58
+ const governingIds = new Set(changeSets.map(({ id }) => id));
59
+ return attempt.changeSetIds.some((id) => governingIds.has(id));
60
+ }
61
+ function candidateProjectHead(candidate, projectId) {
62
+ return candidate.gitSnapshot?.projects.find((project) => project.projectId === projectId)?.commit
63
+ ?? candidate.taskMainSnapshot?.projects.find((project) => project.projectId === projectId)?.headCommit;
64
+ }
65
+ function compareChangeSets(left, right) {
66
+ return left.createdAt.localeCompare(right.createdAt)
67
+ || left.id.localeCompare(right.id, undefined, { numeric: true });
68
+ }
69
+ function compareQueueEntries(left, right) {
70
+ return left.updatedAt.localeCompare(right.updatedAt)
71
+ || left.id.localeCompare(right.id, undefined, { numeric: true });
72
+ }
@@ -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
- import { projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
3
- import { taskDeliveryPath } from "../task/task.js";
2
+ import { projectFirstProgressAdvisory } from "../runtime/firstProgressAdvisory.js";
4
3
  /** One Task's orchestration cost and advisory projection, with no writes. */
5
4
  export function projectTaskOrchestration(facts) {
6
5
  const evidence = {
@@ -36,14 +35,14 @@ export function projectTaskOrchestration(facts) {
36
35
  const repeatedIdentities = [...integrationIdentities.values()]
37
36
  .reduce((total, count) => total + Math.max(0, count - 1), 0);
38
37
  const leaderSessions = facts.roleSessionSets.find(({ owner }) => owner.roleName === "leader") ?? null;
39
- const firstProgress = projectFirstProgressStopLoss({
38
+ const firstProgress = projectFirstProgressAdvisory({
40
39
  sessions: leaderSessions,
41
40
  events: facts.events,
42
41
  workItems: facts.workItems,
43
42
  reviewRounds: facts.reviewRounds,
44
43
  integrations: facts.integrations
45
44
  });
46
- const advisories = projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgress.exhausted);
45
+ const advisories = projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgress.attentionRecommended);
47
46
  const publicationAt = facts.task.completedAt === undefined
48
47
  ? undefined
49
48
  : facts.publications
@@ -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)),
@@ -87,26 +86,13 @@ export function projectTaskOrchestration(facts) {
87
86
  advisories
88
87
  });
89
88
  }
90
- function projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, stopLoss) {
89
+ function projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgressAttention) {
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:"))));
@@ -150,10 +136,10 @@ function projectAdvisories(facts, classifications, fullRounds, repeatedIdentitie
150
136
  refs: recent.map(({ id }) => `review-round:${id}`)
151
137
  });
152
138
  }
153
- if (stopLoss) {
139
+ if (firstProgressAttention) {
154
140
  result.push({
155
- code: "provider-first-progress-stop-loss",
156
- reason: "Two fresh Leader generations produced no first durable progress; hand off to the unique Operator before another generation.",
141
+ code: "provider-first-progress-advisory",
142
+ reason: "Two fresh Leader generations produced no first durable progress; consider Operator attention before another generation.",
157
143
  refs: [facts.task.id]
158
144
  });
159
145
  }
@@ -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
  ];
@@ -113,23 +113,17 @@ export function assertTaskBaseFreshnessForCompletion(report, options = {}) {
113
113
  const acceptedPublishedTree = options.acceptedPublishedTreeProjectId === entry.projectId;
114
114
  if ((entry.status === "behind" || entry.status === "diverged")
115
115
  && !acceptedPublishedTree) {
116
- throw usageError(`Task ${report.taskId} Project ${entry.projectId} base is ${entry.status}; `
117
- + `run 'yui task base status ${report.taskId} --refresh' and choose an explicit delivery base. `
118
- + "Safe resolutions are to rebase or merge the Task workspace onto the refreshed remote base, "
119
- + "create a clean delivery branch from that base and cherry-pick the Task changes, "
120
- + "or ask the Leader to resolve it. Continuing without rebasing is allowed only after explicitly "
121
- + "recording the unrelated-diff risk.");
116
+ warnings.push(`Project ${entry.projectId} base is ${entry.status}; the Leader must choose the delivery base `
117
+ + "and account for any remote-only changes.");
122
118
  }
123
119
  if (entry.workspaceClean === false) {
124
120
  throw usageError(`Task ${report.taskId} Project ${entry.projectId} workspace is dirty; `
125
121
  + "commit, integrate, or clean it before completion.");
126
122
  }
127
123
  if (entry.status === "unknown") {
128
- if (report.refreshed) {
129
- throw usageError(`Task ${report.taskId} Project ${entry.projectId} remote base could not be refreshed: `
130
- + `${entry.error ?? "unknown error"}`);
131
- }
132
- warnings.push(`Project ${entry.projectId} remote base is unknown; local completion continues without a remote claim.`);
124
+ warnings.push(report.refreshed
125
+ ? `Project ${entry.projectId} remote base could not be refreshed: ${entry.error ?? "unknown error"}; local completion continues without a remote claim.`
126
+ : `Project ${entry.projectId} remote base is unknown; local completion continues without a remote claim.`);
133
127
  }
134
128
  }
135
129
  return warnings;
@@ -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);