@zq-silk/yui 0.8.3 → 0.8.7

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 (73) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +66 -19
  3. package/dist/cli/commandCatalog.js +43 -14
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli/updatePorts.js +6 -0
  6. package/dist/cli.js +252 -37
  7. package/dist/commands/executionAuditCommands.js +30 -0
  8. package/dist/commands/globalRoleCommands.js +8 -4
  9. package/dist/commands/operatorCommands.js +42 -1
  10. package/dist/commands/taskCommands.js +527 -147
  11. package/dist/commands/taskCompletionGate.js +36 -24
  12. package/dist/commands/taskContextCommand.js +11 -4
  13. package/dist/commands/taskInputCommands.js +48 -10
  14. package/dist/commands/taskNextActionCommand.js +38 -3
  15. package/dist/commands/taskOverviewCommand.js +2 -1
  16. package/dist/commands/taskRoleRuntimeStatus.js +2 -1
  17. package/dist/context/runContextPack.js +9 -5
  18. package/dist/context/sessionBootstrapManifest.js +158 -11
  19. package/dist/context/wakeNotification.js +5 -3
  20. package/dist/controller/clientRuntime.js +15 -15
  21. package/dist/controller/controller.js +16 -8
  22. package/dist/controller/fileSchedulerStoreAdapter.js +67 -7
  23. package/dist/controller/handoverCandidate.js +10 -3
  24. package/dist/controller/sessionNotify.js +4 -22
  25. package/dist/executor/agentAdapter.js +2 -2
  26. package/dist/executor/agentExecutor.js +29 -9
  27. package/dist/executor/fileRoleLaunchPlanner.js +37 -45
  28. package/dist/integration/gitIntegrationService.js +50 -2
  29. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  30. package/dist/observability/executionAudit.js +47 -1
  31. package/dist/observability/faultClassification.js +6 -4
  32. package/dist/observability/orchestrationMetrics.js +196 -0
  33. package/dist/operator/operatorSessionHistory.js +36 -0
  34. package/dist/release/releaseHandover.js +7 -5
  35. package/dist/release/runtimeRelease.js +15 -0
  36. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  37. package/dist/review/deltaRecheck.js +3 -2
  38. package/dist/review/reviewFindingLedger.js +5 -4
  39. package/dist/review/reviewOutcomeClassifier.js +263 -54
  40. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  41. package/dist/review/taskFinalReviewContractRebind.js +367 -0
  42. package/dist/run/runIdentity.js +10 -70
  43. package/dist/runtime/agentHost.js +3 -4
  44. package/dist/runtime/codexAppServerRuntime.js +6 -0
  45. package/dist/runtime/exactControlPlane.js +47 -37
  46. package/dist/runtime/firstProgressStopLoss.js +54 -0
  47. package/dist/runtime/launchBroker.js +10 -2
  48. package/dist/runtime/runtimeDeadlines.js +14 -0
  49. package/dist/runtime/sessionTitle.js +24 -12
  50. package/dist/runtime/structuredProviderHost.js +7 -1
  51. package/dist/runtime/tmuxAdapters.js +10 -3
  52. package/dist/scheduler/actionability.js +4 -2
  53. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  54. package/dist/scheduler/activeTaskProgress.js +2 -1
  55. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  56. package/dist/scheduler/taskExecutionProjection.js +13 -4
  57. package/dist/scheduler/wakeReason.js +1 -0
  58. package/dist/storage/sqliteStore.js +18 -3
  59. package/dist/storage/taskStore.js +14 -3
  60. package/dist/task/completionReadiness.js +48 -19
  61. package/dist/task/deliveryGuard.js +3 -1
  62. package/dist/task/nextAction.js +153 -55
  63. package/dist/task/repairWave.js +14 -1
  64. package/dist/task/task.js +10 -0
  65. package/dist/task/taskRecordRetirement.js +72 -0
  66. package/dist/web/webSnapshot.js +7 -1
  67. package/dist/workItem/workItem.js +6 -4
  68. package/i18n/README.zh-CN.md +48 -9
  69. package/package.json +1 -1
  70. package/skills/yui-leader/SKILL.md +73 -31
  71. package/skills/yui-operator/SKILL.md +58 -10
  72. package/skills/yui-reviewer/SKILL.md +23 -0
  73. package/skills/yui-runtime/SKILL.md +6 -6
@@ -2,13 +2,16 @@ 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";
5
6
  import { publicationExternalKey } from "../task/publicationReference.js";
7
+ import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
6
8
  import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
7
9
  /**
8
10
  * Verify the one supported ancestry waiver before `task complete` mutates any
9
11
  * durable state. The explicit Publication must be the current verified merged
10
- * record, bind the exact reviewed physical Task head, and name an
11
- * ancestry-divergent commit with the exact same Git tree.
12
+ * record, bind the exact physical Task head (and, for integrated delivery, its
13
+ * completed final Review), and name an ancestry-divergent commit with the
14
+ * exact same Git tree.
12
15
  */
