@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
@@ -20,6 +20,8 @@ const PROJECT_KNOWLEDGE_PROPOSALS_FROM_VERSION = 3;
20
20
  const PROJECT_KNOWLEDGE_PROPOSALS_TO_VERSION = 4;
21
21
  const TASK_FROM_VERSION = 3;
22
22
  const TASK_TO_VERSION = 4;
23
+ const TASK_INTENT_FROM_VERSION = 4;
24
+ const TASK_INTENT_TO_VERSION = 5;
23
25
  const WORK_ITEM_FROM_VERSION = 6;
24
26
  const WORK_ITEM_TO_VERSION = 7;
25
27
  const WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION = 7;
@@ -45,6 +47,8 @@ const REVIEW_ROUND_FROM_VERSION = 2;
45
47
  const REVIEW_ROUND_TO_VERSION = 3;
46
48
  const REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION = 3;
47
49
  const REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION = 4;
50
+ const REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION = 4;
51
+ const REVIEW_ROUND_TASK_ANCHOR_TO_VERSION = 5;
48
52
  const ACTIVE_RUN_POINTER_FROM_VERSION = 1;
49
53
  const ACTIVE_RUN_POINTER_TO_VERSION = 2;
50
54
  const ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION = 2;
@@ -136,6 +140,7 @@ export function createProductionStorageRegistry() {
136
140
  .registerOfflineMigration(configV2Step())
137
141
  .registerCompatible(projectKnowledgeProposalsStep())
138
142
  .registerOfflineMigration(taskWorkspaceIdentityStep())
143
+ .registerOfflineMigration(taskIntentStep())
139
144
  .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
140
145
  .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_GIT_SNAPSHOT_FROM_VERSION, WORK_ITEM_GIT_SNAPSHOT_TO_VERSION, "workItems"))
141
146
  .registerOfflineMigration(workItemExecutionGroupHistoryStep())
@@ -146,6 +151,7 @@ export function createProductionStorageRegistry() {
146
151
  .registerOfflineMigration(messageWakePolicyStep())
147
152
  .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_FROM_VERSION, REVIEW_ROUND_TO_VERSION, "reviewRounds"))
148
153
  .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION, REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION, "reviewRounds"))
154
+ .registerOfflineMigration(reviewRoundTaskAnchorStep())
149
155
  .registerOfflineMigration(recordFamilyStep("activeRunPointer", ACTIVE_RUN_POINTER_FROM_VERSION, ACTIVE_RUN_POINTER_TO_VERSION, "activeRuns"))
150
156
  .registerOfflineMigration(managedWorkspaceFamilyStep())
151
157
  .registerOfflineMigration(activeRunPointerNamespaceStep())
@@ -1546,6 +1552,65 @@ function recordFamilyStep(recordKind, fromVersion, toVersion, taskMapKey) {
1546
1552
  declaredEffects: []
1547
1553
  };
1548
1554
  }
