@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
@@ -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}.`);
@@ -1,9 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { callController, readControllerDiscovery, stopOrphanedFileTaskController, stopPreviousFileTaskController } from "../core/controllerClient.js";
4
+ import { controllerSocketPath } from "../core/controllerEndpoint.js";
4
5
  import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
5
6
  import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
6
7
  import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
8
+ import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
7
9
  import { hasRuntimeLifecycleWork } from "../runtime/lifecycleReservation.js";
8
10
  import { assertControllerStatusIdentity } from "../runtime/exactControlPlane.js";
9
11
  import { EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES } from "./domainIdentity.js";
@@ -11,6 +13,7 @@ import { yuiVersionIdentity } from "../version.js";
11
13
  import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
12
14
  import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
13
15
  import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
16
+ import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
14
17
  const STARTUP_TIMEOUT_MS = 5_000;
15
18
  const POLL_INTERVAL_MS = 50;
16
19
  const ENVIRONMENT_REFRESH_TIMEOUT_MS = 500;
@@ -502,6 +505,63 @@ export class FileTaskWorkflowRuntime {
502
505
  throw new Error(`Global Role runtime session is still active: ${roleName}.`);
503
506
  }
504
507
  }
508
+ /** Drain pending runtime facts while an external maintenance fence is held. */
509
+ async drainController() {
510
+ await ensureFileTaskController(this.home, {
511
+ environment: this.clientOptions.environment
512
+ });
513
+ await callFileTaskController(this.home, "scheduler.scan", {}, {
514
+ ...this.clientOptions,
515
+ requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
516
+ });
517
+ }
518
+ /**
519
+ * Maintenance-only exact stop used after the Controller has fully exited.
520
+ * The dormant candidate fences the durable cleanup request; physical owner
521
+ * records and Task runtime isolation are cleared through the same primitives
522
+ * as the Controller lifecycle path before the Session becomes stopped.
523
+ */
524
+ async stopDormantSession(candidate) {
525
+ const queuedAt = new Date();
526
+ const target = this.schedulerStore.enqueueRuntimeCleanup(candidate.owner, queuedAt, candidate);
527
+ if (target === null) {
528
+ throw new Error(`Dormant Session changed before maintenance cleanup: ${runtimeOwnerLabel(candidate.owner)}.`);
529
+ }
530
+ const reconciliation = new SessionOwnerReconciliation({
531
+ home: this.home,
532
+ store: this.store,
533
+ environment: this.clientOptions.environment,
534
+ tmux: this.tmux
535
+ });
536
+ const termination = await reconciliation.terminateOwner(candidate.owner);
537
+ if (termination.outcome !== "stop-confirmed") {
538
+ throw new Error(`Role runtime cleanup could not prove physical exit: ${runtimeOwnerLabel(candidate.owner)}; `
539
+ + termination.remaining
540
+ .map(({ record, detail }) => `${record.launchId}: ${detail}`)
541
+ .join("; "));
542
+ }
543
+ if (candidate.owner.scope === "task" && candidate.launchId !== undefined) {
544
+ const isolation = new FileTaskRuntimeIsolation({
545
+ runtimeRoot: `${this.home}.task-runtimes`,
546
+ controlPlane: {
547
+ yuiHome: this.home,
548
+ controllerSocketPath: controllerSocketPath(this.store.getHomeIdentity().homeId),
549
+ tmuxNamespace: yuiTmuxServerName(this.home),
550
+ globalInstallPaths: [process.execPath]
551
+ }
552
+ });
553
+ isolation.cleanupTaskLaunch({
554
+ taskId: candidate.owner.taskId,
555
+ launchId: candidate.launchId,
556
+ reason: this.store.getTask(candidate.owner.taskId)?.status === "completed"
557
+ ? "completion"
558
+ : "interruption"
559
+ });
560
+ }
561
+ if (!this.schedulerStore.completeRuntimeCleanup(target, new Date())) {
562
+ throw new Error(`Role runtime cleanup state changed before completion: ${runtimeOwnerLabel(candidate.owner)}.`);
563
+ }
564
+ }
505
565
  inspectTaskRolePanes(taskId) {
506
566
  return this.tmux.inspectTaskRolePanes(taskId);
507
567
  }
@@ -532,6 +592,11 @@ export class FileTaskWorkflowRuntime {
532
592
  });
533
593
  }
534
594
  }
595
+ function runtimeOwnerLabel(owner) {
596
+ return owner.scope === "task"
597
+ ? `${owner.taskId}/${owner.roleName}`
598
+ : `global/${owner.roleName}`;
599
+ }
535
600
  const MANAGED_RUNTIME_ENVIRONMENT = new Set(YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES);
536
601
  function foregroundGlobalRoleEnvironment(store, roleName, source) {
537
602
  const role = store.getGlobalRole?.(roleName);
@@ -3,7 +3,6 @@ import { isDeepStrictEqual } from "node:util";
3
3
  import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleProviderRuntimeForCleanup, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
4
4
  import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, markProviderTurnDeliveryUnknown, rejectProviderTurn, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
5
5
  import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
6
- import { boundProviderRetryBeforeFirstProgress, projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
7
6
  import { hasRecentTurnId } from "../executor/turnCompletion.js";
8
7
  import { createTaskEvent } from "../event/taskEvent.js";
9
8
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
@@ -1300,40 +1299,6 @@ export class FileSchedulerStoreAdapter {
1300
1299
  clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
1301
1300
  getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
1302
1301
  getOperatorNotification(taskId) { return this.store.getOperatorNotification(taskId); }
1303
- saveLeaderFirstProgressStopLoss(input) {
1304
- return this.store.transaction((store) => {
1305
- const task = store.getTask(input.taskId);
1306
- const role = store.getRole(input.taskId, input.roleName);
1307
- if (task === null || task.status !== "active" || role === null
1308
- || store.getLeaderFailure(input.taskId) !== null) {
1309
- return "state-changed";
1310
- }
1311
- const stopLoss = projectFirstProgressStopLoss({
1312
- sessions: store.getTaskRoleSessionSet(input.taskId, input.roleName),
1313
- events: store.listEvents(input.taskId),
1314
- workItems: store.listWorkItems(input.taskId),
1315
- reviewRounds: store.listReviewRounds(input.taskId),
1316
- integrations: store.listIntegrationAttempts(input.taskId)
1317
- });
1318
- if (!stopLoss.exhausted || stopLoss.fingerprint !== input.expectedFingerprint) {
1319
- return "state-changed";
1320
- }
1321
- const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
1322
- const lastSession = sessions === null
1323
- ? undefined
1324
- : [...(sessions.history ?? []), ...Object.values(sessions.sessions)]
1325
- .sort((left, right) => left.createdAt.localeCompare(right.createdAt))
1326
- .at(-1);
1327
- const message = `Leader first-progress stop-loss: ${stopLoss.reason}`;
1328
- store.saveRole(input.taskId, updateRoleStatus(role, "failed", input.now));
1329
- store.saveLeaderFailure(recordLeaderFailure(input.taskId, lastSession?.nativeSessionId ?? "(unregistered)", message, input.now, null));
1330
- store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, message, input.now, store.getOperatorNotification(input.taskId)));
1331
- enqueueWork(store, { kind: "operator" }, "leader-first-progress-stop-loss", input.now, [
1332
- { type: "task", id: input.taskId }
1333
- ]);
1334
- return "recorded";
1335
- });
1336
- }
1337
1302
  saveLeaderDispatch(input) {
1338
1303
  return this.store.transaction((store) => {
1339
1304
  const task = store.getTask(input.task.id);
@@ -2287,7 +2252,7 @@ export class FileSchedulerStoreAdapter {
2287
2252
  failedNativeTurnId: input.nativeTurnId,
2288
2253
  lastErrorSummary: summary,
2289
2254
  ...(input.retryAfterMs === undefined ? {} : { retryAfterMs: input.retryAfterMs })
2290
- }, now, configuredProviderRetryPolicy(store, input.taskId, input.roleName));
2255
+ }, now, configuredProviderRetryPolicy(store));
2291
2256
  if (retryDecision.outcome === "exhausted") {
2292
2257
  this.recordProviderRetryClassified(store, input, classification.errorClass, {
2293
2258
  wouldRetry: "false",
@@ -4069,19 +4034,7 @@ function compareCanonicalObservationOrder(left, right) {
4069
4034
  || (left.ordinal ?? -1) - (right.ordinal ?? -1)
4070
4035
  || left.eventId.localeCompare(right.eventId);
4071
4036
  }
4072
- function configuredProviderRetryPolicy(store, taskId, roleName) {
4037
+ function configuredProviderRetryPolicy(store) {
4073
4038
  const config = providerRetryConfig(store.getConfig());
4074
- if (taskId !== undefined && roleName === "leader") {
4075
- const progress = projectFirstProgressStopLoss({
4076
- sessions: store.getTaskRoleSessionSet(taskId, roleName),
4077
- events: store.listEvents(taskId),
4078
- workItems: store.listWorkItems(taskId),
4079
- reviewRounds: store.listReviewRounds(taskId),
4080
- integrations: store.listIntegrationAttempts(taskId)
4081
- });
4082
- if (progress.firstProgressAt === undefined) {
4083
- return boundProviderRetryBeforeFirstProgress(config, progress);
4084
- }
4085
- }
4086
4039
  return { delaysMs: config.delaysMs, maxWindowMs: config.maxWindowMs };
4087
4040
  }
@@ -4,7 +4,7 @@ import Database from "better-sqlite3";
4
4
  import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
5
5
  import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
6
6
  import { inspectAgentCapabilities, resolveAgentAdapter } from "../executor/agentAdapter.js";
7
- import { inspectCodexLaunchConfig } from "../executor/codexConfigConflict.js";
7
+ import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from "../executor/codexConfigConflict.js";
8
8
  import { nativeAdditionalDirectories, nativeAgentWorkspace, withNativeProjectDirectories } from "../executor/fileRoleLaunchPlanner.js";
9
9
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
10
10
  import { compileRoleSessionContext } from "../context/roleSessionContext.js";
@@ -789,14 +789,8 @@ function checkReviewerLaunch(agent, role, binding, adapter, environment, home) {
789
789
  trustWorkspace: true
790
790
  })
791
791
  : undefined;
792
- if (codexConfig?.notify.status === "configured") {
793
- return {
794
- name: "reviewer launch",
795
- status: "invalid",
796
- detail: "Codex notify is already configured by "
797
- + `${codexConfig.notify.source}; Yui requires exclusive ownership of the structured `
798
- + "notify callback and refuses to replace or be replaced by native configuration."
799
- };
792
+ if (codexConfig !== undefined) {
793
+ assertCodexLaunchOverridesAvailable(codexConfig, ["developerInstructions", "notify"]);
800
794
  }
801
795
  const reviewerContext = adapter.id === "codex"
802
796
  ? compileRoleSessionContext(home, role, { scope: "global" }, { purpose: "review" })
@@ -810,8 +804,7 @@ function checkReviewerLaunch(agent, role, binding, adapter, environment, home) {
810
804
  ? {}
811
805
  : {
812
806
  developerInstructions: reviewerContext.developerInstructions,
813
- skills: reviewerContext.skills,
814
- codexDeveloperInstructions: codexConfig?.developerInstructions
807
+ skills: reviewerContext.skills
815
808
  })
816
809
  });
817
810
  if (compiled.argv.length === 0)
@@ -819,7 +812,18 @@ function checkReviewerLaunch(agent, role, binding, adapter, environment, home) {
819
812
  return {
820
813
  name: "reviewer launch",
821
814
  status: "ok",
822
- detail: `adapter=${adapter.id} strategy=${compiled.sessionStrategy} command=${agent.command} addDirs=${launchConfig.additionalDirectories?.length ?? 0}`
815
+ detail: [
816
+ `adapter=${adapter.id}`,
817
+ `strategy=${compiled.sessionStrategy}`,
818
+ `command=${agent.command}`,
819
+ `addDirs=${launchConfig.additionalDirectories?.length ?? 0}`,
820
+ ...(codexConfig?.developerInstructions.status === "configured"
821
+ ? [`override=developer_instructions@${codexConfig.developerInstructions.source}`]
822
+ : []),
823
+ ...(codexConfig?.notify.status === "configured"
824
+ ? [`override=notify@${codexConfig.notify.source}`]
825
+ : [])
826
+ ].join(" ")
823
827
  };
824
828
  }
825
829
  catch (error) {
@@ -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)
@@ -4,7 +4,6 @@ import { isAbsolute, resolve } from "node:path";
4
4
  import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
5
5
  import { ownedArgumentsForAdapter, validateAgentAdvancedArguments, validateAgentBaseArguments } from "../agent/argumentPolicy.js";
6
6
  import { writeTextFileAtomically } from "../storage/durableFile.js";
7
- import { inspectCodexDeveloperInstructions } from "./codexConfigConflict.js";
8
7
  import { discoverClaudeConfiguration, discoverCodexConfiguration } from "./agentConfigurationProbe.js";
9
8
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
10
9
  const SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
@@ -139,16 +138,6 @@ class CodexAdapter extends BaseAdapter {
139
138
  ];
140
139
  if (instructions.length === 0)
141
140
  return workspaceTrust;
142
- const nativeInstructions = input.codexDeveloperInstructions
143
- ?? inspectCodexDeveloperInstructions({
144
- workspace: input.workspace,
145
- profile: input.config.profile,
146
- trustWorkspace: true
147
- });
148
- if (nativeInstructions.status === "configured") {
149
- throw new Error("Codex developer_instructions is already configured by "
150
- + `${nativeInstructions.source}; Yui refuses to replace native developer instructions.`);
151
- }
152
141
  return [
153
142
  ...workspaceTrust,
154
143
  "--config",
@@ -375,37 +364,33 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
375
364
  if (!supported) {
376
365
  return snapshot(agent, adapter, {
377
366
  status: "unsupported-version", command: agent.command, version,
378
- reason: adapter.id === "codex"
379
- ? `Minimum supported version is ${adapter.supportedVersion}.`
380
- : `Supported version line starts at ${adapter.supportedVersion}.`,
367
+ reason: `Minimum supported version is ${adapter.supportedVersion}.`,
381
368
  probedAt: at
382
369
  }, fields, at, [`Installed version ${version} is not supported by adapter ${adapter.id}.`]);
383
370
  }
384
371
  const help = run(agent.command, ["--help"]);
385
372
  const helpFailure = failed(help);
386
- if (adapter.id === "codex" && helpFailure !== undefined) {
373
+ if (helpFailure !== undefined) {
387
374
  return snapshot(agent, adapter, {
388
375
  status: "probe-failed", command: agent.command, version,
389
- reason: `Required Codex capability probe failed: ${helpFailure}`, probedAt: at
376
+ reason: `Required ${adapter.label} capability probe failed: ${helpFailure}`,
377
+ probedAt: at
390
378
  }, fields, at);
391
379
  }
392
- if (helpFailure === undefined) {
393
- const helpOutput = output(help.stdout, help.stderr);
394
- fields = fromHelp(agent.adapterId, helpOutput);
395
- if (adapter.id === "codex") {
396
- const missing = missingCodexCapabilities(helpOutput);
397
- if (missing.length > 0) {
398
- return snapshot(agent, adapter, {
399
- status: "unsupported-version", command: agent.command, version,
400
- reason: `Codex CLI is missing required capabilities: ${missing.join(", ")}.`,
401
- probedAt: at
402
- }, fields, at);
403
- }
404
- if (compareVersions(version, CODEX_TESTED_THROUGH_VERSION) > 0) {
405
- warnings.push(`Installed Codex version ${version} is newer than the latest tested version `
406
- + `${CODEX_TESTED_THROUGH_VERSION}; required capabilities were detected.`);
407
- }
408
- }
380
+ const helpOutput = output(help.stdout, help.stderr);
381
+ fields = fromHelp(agent.adapterId, helpOutput);
382
+ const missing = missingRequiredCapabilities(adapter.id, helpOutput);
383
+ if (missing.length > 0) {
384
+ return snapshot(agent, adapter, {
385
+ status: "unsupported-version", command: agent.command, version,
386
+ reason: `${adapter.label} CLI is missing required capabilities: ${missing.join(", ")}.`,
387
+ probedAt: at
388
+ }, fields, at);
389
+ }
390
+ if (adapter.id === "codex"
391
+ && compareVersions(version, CODEX_TESTED_THROUGH_VERSION) > 0) {
392
+ warnings.push(`Installed Codex version ${version} is newer than the latest tested version `
393
+ + `${CODEX_TESTED_THROUGH_VERSION}; required capabilities were detected.`);
409
394
  }
410
395
  return snapshot(agent, adapter, {
411
396
  status: "installed", command: agent.command, version, probedAt: at
@@ -498,10 +483,7 @@ function output(stdout, stderr) {
498
483
  return value;
499
484
  }
500
485
  function supports(version, adapter) {
501
- if (adapter.id === "codex")
502
- return compareVersions(version, adapter.supportedVersion) >= 0;
503
- const left = version.split(".").map(Number), right = adapter.supportedVersion.split(".").map(Number);
504
- return left[0] === right[0] && left[1] === right[1] && left[2] >= right[2];
486
+ return compareVersions(version, adapter.supportedVersion) >= 0;
505
487
  }
506
488
  function compareVersions(leftVersion, rightVersion) {
507
489
  const left = leftVersion.split(".").map(Number);
@@ -513,11 +495,26 @@ function compareVersions(leftVersion, rightVersion) {
513
495
  }
514
496
  return 0;
515
497
  }
516
- function missingCodexCapabilities(help) {
517
- const required = [
518
- [/(?:^|\s)--config(?:\s|[=<,]|$)/m, "--config"],
519
- [/^\s*resume(?:\s|$)/m, "resume"]
520
- ];
498
+ function missingRequiredCapabilities(id, help) {
499
+ const required = id === "codex"
500
+ ? [
501
+ [/(?:^|\s)--config(?:\s|[=<,]|$)/m, "--config"],
502
+ [/^\s*resume(?:\s|$)/m, "resume"]
503
+ ]
504
+ : [
505
+ [/(?:^|\s)--append-system-prompt(?:-file|\[-file\])(?:\s|[=<,]|$)/m,
506
+ "--append-system-prompt-file"],
507
+ [/(?:^|\s)--resume(?:\s|[=<,]|$)/m, "--resume"],
508
+ [/(?:^|\s)--session-id(?:\s|[=<,]|$)/m, "--session-id"],
509
+ [/(?:^|\s)-p(?:\s|[=<,]|$)/m, "-p"],
510
+ [/(?:^|\s)--output-format(?:\s|[=<,]|$)/m, "--output-format"],
511
+ [/(?:^|\s)--input-format(?:\s|[=<,]|$)/m, "--input-format"],
512
+ [/(?:^|\s)--verbose(?:\s|[=<,]|$)/m, "--verbose"],
513
+ [/(?:^|\s)--replay-user-messages(?:\s|[=<,]|$)/m,
514
+ "--replay-user-messages"],
515
+ [/(?:^|\s)--plugin-dir(?:\s|[=<,]|$)/m, "--plugin-dir"],
516
+ [/(?:^|\s)--name(?:\s|[=<,]|$)/m, "--name"]
517
+ ];
521
518
  return required.flatMap(([pattern, label]) => pattern.test(help) ? [] : [label]);
522
519
  }
523
520
  function cloneConfig(config, paths) {
@@ -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