13
16
  export async function verifyTaskCompletionPublishedTree(taskId, publicationId, store, options = {}) {
14
17
  const task = store.getTask(taskId);
@@ -27,10 +30,11 @@ export async function verifyTaskCompletionPublishedTree(taskId, publicationId, s
27
30
  throw usageError(`Publication ${publication.id} must record exact local and remote commits.`);
28
31
  }
29
32
  const workspace = requireTaskWorkspace(store, task);
30
- const latestReview = latestTaskFinalReview(store, task.id);
31
- if (latestReview === undefined
32
- || latestReview.status !== "completed"
33
- || latestReview.taskCandidate === undefined) {
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)) {
34
38
  throw usageError(`Task ${task.id} requires a latest completed Task-final Review before accepting a published tree.`);
35
39
  }
36
40
  const git = options.git ?? new NodeGitWorkspace();
@@ -40,18 +44,21 @@ export async function verifyTaskCompletionPublishedTree(taskId, publicationId, s
40
44
  if (entry === undefined || entry.access !== "write") {
41
45
  throw usageError(`Task ${task.id} has no writable managed main workspace for Project ${taskBinding.projectId}.`);
42
46
  }
43
- const reviewedCommit = latestReview.taskCandidate.projects.find(({ projectId }) => (projectId === taskBinding.projectId))?.commit;
44
- if (reviewedCommit === undefined) {
45
- throw usageError(`Task-final Review ${latestReview.id} omitted Project ${taskBinding.projectId}.`);
46
- }
47
47
  const actualCommit = (await git.inspect(entry.path, "HEAD")).baseCommit;
48
- if (actualCommit !== reviewedCommit) {
49
- throw usageError(`Task-final Review ${latestReview.id} does not match Task head `
50
- + `${taskBinding.projectId}@${actualCommit}.`);
48
+ if (latestReview !== undefined) {
49
+ const reviewedCommit = latestReview.taskCandidate.projects.find(({ projectId }) => (projectId === taskBinding.projectId))?.commit;
50
+ if (reviewedCommit === undefined) {
51
+ throw usageError(`Task-final Review ${latestReview.id} omitted Project ${taskBinding.projectId}.`);
52
+ }
53
+ if (actualCommit !== reviewedCommit) {
54
+ throw usageError(`Task-final Review ${latestReview.id} does not match Task head `
55
+ + `${taskBinding.projectId}@${actualCommit}.`);
56
+ }
51
57
  }
52
58
  actualHeads.set(taskBinding.projectId, actualCommit);
53
59
  }
54
- if (actualHeads.size !== latestReview.taskCandidate.projects.length) {
60
+ if (latestReview !== undefined
61
+ && actualHeads.size !== latestReview.taskCandidate.projects.length) {
55
62
  throw usageError(`Task-final Review ${latestReview.id} Project set does not match Task ${task.id}.`);
56
63
  }
57
64
  const entry = workspaceProjectEntry(workspace, publication.projectId);
@@ -94,7 +101,7 @@ export async function verifyTaskCompletionPublishedTree(taskId, publicationId, s
94
101
  taskId: task.id,
95
102
  projectId: publication.projectId,
96
103
  publicationId: publication.id,
97
- reviewRoundId: latestReview.id,
104
+ ...(latestReview === undefined ? {} : { reviewRoundId: latestReview.id }),
98
105
  localCommit,
99
106
  remoteCommit,
100
107
  tree: localTree
@@ -115,13 +122,18 @@ export function assertTaskCompletionPublishedTreeProof(store, task, publicationI
115
122
  || publication.remoteCommit !== proof.remoteCommit) {
116
123
  throw usageError(`Publication evidence changed before Task completion: ${publication.id}.`);
117
124
  }
118
- const latestReview = latestTaskFinalReview(store, task.id);
119
- if (latestReview === undefined
120
- || latestReview.id !== proof.reviewRoundId
121
- || latestReview.status !== "completed"
122
- || latestReview.taskCandidate === undefined
123
- || !sameTaskCandidate(latestReview.taskCandidate, actualCandidate)) {
124
- throw usageError(`Task-final Review evidence changed before published-tree completion: ${task.id}.`);
125
+ if (taskDeliveryPath(task) === "integrated") {
126
+ const latestReview = latestTaskFinalReview(store, task.id);
127
+ if (latestReview === undefined
128
+ || latestReview.id !== proof.reviewRoundId
129
+ || !isSemanticReviewRound(latestReview, store)
130
+ || latestReview.taskCandidate === undefined
131
+ || !sameTaskCandidate(latestReview.taskCandidate, actualCandidate)) {
132
+ throw usageError(`Task-final Review evidence changed before published-tree completion: ${task.id}.`);
133
+ }
134
+ }
135
+ else if (proof.reviewRoundId !== undefined) {
136
+ throw usageError(`Direct published-tree proof unexpectedly binds a final Review: ${task.id}.`);
125
137
  }
126
138
  const actualCommit = actualCandidate.projects.find(({ projectId }) => (projectId === proof.projectId))?.commit;
127
139
  if (actualCommit !== proof.localCommit) {
@@ -321,7 +333,7 @@ async function hasCompletedTaskFinalReviewForCurrentCandidate(store, task, works
321
333
  .filter((round) => (round.scope ?? "work-item") === "task")
322
334
  .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
323
335
  const latest = rounds.at(-1);
324
- if (latest === undefined || latest.status !== "completed")
336
+ if (latest === undefined || !isSemanticReviewRound(latest, store))
325
337
  return false;
326
338
  if (latest.taskCandidate === undefined)
327
339
  return false;
@@ -354,7 +366,7 @@ function latestCommittedIntegration(store, taskId, projectId) {
354
366
  function hasFrozenTaskBaseline(store, taskId, projectId, currentCommit) {
355
367
  return store.listReviewRounds(taskId)
356
368
  .some((round) => ((round.scope ?? "work-item") === "task"
357
- && round.status === "completed"
369
+ && isSemanticReviewRound(round, store)
358
370
  && round.taskCandidate !== undefined
359
371
  && round.taskCandidate.projects.some((entry) => (entry.projectId === projectId && entry.commit === currentCommit))));
360
372
  }
@@ -4,9 +4,11 @@ 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";
7
8
  import { inspectTaskRoleSessionRecovery } from "./taskRoleRuntimeStatus.js";
8
9
  import { summarizeExecutionGroup } from "../execution/executionGroup.js";
9
10
  import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
11
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
10
12
  const RECENT_RECORD_LIMIT = 5;
11
13
  const RELATED_RECORD_LIMIT = 5;
12
14
  const SUMMARY_TEXT_LIMIT = 400;
@@ -40,7 +42,8 @@ export function runTaskContextCommand(args, store) {
40
42
  roleName: role.name
41
43
  }))
42
44
  ].filter((mailbox) => mailbox !== null);
43
- const agentRuns = chronological(reader.listAgentRuns(task.id));
45
+ const events = reader.listEvents(task.id);
46
+ const agentRuns = chronological(operationalTaskRecords(reader.listAgentRuns(task.id), events, "agent-run"));
44
47
  const reviewRounds = chronological(reader.listReviewRounds(task.id));
45
48
  const changeSets = chronological(reader.listChangeSets(task.id));
46
49
  const integrations = chronological(reader.listIntegrationAttempts(task.id));
@@ -51,6 +54,7 @@ export function runTaskContextCommand(args, store) {
51
54
  }
52
55
  return {
53
56
  task,
57
+ deliveryPath: taskDeliveryPath(task),
54
58
  execution,
55
59
  reviewConfig: reader.getReviewConfig(),
56
60
  brief: reader.getTaskBrief(task.id),
@@ -68,10 +72,10 @@ export function runTaskContextCommand(args, store) {
68
72
  changeSets,
69
73
  integrations,
70
74
  publications,
71
- messages: reader.listMessages(task.id),
75
+ messages: operationalTaskRecords(reader.listMessages(task.id), events, "message"),
72
76
  openInputRequests: inputRequests.filter((request) => request.status === "open"),
73
77
  resolvedInputRequests: inputRequests.filter((request) => request.status !== "open"),
74
- events: reader.listEvents(task.id),
78
+ events,
75
79
  nextAction: projectNextAction(nextActionFacts)
76
80
  };
77
81
  });
@@ -137,9 +141,12 @@ export function runTaskContextCommand(args, store) {
137
141
  ...(managedWorkspaces.length === 0
138
142
  ? [" None."]
139
143
  : managedWorkspaces.map((workspace) => (` ${managedWorkspaceLabel(workspace)}: ${workspace.root} (${workspace.entries.filter(({ access }) => access === "write").length} writable / ${workspace.entries.length} Projects)`))),
144
+ `Delivery: ${taskDeliveryPath(task)}`,
140
145
  `Completion evidence: ${task.requireIntegration
141
146
  ? "WorkItem, ChangeSet, and committed Integration required"
142
- : "delivery integration not required"}`,
147
+ : task.projectBindings.length > 0
148
+ ? "clean committed Task main required"
149
+ : "no Project evidence required"}`,
143
150
  `Global review: ${reviewConfig === null
144
151
  ? "disabled"
145
152
  : `${reviewConfig.roleName} (${reviewConfig.trigger})`}`,
@@ -8,6 +8,8 @@ import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
8
8
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
9
9
  import { clearMatchingLeaderStallAttention, isRoleRunStalled, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
10
10
  import { terminalizeExactTaskRun } from "../lifecycle/exactRunTerminalization.js";
11
+ import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
12
+ import { isLinuxProcessLive, listOwnedProcessTree } from "../runtime/sessionOwnerIdentity.js";
11
13
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
12
14
  const LEADER_ROLE = "leader";
13
15
  export function runTaskInputCommand(args, store, options) {
@@ -270,16 +272,33 @@ export function isCurrentGlobalOperator(store, environment) {
270
272
  const role = store.getGlobalRole("operator");
271
273
  if (role === null)
272
274
  return false;
273
- const sessions = store.getGlobalRoleSessionSet(role.name);
274
- const session = activeLiveRoleAgentSession(sessions);
275
- if (sessions === null || session === null || sessions.activeAgentId !== role.activeAgentId) {
276
- return false;
277
- }
278
275
  const agentId = exactIdentity(environment.YUI_AGENT_ID);
279
276
  const adapterId = exactIdentity(environment.YUI_ADAPTER_ID);
280
277
  const launchId = exactIdentity(environment.YUI_LAUNCH_ID);
281
278
  const nativeSessionId = exactIdentity(environment.YUI_NATIVE_SESSION_ID);
282
279
  const binding = role.agentBindings[role.activeAgentId];
280
+ if (agentId === undefined
281
+ || adapterId === undefined
282
+ || launchId === undefined
283
+ || binding === undefined
284
+ || binding.agentId !== agentId
285
+ || binding.adapterId !== adapterId)
286
+ return false;
287
+ const sessions = store.getGlobalRoleSessionSet(role.name);
288
+ const session = activeLiveRoleAgentSession(sessions);
289
+ if (sessions === null || session === null || sessions.activeAgentId !== role.activeAgentId) {
290
+ // Codex learns its native Session ID only after the first Turn. During
291
+ // that narrow bootstrap window, authenticate against the durable launch
292
+ // reservation and its strongly attributed live process owner instead.
293
+ return nativeSessionId === undefined
294
+ && binding.adapterId === "codex"
295
+ && currentProcessBelongsToReservedGlobalLaunch(store, {
296
+ roleName: role.name,
297
+ agentId,
298
+ adapterId,
299
+ launchId
300
+ });
301
+ }
283
302
  // A fresh Codex launch discovers its native Session asynchronously. Its
284
303
  // launch envelope therefore cannot carry YUI_NATIVE_SESSION_ID, but the
285
304
  // durable Session still binds that provider identity to the exact launch
@@ -292,11 +311,7 @@ export function isCurrentGlobalOperator(store, environment) {
292
311
  && session.adapterId === "codex"
293
312
  && session.launchId !== undefined
294
313
  && session.launchId === launchId));
295
- return agentId !== undefined
296
- && adapterId !== undefined
297
- && launchId !== undefined
298
- && binding !== undefined
299
- && binding.agentId === session.agentId
314
+ return binding.agentId === session.agentId
300
315
  && binding.adapterId === session.adapterId
301
316
  && session.agentId === agentId
302
317
  && session.adapterId === adapterId
@@ -304,6 +319,29 @@ export function isCurrentGlobalOperator(store, environment) {
304
319
  && session.launchId === launchId
305
320
  && nativeSessionMatches;
306
321
  }
322
+ function currentProcessBelongsToReservedGlobalLaunch(store, input) {
323
+ if (store.getSessionOwner === undefined || store.getWorkMailbox === undefined)
324
+ return false;
325
+ const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
326
+ scope: "global",
327
+ roleName: input.roleName
328
+ }));
329
+ if (!isRuntimeLaunchReservation(mailbox?.processing, input.launchId)
330
+ || hasRuntimeCleanupObligation(mailbox))
331
+ return false;
332
+ const owner = store.getSessionOwner(input.launchId);
333
+ if (owner === null
334
+ || owner.owner.scope !== "global"
335
+ || owner.owner.roleName !== input.roleName
336
+ || owner.agentId !== input.agentId
337
+ || owner.adapterId !== input.adapterId
338
+ || owner.launchId !== input.launchId
339
+ || owner.providerRoot.attribution !== "launch-env"
340
+ || !isLinuxProcessLive(owner.providerRoot.pid, owner.providerRoot.startIdentity)) {
341
+ return false;
342
+ }
343
+ return listOwnedProcessTree(owner.providerRoot.pid, owner.providerRoot.processGroupId).some(({ pid }) => pid === process.pid);
344
+ }
307
345
  function inputAnswerer(environment) {
308
346
  const env = environment ?? {};
309
347
  if (env.YUI_SESSION_SCOPE === undefined && env.YUI_ROLE === undefined)
@@ -2,6 +2,9 @@ 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
+ import { projectTaskOrchestration } from "../observability/orchestrationMetrics.js";
7
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
5
8
  /**
6
9
  * Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
7
10
  * Folds the existing durable records into exactly one protocol-level next
@@ -55,7 +58,30 @@ export function runTaskNextActionCommand(args, store) {
55
58
  throw taskNotFound(taskId);
56
59
  completionReadiness = projectCompletionReadiness(readinessFacts);
57
60
  }
58
- return { action, repairWave, completionReadiness, knowledgeProposals };
61
+ const events = reader.listEvents(taskId);
62
+ const orchestration = projectTaskOrchestration({
63
+ task: reader.getTask(taskId),
64
+ runs: operationalTaskRecords(reader.listAgentRuns(taskId), events, "agent-run"),
65
+ roleSessionSets: reader.listRoleSessionSets(taskId),
66
+ workItems: reader.listWorkItems(taskId),
67
+ changeSets: reader.listChangeSets(taskId),
68
+ reviewRounds: reader.listReviewRounds(taskId),
69
+ reviewFindings: reader.listReviewFindings(taskId),
70
+ integrations: reader.listIntegrationAttempts(taskId),
71
+ durableJobs: reader.listDurableJobs(taskId),
72
+ publications: reader.listPublicationReferences(taskId),
73
+ decisions: reader.listDecisions(taskId),
74
+ events,
75
+ managedWorkspaces: reader.listManagedWorkspaces(taskId)
76
+ });
77
+ return {
78
+ deliveryPath: taskDeliveryPath(facts.task),
79
+ action,
80
+ repairWave,
81
+ completionReadiness,
82
+ knowledgeProposals,
83
+ orchestration
84
+ };
59
85
  });
60
86
  if (asJson) {
61
87
  return {
@@ -66,7 +92,7 @@ export function runTaskNextActionCommand(args, store) {
66
92
  }
67
93
  return {
68
94
  kind: "output",
69
- output: renderNextAction(data.action, data.repairWave, data.completionReadiness, data.knowledgeProposals),
95
+ output: renderNextAction(data.action, data.deliveryPath, data.repairWave, data.completionReadiness, data.knowledgeProposals, data.orchestration.advisories),
70
96
  data
71
97
  };
72
98
  }
@@ -84,9 +110,10 @@ function repairWaveFor(action, facts) {
84
110
  return null;
85
111
  return planRepairWave(round.id, findings);
86
112
  }
87
- function renderNextAction(action, repairWave, completionReadiness, knowledgeProposals) {
113
+ function renderNextAction(action, deliveryPath, repairWave, completionReadiness, knowledgeProposals, orchestrationAdvisories) {
88
114
  const lines = [
89
115
  `Task: ${action.taskId}`,
116
+ `Delivery: ${deliveryPath}`,
90
117
  `Next action: ${action.kind}`,
91
118
  `Reason: ${action.reason}`,
92
119
  ...(action.refs.length === 0
@@ -131,6 +158,10 @@ function renderNextAction(action, repairWave, completionReadiness, knowledgeProp
131
158
  lines.push(`Completion readiness: ${completionReadiness.blockers.length} blocker(s)`, ...completionReadiness.blockers.map((blocker) => ` ${blocker.code} (${blocker.ref.kind} ${blocker.ref.id}): ${blocker.reason}`
132
159
  + ` — fix: ${blocker.fix}`));
133
160
  }
161
+ if (completionReadiness.advisories.length > 0) {
162
+ lines.push(`Completion advisories (non-blocking): ${completionReadiness.advisories.length}`, ...completionReadiness.advisories.map((advisory) => ` ${advisory.code} (${advisory.ref.kind} ${advisory.ref.id}): ${advisory.reason}`
163
+ + ` — fix before archive: ${advisory.fix}`));
164
+ }
134
165
  }
135
166
  if (repairWave !== null) {
136
167
  lines.push(`Repair wave (${repairWave.openFindingCount} open finding(s), ${repairWave.groups.length} group(s)):`, ...repairWave.groups.map((group) => ` ${group.id}: findings ${group.findingIds.join(", ")}`
@@ -141,5 +172,9 @@ function renderNextAction(action, repairWave, completionReadiness, knowledgeProp
141
172
  lines.push(`Knowledge proposals (non-blocking): ${knowledgeProposals.length} pending`, ...knowledgeProposals.map((proposal) => ` ${proposal.projectId}/${proposal.proposalId}: ${proposal.title}`
142
173
  + ` — review: yui project knowledge proposals list ${proposal.projectId}`));
143
174
  }
175
+ if (orchestrationAdvisories.length > 0) {
176
+ lines.push(`Orchestration advisories (non-blocking): ${orchestrationAdvisories.length}`, ...orchestrationAdvisories.map((advisory) => ` ${advisory.code}: ${advisory.reason}`
177
+ + (advisory.refs.length === 0 ? "" : ` — refs ${advisory.refs.join(", ")}`)));
178
+ }
144
179
  return `${lines.join("\n")}\n`;
145
180
  }
@@ -226,7 +226,8 @@ function collectBlockers(workItems, openInputRequests, attention) {
226
226
  }
227
227
  if (item.status !== "pending")
228
228
  continue;
229
- const dependencies = item.dependsOn.filter((dependency) => (workById.get(dependency)?.status !== "completed"));
229
+ const dependencies = item.dependsOn.filter((dependency) => (workById.get(dependency)?.status !== "completed"
230
+ && workById.get(dependency)?.status !== "retired"));
230
231
  if (dependencies.length === 0)
231
232
  continue;
232
233
  blockers.push({
@@ -7,6 +7,7 @@ import { classifyRuntimeHealth, projectRuntimeMailbox, projectRuntimeObservation
7
7
  import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
8
8
  import { resolveRuntimeHealth } from "../config/yuiConfig.js";
9
9
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
10
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
10
11
  export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
11
12
  const taskOpenInputRequestCount = store.listInputRequests(taskId)
12
13
  .filter((request) => request.status === "open").length;
@@ -140,7 +141,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
140
141
  // Issue 09: the last Run outcome is a separate axis from the Session
141
142
  // lifecycle. A Session that stops after its Run yielded must not retroactively
142
143
  // turn that Run into a failure; the status display keeps both visible.
143
- const lastRun = store.listAgentRuns(taskId)
144
+ const lastRun = operationalTaskRecords(store.listAgentRuns(taskId), store.listEvents(taskId), "agent-run")
144
145
  .filter((candidate) => candidate.roleName === role.name)
145
146
  .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt))[0]
146
147
  ?? null;
@@ -1,3 +1,4 @@
1
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
1
2
  import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
2
3
  import { RUN_BOOTSTRAP_MAX_DELTAS } from "./runContextContract.js";
3
4
  import { contextContentDigest, contextSnapshotRef, createContextSnapshot, validateContextSnapshot } from "./contextSnapshot.js";
@@ -213,9 +214,12 @@ function collectAuthorizedContext(store, run) {
213
214
  if (view === "worker") {
214
215
  for (const dependencyId of item.dependsOn) {
215
216
  const dependency = store.getWorkItem(task.id, dependencyId);
216
- if (dependency === null || dependency.status !== "completed") {
217
+ if (dependency === null
218
+ || (dependency.status !== "completed" && dependency.status !== "retired")) {
217
219
  throw new Error(`Run WorkItem dependency is not accepted: ${dependencyId}.`);
218
220
  }
221
+ if (dependency.status === "retired")
222
+ continue;
219
223
  result.push(materialize("L3", "accepted-work-item", dependency.id, dependency));
220
224
  }
221
225
  }
@@ -242,7 +246,8 @@ function collectAuthorizedContext(store, run) {
242
246
  }
243
247
  }
244
248
  if (view === "leader") {
245
- for (const item of store.listWorkItems(task.id)) {
249
+ const events = store.listEvents(task.id);
250
+ for (const item of store.listWorkItems(task.id).filter(({ status }) => status !== "retired")) {
246
251
  result.push(materialize("L3", "work-item", item.id, item));
247
252
  }
248
253
  for (const decision of store.listDecisions(task.id)) {
@@ -257,14 +262,13 @@ function collectAuthorizedContext(store, run) {
257
262
  for (const finding of store.listReviewFindings(task.id)) {
258
263
  result.push(materialize("L3", "review-finding", finding.id, finding));
259
264
  }
260
- for (const agentRun of store.listAgentRuns(task.id).slice(-24)) {
265
+ for (const agentRun of operationalTaskRecords(store.listAgentRuns(task.id), events, "agent-run").slice(-24)) {
261
266
  result.push(materialize("L4", "agent-run", agentRun.id, agentRun));
262
267
  }
263
- for (const message of store.listMessages(task.id).slice(-16)) {
268
+ for (const message of operationalTaskRecords(store.listMessages(task.id), events, "message").slice(-16)) {
264
269
  result.push(materialize("L4", "task-message", message.id, message));
265
270
  }
266
271
  const publishedTreeAuthorizations = [];
267
- const events = store.listEvents(task.id);
268
272
  for (let index = events.length - 1; index >= 0; index -= 1) {
269
273
  const event = events[index];
270
274
  if (event.type === "task.completed" || event.type === "task.reopened")
@@ -1,20 +1,93 @@
1
1
  import { createHash } from "node:crypto";
2
- import { chmodSync } from "node:fs";
3
- import { join, resolve } from "node:path";
4
- import { exactControlPlaneCommandPrefix, exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
2
+ import { chmodSync, existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
5
5
  import { writeTextFileAtomically } from "../storage/durableFile.js";
6
6
  import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
7
7
  export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
8
+ const ORDINARY_SESSION_CLI = [
9
+ "#!/bin/sh",
10
+ "exec yui \"$@\"",
11
+ ""
12
+ ].join("\n");
13
+ /** Read back one immutable Session Manifest and verify its content digest. */
14
+ export function readSessionBootstrapManifest(path) {
15
+ const source = resolve(path);
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(readFileSync(source, "utf8"));
19
+ }
20
+ catch (error) {
21
+ throw new Error(`Session Manifest is unreadable: ${source}.`, { cause: error });
22
+ }
23
+ if (parsed === null || typeof parsed !== "object") {
24
+ throw new Error("Session Manifest is invalid.");
25
+ }
26
+ const record = parsed;
27
+ const claimedDigest = requireDigest(record.digest, "Session Manifest digest");
28
+ const { digest: _digest, ...body } = record;
29
+ if (digest(body) !== claimedDigest) {
30
+ throw new Error("Session Manifest digest does not match its immutable content.");
31
+ }
32
+ if (record.schemaVersion !== SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION
33
+ || record.protocol !== SESSION_CONTEXT_PROTOCOL
34
+ || record.owner === null
35
+ || typeof record.owner !== "object"
36
+ || (record.owner.scope !== "global"
37
+ && record.owner.scope !== "task")
38
+ || typeof record.effectiveRevision !== "number"
39
+ || !Number.isSafeInteger(record.effectiveRevision)
40
+ || record.effectiveRevision < 1
41
+ || (record.roleKind !== "operator"
42
+ && record.roleKind !== "global"
43
+ && record.roleKind !== "leader"
44
+ && record.roleKind !== "worker"
45
+ && record.roleKind !== "reviewer")
46
+ || record.controlPlane === null
47
+ || typeof record.controlPlane !== "object"
48
+ || !Array.isArray(record.skills)
49
+ || record.roleProfileRef === null
50
+ || typeof record.roleProfileRef !== "object"
51
+ || record.contextProtocol === null
52
+ || typeof record.contextProtocol !== "object") {
53
+ throw new Error("Session Manifest shape is invalid.");
54
+ }
55
+ const owner = record.owner;
56
+ if (owner.scope === "task" && typeof owner.taskId !== "string") {
57
+ throw new Error("Task Session Manifest owner is invalid.");
58
+ }
59
+ const control = record.controlPlane;
60
+ requireText(control.descriptorPath, "Session Manifest control descriptor path");
61
+ requireText(control.sessionCliPath, "Session Manifest CLI path");
62
+ requireDigest(control.digest, "Session Manifest control-plane digest");
63
+ for (const skill of record.skills) {
64
+ if (skill === null || typeof skill !== "object") {
65
+ throw new Error("Session Manifest Skill entry is invalid.");
66
+ }
67
+ const entry = skill;
68
+ requireText(entry.id, "Session Manifest Skill id");
69
+ requireText(entry.path, "Session Manifest Skill path");
70
+ requireDigest(entry.digest, "Session Manifest Skill digest");
71
+ }
72
+ const profile = record.roleProfileRef;
73
+ requireDigest(profile.digest, "Session Manifest Role Profile digest");
74
+ requireText(profile.path, "Session Manifest Role Profile path");
75
+ const protocol = record.contextProtocol;
76
+ requireText(protocol.loadCommand, "Session Manifest Context load command");
77
+ if (protocol.expandCommand !== undefined) {
78
+ requireText(protocol.expandCommand, "Session Manifest Context expand command");
79
+ }
80
+ return Object.freeze(parsed);
81
+ }
8
82
  export function materializeSessionBootstrap(input) {
9
83
  const home = resolve(input.yuiHome);
10
84
  const controlDigest = exactControlPlaneDigest(input.controlPlane);
11
85
  const descriptorPath = resolve(join(home, "runtime", "control-plane", `${controlDigest}.json`));
12
86
  writeImmutableText(descriptorPath, `${serializeExactDescriptor(input.controlPlane)}\n`);
13
- const sessionCliContent = [
14
- "#!/bin/sh",
15
- `exec ${exactControlPlaneCommandPrefix(input.controlPlane)} \"$@\"`,
16
- ""
17
- ].join("\n");
87
+ // Session identity is carried by the immutable Manifest and durable Role/
88
+ // Run fences. Resolve the ordinary CLI on every invocation so package or
89
+ // release upgrades do not invalidate a still-current native Session.
90
+ const sessionCliContent = ORDINARY_SESSION_CLI;
18
91
  const sessionCliDigest = digest(sessionCliContent);
19
92
  const sessionCliPath = resolve(join(home, "runtime", "session-cli", `yui-${sessionCliDigest}.sh`));
20
93
  writeImmutableText(sessionCliPath, sessionCliContent);
@@ -53,11 +126,11 @@ export function materializeSessionBootstrap(input) {
53
126
  roleProfileRef: { digest: profileDigest, path: roleProfilePath },
54
127
  contextProtocol: input.owner.scope === "global"
55
128
  ? {
56
- loadCommand: `\"${sessionCliPath}\" session context \"$YUI_ROLE\" --json`
129
+ loadCommand: "yui session context \"$YUI_ROLE\" --json"
57
130
  }
58
131
  : {
59
- loadCommand: `\"${sessionCliPath}\" task run context \"$YUI_TASK_ID/<run-id>\" --json`,
60
- expandCommand: `\"${sessionCliPath}\" task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json`
132
+ loadCommand: "yui task run context \"$YUI_TASK_ID/<run-id>\" --json",
133
+ expandCommand: "yui task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json"
61
134
  }
62
135
  };
63
136
  const manifest = Object.freeze({ ...body, digest: digest(body) });
@@ -71,6 +144,68 @@ export function materializeSessionBootstrap(input) {
71
144
  descriptorPath
72
145
  });
73
146
  }