1555
+ function reviewRoundTaskAnchorStep() {
1556
+ return {
1557
+ axis: "record",
1558
+ recordKind: "reviewRound",
1559
+ fromVersion: REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION,
1560
+ toVersion: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION,
1561
+ preconditions: (snapshot) => requireRecordFamilyVersion(snapshot, "reviewRound", REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION, "reviewRounds"),
1562
+ transform: migrateReviewRoundTaskAnchors,
1563
+ declaredEffects: []
1564
+ };
1565
+ }
1566
+ function migrateReviewRoundTaskAnchors(snapshot) {
1567
+ requireRecordFamilyVersion(snapshot, "reviewRound", REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION, "reviewRounds");
1568
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
1569
+ const schemaManifest = {
1570
+ ...snapshot.schemaManifest,
1571
+ recordVersions: {
1572
+ ...manifestVersions,
1573
+ reviewRound: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION
1574
+ }
1575
+ };
1576
+ if (snapshot.state === null)
1577
+ return { schemaManifest, state: null };
1578
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
1579
+ const nextTasks = {};
1580
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
1581
+ const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
1582
+ const rawRounds = aggregate.reviewRounds;
1583
+ if (rawRounds === undefined) {
1584
+ nextTasks[taskId] = { ...aggregate };
1585
+ continue;
1586
+ }
1587
+ const rounds = asObject(rawRounds, `reviewRound map ${taskId}`);
1588
+ const nextRounds = {};
1589
+ for (const [roundId, rawRound] of Object.entries(rounds)) {
1590
+ const round = asObject(rawRound, `reviewRound ${taskId}/${roundId}`);
1591
+ if (round.scope !== "task") {
1592
+ nextRounds[roundId] = {
1593
+ ...round,
1594
+ schemaVersion: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION
1595
+ };
1596
+ continue;
1597
+ }
1598
+ const workItemId = requiredMigrationText(round.workItemId, `reviewRound ${taskId}/${roundId} WorkItem id`);
1599
+ const candidateId = requiredMigrationText(round.candidateId, `reviewRound ${taskId}/${roundId} Candidate id`);
1600
+ const { workItemId: _workItemId, candidateId: _candidateId, schemaVersion: _schemaVersion, ...retained } = round;
1601
+ nextRounds[roundId] = {
1602
+ ...retained,
1603
+ schemaVersion: REVIEW_ROUND_TASK_ANCHOR_TO_VERSION,
1604
+ legacyAnchor: { workItemId, candidateId }
1605
+ };
1606
+ }
1607
+ nextTasks[taskId] = { ...aggregate, reviewRounds: nextRounds };
1608
+ }
1609
+ return {
1610
+ schemaManifest,
1611
+ state: { ...snapshot.state, tasks: nextTasks }
1612
+ };
1613
+ }
1549
1614
  function agentRunContextProtocolStep() {
1550
1615
  return {
1551
1616
  axis: "record",
@@ -2747,6 +2812,79 @@ function migrateTaskV3ToV4(snapshot) {
2747
2812
  state: { ...snapshot.state, tasks: nextTasks }
2748
2813
  };
2749
2814
  }
2815
+ /**
2816
+ * Task v5 replaces the stored delivery topology switch with an optional,
2817
+ * Project-defined intent. Historical topology is retained as audit context,
2818
+ * but no current workflow decision reads it.
2819
+ */
2820
+ function taskIntentStep() {
2821
+ return {
2822
+ axis: "record",
2823
+ recordKind: "task",
2824
+ fromVersion: TASK_INTENT_FROM_VERSION,
2825
+ toVersion: TASK_INTENT_TO_VERSION,
2826
+ preconditions: requireTaskV4Family,
2827
+ transform: migrateTaskV4ToV5,
2828
+ declaredEffects: []
2829
+ };
2830
+ }
2831
+ function requireTaskV4Family(snapshot) {
2832
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2833
+ if (manifestVersions.task !== TASK_INTENT_FROM_VERSION) {
2834
+ throw new Error(`Record task migration requires manifest version ${TASK_INTENT_FROM_VERSION}.`);
2835
+ }
2836
+ if (snapshot.state === null)
2837
+ return;
2838
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
2839
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
2840
+ const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
2841
+ if (aggregate.task === undefined)
2842
+ continue;
2843
+ const record = asObject(aggregate.task, `Task ${taskId}`);
2844
+ if (record.schemaVersion !== TASK_INTENT_FROM_VERSION) {
2845
+ throw new Error(`Task ${taskId} must use schemaVersion ${TASK_INTENT_FROM_VERSION}.`);
2846
+ }
2847
+ }
2848
+ }
2849
+ function migrateTaskV4ToV5(snapshot) {
2850
+ requireTaskV4Family(snapshot);
2851
+ const manifestVersions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2852
+ const schemaManifest = {
2853
+ ...snapshot.schemaManifest,
2854
+ recordVersions: { ...manifestVersions, task: TASK_INTENT_TO_VERSION }
2855
+ };
2856
+ if (snapshot.state === null)
2857
+ return { schemaManifest, state: null };
2858
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
2859
+ const nextTasks = {};
2860
+ for (const [taskId, rawTask] of Object.entries(tasks)) {
2861
+ const aggregate = asObject(rawTask, `Task aggregate ${taskId}`);
2862
+ if (aggregate.task === undefined) {
2863
+ nextTasks[taskId] = { ...aggregate };
2864
+ continue;
2865
+ }
2866
+ const task = asObject(aggregate.task, `Task ${taskId}`);
2867
+ const { requireIntegration, schemaVersion: _schemaVersion, ...retained } = task;
2868
+ const projectBindings = Array.isArray(task.projectBindings)
2869
+ ? task.projectBindings
2870
+ : [];
2871
+ const legacyDeliveryPath = projectBindings.length === 0
2872
+ ? undefined
2873
+ : requireIntegration === true ? "integrated" : "direct";
2874
+ nextTasks[taskId] = {
2875
+ ...aggregate,
2876
+ task: {
2877
+ ...retained,
2878
+ schemaVersion: TASK_INTENT_TO_VERSION,
2879
+ ...(legacyDeliveryPath === undefined ? {} : { legacyDeliveryPath })
2880
+ }
2881
+ };
2882
+ }
2883
+ return {
2884
+ schemaManifest,
2885
+ state: { ...snapshot.state, tasks: nextTasks }
2886
+ };
2887
+ }
2750
2888
  /**
2751
2889
  * A version bump is deliverable only when the shared planner resolves the full
2752
2890
  * adjacent path. This also covers target-only record families as explicit 0->1
@@ -722,11 +722,12 @@ export class SqliteTaskStore {
722
722
  id: task.id,
723
723
  status: task.status,
724
724
  projectBindings: task.projectBindings,
725
- requireIntegration: task.requireIntegration
725
+ type: task.type
726
726
  },
727
727
  workItems: this.#sortById(this.#listPayload("work_items", "task_id = ?", [taskId]), (item) => item.id),
728
728
  changeSets: this.#sortById(this.#listPayload("change_sets", "task_id = ?", [taskId]), (changeSet) => changeSet.id),
729
729
  integrations: this.#sortById(this.#listPayload("integration_attempts", "task_id = ?", [taskId]), (attempt) => attempt.id),
730
+ integrationQueueEntries: this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id),
730
731
  reviewRounds: this.#sortById(this.#listPayload("review_rounds", "task_id = ?", [taskId]), (round) => round.id),
731
732
  taskFinalReviewContractEvents: events
732
733
  .filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
@@ -62,13 +62,13 @@ export const CURRENT_PROJECT_SCHEMA_VERSION = 4;
62
62
  export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 2;
63
63
  export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
64
64
  export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 3;
65
- export const CURRENT_TASK_SCHEMA_VERSION = 4;
65
+ export const CURRENT_TASK_SCHEMA_VERSION = 5;
66
66
  export const CURRENT_TASK_BRIEF_SCHEMA_VERSION = 2;
67
67
  export const CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
68
68
  export const CURRENT_TASK_ROLE_SCHEMA_VERSION = 3;
69
69
  export const CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION = 2;
70
70
  export const CURRENT_WORK_ITEM_SCHEMA_VERSION = 9;
71
- export const CURRENT_REVIEW_ROUND_SCHEMA_VERSION = 4;
71
+ export const CURRENT_REVIEW_ROUND_SCHEMA_VERSION = 5;
72
72
  export const CURRENT_CHANGE_SET_SCHEMA_VERSION = 3;
73
73
  export const CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION = 4;
74
74
  export const CURRENT_MESSAGE_SCHEMA_VERSION = 3;
@@ -417,11 +417,12 @@ export class FileTaskStore {
417
417
  id: aggregate.task.id,
418
418
  status: aggregate.task.status,
419
419
  projectBindings: aggregate.task.projectBindings,
420
- requireIntegration: aggregate.task.requireIntegration
420
+ type: aggregate.task.type
421
421
  },
422
422
  workItems: values(aggregate.workItems, "id"),
423
423
  changeSets: values(aggregate.changeSets, "id"),
424
424
  integrations: values(aggregate.integrationAttempts, "id"),
425
+ integrationQueueEntries: values(aggregate.integrationQueue, "id"),
425
426
  reviewRounds: values(aggregate.reviewRounds, "id"),
426
427
  taskFinalReviewContractEvents: events
427
428
  .filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
@@ -1026,18 +1027,16 @@ export class FileTaskStore {
1026
1027
  throw new StorageRecordError(`ReviewRound belongs to another Task: ${stored.taskId}.`);
1027
1028
  }
1028
1029
  const aggregate = this.#requireTaskForWrite(taskId);
1029
- const item = aggregate.workItems[stored.workItemId];
1030
- if (item === undefined) {
1031
- throw new StorageRecordError(`ReviewRound Work Item not found: ${stored.workItemId}.`);
1032
- }
1033
- const candidate = item.candidates.find(({ id }) => id === stored.candidateId);
1034
- if (candidate === undefined) {
1035
- throw new StorageRecordError(`ReviewRound Candidate not found: ${stored.candidateId}.`);
1036
- }
1037
- assertWorkItemCandidateReferences(aggregate, item, candidate, `ReviewRound candidate ${stored.id}`);
1038
- if ((stored.scope ?? "work-item") === "task"
1039
- && !sameTaskFinalReviewContract(stored.taskFinalReviewContract, candidate.taskFinalReviewContract)) {
1040
- throw new StorageRecordError(`Task ReviewRound contract does not match its Candidate: ${stored.id}.`);
1030
+ if ((stored.scope ?? "work-item") === "work-item") {
1031
+ const item = aggregate.workItems[stored.workItemId];
1032
+ if (item === undefined) {
1033
+ throw new StorageRecordError(`ReviewRound Work Item not found: ${stored.workItemId}.`);
1034
+ }
1035
+ const candidate = item.candidates.find(({ id }) => id === stored.candidateId);
1036
+ if (candidate === undefined) {
1037
+ throw new StorageRecordError(`ReviewRound Candidate not found: ${stored.candidateId}.`);
1038
+ }
1039
+ assertWorkItemCandidateReferences(aggregate, item, candidate, `ReviewRound candidate ${stored.id}`);
1041
1040
  }
1042
1041
  if (stored.reviewerRunId !== undefined) {
1043
1042
  const reviewerRun = aggregate.agentRuns[stored.reviewerRunId];
@@ -3349,24 +3348,23 @@ function validateCanonicalTaskReferences(state, aggregate) {
3349
3348
  }
3350
3349
  }
3351
3350
  for (const round of Object.values(aggregate.reviewRounds)) {
3352
- const item = aggregate.workItems[round.workItemId];
3353
- if (item === undefined) {
3354
- throw new StorageRecordError(`ReviewRound references are invalid: ${round.id}.`);
3355
- }
3356
- const candidate = item.candidates.find(({ id }) => id === round.candidateId);
3357
- if (candidate === undefined) {
3358
- throw new StorageRecordError(`ReviewRound Candidate not found: ${round.candidateId}.`);
3359
- }
3360
- assertWorkItemCandidateReferences(aggregate, item, candidate, `ReviewRound candidate ${round.id}`);
3361
3351
  if ((round.scope ?? "work-item") === "task") {
3362
3352
  const frozenProjects = round.taskCandidate?.projects ?? [];
3363
3353
  if (frozenProjects.length !== boundProjects.size
3364
3354
  || frozenProjects.some(({ projectId }) => !boundProjects.has(projectId))) {
3365
3355
  throw new StorageRecordError(`Task ReviewRound Projects do not match Task scope: ${round.id}.`);
3366
3356
  }
3367
- if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, candidate.taskFinalReviewContract)) {
3368
- throw new StorageRecordError(`Task ReviewRound contract does not match its Candidate: ${round.id}.`);
3357
+ }
3358
+ else {
3359
+ const item = aggregate.workItems[round.workItemId];
3360
+ if (item === undefined) {
3361
+ throw new StorageRecordError(`ReviewRound references are invalid: ${round.id}.`);
3362
+ }
3363
+ const candidate = item.candidates.find(({ id }) => id === round.candidateId);
3364
+ if (candidate === undefined) {
3365
+ throw new StorageRecordError(`ReviewRound Candidate not found: ${round.candidateId}.`);
3369
3366
  }
3367
+ assertWorkItemCandidateReferences(aggregate, item, candidate, `ReviewRound candidate ${round.id}`);
3370
3368
  }
3371
3369
  if (round.reviewerRunId !== undefined) {
3372
3370
  const reviewerRun = aggregate.agentRuns[round.reviewerRunId];
@@ -3496,7 +3494,9 @@ function assertManagedWorkspaceReferences(aggregate, workspace, label, workItemO
3496
3494
  if (workspace.entries.some(({ access }) => access !== "write")) {
3497
3495
  throw new StorageRecordError(`${label} ReviewRound workspace must be writable: ${taskId}/${round.id}.`);
3498
3496
  }
3499
- const item = aggregate.workItems[round.workItemId];
3497
+ const item = round.workItemId === undefined
3498
+ ? undefined
3499
+ : aggregate.workItems[round.workItemId];
3500
3500
  const candidate = item?.candidates.find(({ id }) => id === round.candidateId);
3501
3501
  const frozenProjects = round.scope === "task"
3502
3502
  ? round.taskCandidate?.projects
@@ -298,7 +298,7 @@ function describeOfflineBlockers(inventory) {
298
298
  return `${index + 1}. ${identity.length === 0 ? "identity=unknown" : identity} ` +
299
299
  `reason=${reason}`;
300
300
  });
301
- return `Offline migration blocked by ${inventory.total} active runtime item(s). ` +
301
+ return `Offline Home migration cannot start while ${inventory.total} runtime obligation(s) remain. ` +
302
302
  lines.join("; ");
303
303
  }
304
304
  async function readOfflineInventory(options, home) {
@@ -315,15 +315,43 @@ async function readOfflineInventory(options, home) {
315
315
  }
316
316
  function offlineInventoryBlocker(inventory, sceneUnchanged) {
317
317
  const lifecycleOnly = inventory.blockers.every(({ reason }) => (reason === "pending-mailbox" || reason === "pending-inbox"));
318
+ const runtimeUnknown = inventory.blockers.some(({ reason }) => (reason === "native-session-unknown"));
319
+ const action = lifecycleOnly
320
+ ? (sceneUnchanged
321
+ ? "No binary, Controller, fence, or Home change was made. Wait for the listed lifecycle "
322
+ + "events to settle, then re-run `yui update`."
323
+ : "The Home was not switched. Wait for the listed lifecycle events to settle, then "
324
+ + "re-run `yui update`.")
325
+ : runtimeUnknown
326
+ ? (sceneUnchanged
327
+ ? "Offline Home migration could not confirm that every native Session is stopped. Run "
328
+ + "`yui controller status --verbose` from a normal shell and inspect the reported "
329
+ + "runtime ownership. Use `yui controller cleanup` only for resources it classifies "
330
+ + "for cleanup, then re-run `yui update`; do not kill unknown processes blindly. No "
331
+ + "binary, Controller, fence, or Home change was made."
332
+ : "The Home was not switched because native Session state could not be confirmed. Run "
333
+ + "`yui controller status --verbose`, inspect the reported ownership, and use "
334
+ + "`yui controller cleanup` only for resources it classifies for cleanup before "
335
+ + "retrying; do not kill unknown processes blindly.")
336
+ : (sceneUnchanged
337
+ ? "This version requires an offline Home migration with every managed Agent Session "
338
+ + "stopped. Let the listed Turns or Runs finish. If the installed `yui` supports "
339
+ + "`session stop --all`, run `yui session stop --all` from a normal shell; otherwise "
340
+ + "exit every listed managed Session manually. Then re-run `yui update`. No binary, "
341
+ + "Controller, fence, or Home change was made."
342
+ : "The Home was not switched. Let the listed Turns or Runs finish. If the installed "
343
+ + "`yui` supports `session stop --all`, run `yui session stop --all` from a normal "
344
+ + "shell; otherwise exit every listed managed Session manually. Then re-run "
345
+ + "`yui update`.");
318
346
  return {
319
347
  outcome: "blocked",
320
- stage: lifecycleOnly ? "drain-incomplete" : "active-sessions",
348
+ stage: lifecycleOnly
349
+ ? "drain-incomplete"
350
+ : runtimeUnknown
351
+ ? "runtime-unknown"
352
+ : "active-sessions",
321
353
  message: describeOfflineBlockers(inventory),
322
- action: sceneUnchanged
323
- ? "No binary, Controller, fence, or Home change was made. Keep working; when every " +
324
- "listed runtime obligation is clear, re-run `yui update` so preflight is repeated."
325
- : "The Home was not switched. Let every listed runtime obligation settle, then re-run " +
326
- "`yui update`; do not kill, reset, rebind, or retry the blocked runtime blindly.",
354
+ action,
327
355
  blockers: inventory.blockers,
328
356
  retryCommand: "yui update",
329
357
  ...(sceneUnchanged ? { sceneUnchanged: true } : {})
@@ -1,7 +1,9 @@
1
+ import { changeSetDeliverySettled, governingChangeSets, integrationAttemptRequiresSettlement, latestGoverningQueueEntries } from "../integration/deliveryObligation.js";
1
2
  import { isReviewFindingBlocking } from "../review/reviewFinding.js";
2
3
  import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
3
4
  import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
4
5
  import { reviewFindingLedgerWriteFailedFromEvents } from "../review/reviewFindingLedger.js";
6
+ import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
5
7
  import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
6
8
  const ACTIVE_JOB_STATUSES = new Set([
7
9
  "queued",
@@ -20,6 +22,7 @@ export function projectCompletionReadiness(facts, options = {}) {
20
22
  const advisories = [];
21
23
  const { task } = facts;
22
24
  const findingsGate = options.findingsGate ?? true;
25
+ const taskFinalReviewRequired = resolveRecordedTaskFinalReviewContract(task.id, facts.workItems, facts.reviewRounds, facts.taskFinalReviewContractEvents) !== undefined;
23
26
  // A pending/running Task-final Review must be resumed or blocked first.
24
27
  for (const round of facts.reviewRounds) {
25
28
  if ((round.scope ?? "work-item") !== "task")
@@ -49,7 +52,8 @@ export function projectCompletionReadiness(facts, options = {}) {
49
52
  .sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
50
53
  || left.id.localeCompare(right.id, undefined, { numeric: true })))
51
54
  .at(-1);
52
- if (latestCompletedTaskReview !== undefined
55
+ if (taskFinalReviewRequired
56
+ && latestCompletedTaskReview !== undefined
53
57
  && deltaRecheckBlocksAcceptance(latestCompletedTaskReview)) {
54
58
  const round = latestCompletedTaskReview;
55
59
  const disposition = round.deltaRecheck.disposition;
@@ -97,37 +101,43 @@ export function projectCompletionReadiness(facts, options = {}) {
97
101
  fix: `wait for DurableJob ${job.id} to finish, or cancel it`
98
102
  });
99
103
  }
100
- // requireIntegration Tasks need the full evidence chain.
101
- if (task.requireIntegration === true) {
102
- if (facts.workItems.length === 0) {
103
- blockers.push({
104
- code: "integration-evidence-missing",
105
- ref: ref("task", task.id),
106
- reason: `Task ${task.id} requires at least one WorkItem before completion.`,
107
- fix: `yui task work create ${task.id} "<objective>"`
108
- });
109
- }
110
- if (facts.changeSets.length === 0) {
111
- blockers.push({
112
- code: "integration-evidence-missing",
113
- ref: ref("task", task.id),
114
- reason: `Task ${task.id} requires at least one ChangeSet before completion.`,
115
- fix: `yui task work capture ${task.id}/<work-item>`
116
- });
117
- }
118
- if (!facts.integrations.some(({ status }) => status === "committed")) {
104
+ // Integration obligations follow only the Candidate that currently governs
105
+ // each independent delivery unit. Superseded Candidate history remains
106
+ // visible without keeping the Task open forever.
107
+ const deliveryChangeSets = governingChangeSets(facts.workItems, facts.changeSets);
108
+ for (const item of facts.workItems) {
109
+ if (item.status !== "completed")
110
+ continue;
111
+ const candidate = item.candidates?.at(-1);
112
+ const hasGitDelivery = candidate?.workspace !== undefined
113
+ || candidate?.gitSnapshot !== undefined;
114
+ if (hasGitDelivery
115
+ && !deliveryChangeSets.some((changeSet) => changeSet.workItemId === item.id)) {
119
116
  blockers.push({
120
117
  code: "integration-evidence-missing",
121
- ref: ref("task", task.id),
122
- reason: `Task ${task.id} requires a committed Integration Attempt before completion.`,
123
- fix: `yui task integration start ${task.id} --project <project> --change-set <change-set>`
118
+ ref: ref("work-item", item.id),
119
+ reason: `Completed WorkItem ${item.id} has an uncaptured Git delivery.`,
120
+ fix: `yui task work capture ${task.id}/${item.id}`
124
121
  });
125
122
  }
126
123
  }
127
- // Unresolved Integration Attempts must settle.
124
+ for (const changeSet of deliveryChangeSets) {
125
+ if (changeSetDeliverySettled(changeSet, facts.integrations, facts.integrationQueueEntries))
126
+ continue;
127
+ blockers.push({
128
+ code: "integration-evidence-missing",
129
+ ref: ref("change-set", changeSet.id),
130
+ reason: `ChangeSet ${changeSet.id} is not part of a committed Integration.`,
131
+ fix: `yui task integration start ${task.id} --project ${changeSet.projectId} --change-set ${changeSet.id}`
132
+ });
133
+ }
134
+ // Current delivery Attempts and any Attempt that may still be writing must
135
+ // settle. Historical blocked Attempts remain audit evidence only.
128
136
  for (const integration of facts.integrations) {
129
137
  if (!UNRESOLVED_INTEGRATION_STATUSES.has(integration.status))
130
138
  continue;
139
+ if (!integrationAttemptRequiresSettlement(integration, deliveryChangeSets))
140
+ continue;
131
141
  blockers.push({
132
142
  code: "unresolved-integration",
133
143
  ref: ref("integration-attempt", integration.id),
@@ -136,7 +146,7 @@ export function projectCompletionReadiness(facts, options = {}) {
136
146
  });
137
147
  }
138
148
  // Unsettled integration queue entries must commit or be superseded.
139
- for (const entry of facts.integrationQueueEntries) {
149
+ for (const entry of latestGoverningQueueEntries(deliveryChangeSets, facts.integrationQueueEntries)) {
140
150
  if (entry.status === "committed" || entry.status === "superseded")
141
151
  continue;
142
152
  blockers.push({