147
+ /**
148
+ * Converts wrappers produced before the protocol-compatible Session CLI to an
149
+ * ordinary `yui` invocation. Only a valid Session Manifest may nominate a
150
+ * wrapper, and only the exact legacy two-line wrapper shape is changed. The
151
+ * Manifest and its frozen descriptor stay immutable and continue to
152
+ * authenticate the Session; repeated refreshes are no-ops.
153
+ */
154
+ export function refreshManagedSessionCliWrappers(homeInput) {
155
+ const home = resolve(homeInput);
156
+ const manifestDirectory = resolve(join(home, "runtime", "session-manifests"));
157
+ const sessionCliDirectory = resolve(join(home, "runtime", "session-cli"));
158
+ if (!existsSync(manifestDirectory)) {
159
+ return Object.freeze({ refreshed: 0, current: 0, skipped: 0 });
160
+ }
161
+ const wrapperPaths = new Set();
162
+ let skipped = 0;
163
+ for (const name of readdirSync(manifestDirectory).filter((entry) => entry.endsWith(".json"))) {
164
+ const manifestPath = resolve(join(manifestDirectory, name));
165
+ try {
166
+ const manifest = readSessionBootstrapManifest(manifestPath);
167
+ if (manifestPath !== resolve(join(manifestDirectory, `${manifest.digest}.json`))) {
168
+ skipped += 1;
169
+ continue;
170
+ }
171
+ const wrapperPath = resolve(manifest.controlPlane.sessionCliPath);
172
+ if (dirname(wrapperPath) !== sessionCliDirectory) {
173
+ skipped += 1;
174
+ continue;
175
+ }
176
+ wrapperPaths.add(wrapperPath);
177
+ }
178
+ catch {
179
+ // Historical or incomplete manifests are audit material. They must not
180
+ // block current Sessions or an otherwise compatible package update.
181
+ skipped += 1;
182
+ }
183
+ }
184
+ let refreshed = 0;
185
+ let current = 0;
186
+ for (const wrapperPath of wrapperPaths) {
187
+ if (!existsSync(wrapperPath)) {
188
+ skipped += 1;
189
+ continue;
190
+ }
191
+ const content = readFileSync(wrapperPath, "utf8");
192
+ if (content === ORDINARY_SESSION_CLI) {
193
+ current += 1;
194
+ continue;
195
+ }
196
+ if (!isLegacyExactSessionCli(content)) {
197
+ skipped += 1;
198
+ continue;
199
+ }
200
+ writeTextFileAtomically(wrapperPath, ORDINARY_SESSION_CLI);
201
+ chmodSync(wrapperPath, 0o700);
202
+ refreshed += 1;
203
+ }
204
+ return Object.freeze({ refreshed, current, skipped });
205
+ }
206
+ function isLegacyExactSessionCli(content) {
207
+ return /^#!\/bin\/sh\nexec [^\n]+ '--yui-control' '[a-f0-9]{64}' "\$@"\n$/u.test(content);
208
+ }
74
209
  function writeImmutableText(path, content) {
75
210
  writeTextFileAtomically(path, content);
76
211
  chmodSync(path, 0o600);
@@ -79,3 +214,15 @@ function digest(value) {
79
214
  const bytes = typeof value === "string" ? value : JSON.stringify(value);
80
215
  return createHash("sha256").update(bytes).digest("hex");
81
216
  }
217
+ function requireDigest(value, label) {
218
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
219
+ throw new Error(`${label} is invalid.`);
220
+ }
221
+ return value;
222
+ }
223
+ function requireText(value, label) {
224
+ if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
225
+ throw new Error(`${label} is invalid.`);
226
+ }
227
+ return value.trim();
228
+ }
@@ -1,4 +1,5 @@
1
1
  import { renderWakeReason } from "../scheduler/wakeReason.js";
2
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
2
3
  /**
3
4
  * Issue 04 (context token budget) — long-term design:
4
5
  *
@@ -25,12 +26,13 @@ export function buildTaskWakeEnvelope(reader, request) {
25
26
  throw new Error(`Wake envelope ${request.wakeId} must carry at least one reason.`);
26
27
  }
27
28
  const fromTime = Date.parse(request.fromCursor);
29
+ const events = reader.listEvents(request.taskId);
28
30
  const counts = {
29
- events: reader.listEvents(request.taskId)
31
+ events: events
30
32
  .filter((record) => Date.parse(record.createdAt) > fromTime).length,
31
- messages: reader.listMessages(request.taskId)
33
+ messages: operationalTaskRecords(reader.listMessages(request.taskId), events, "message")
32
34
  .filter((record) => Date.parse(record.createdAt) > fromTime).length,
33
- runs: reader.listAgentRuns(request.taskId)
35
+ runs: operationalTaskRecords(reader.listAgentRuns(request.taskId), events, "agent-run")
34
36
  .filter((record) => Date.parse(record.createdAt) > fromTime).length
35
37
  };
36
38
  const lines = [