@zq-silk/yui 0.8.2 → 0.8.6

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 (68) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +61 -9
  3. package/dist/cli/commandCatalog.js +31 -14
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli.js +187 -22
  6. package/dist/commands/executionAuditCommands.js +30 -0
  7. package/dist/commands/operatorCommands.js +42 -1
  8. package/dist/commands/taskCommands.js +648 -74
  9. package/dist/commands/taskCompletionGate.js +166 -2
  10. package/dist/commands/taskContextCommand.js +6 -1
  11. package/dist/commands/taskInputCommands.js +48 -10
  12. package/dist/commands/taskNextActionCommand.js +36 -3
  13. package/dist/context/runContextPack.js +19 -4
  14. package/dist/context/sessionBootstrapManifest.js +83 -2
  15. package/dist/controller/clientRuntime.js +7 -7
  16. package/dist/controller/controller.js +16 -8
  17. package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
  18. package/dist/controller/handoverCandidate.js +10 -3
  19. package/dist/controller/sessionNotify.js +4 -22
  20. package/dist/executor/agentAdapter.js +2 -2
  21. package/dist/executor/agentExecutor.js +25 -5
  22. package/dist/executor/fileRoleLaunchPlanner.js +16 -11
  23. package/dist/integration/gitIntegrationService.js +50 -2
  24. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  25. package/dist/observability/executionAudit.js +47 -1
  26. package/dist/observability/faultClassification.js +6 -4
  27. package/dist/observability/orchestrationMetrics.js +196 -0
  28. package/dist/operator/operatorSessionHistory.js +36 -0
  29. package/dist/release/releaseHandover.js +7 -5
  30. package/dist/release/runtimeRelease.js +15 -0
  31. package/dist/repository/gitWorkspace.js +7 -4
  32. package/dist/repository/taskBaseFreshness.js +4 -2
  33. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  34. package/dist/review/deltaRecheck.js +3 -2
  35. package/dist/review/reviewFindingLedger.js +5 -4
  36. package/dist/review/reviewOutcomeClassifier.js +252 -54
  37. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  38. package/dist/review/taskFinalReviewContractRebind.js +350 -0
  39. package/dist/run/agentRun.js +2 -2
  40. package/dist/run/runIdentity.js +10 -70
  41. package/dist/runtime/agentHost.js +3 -4
  42. package/dist/runtime/codexAppServerRuntime.js +6 -0
  43. package/dist/runtime/firstProgressStopLoss.js +52 -0
  44. package/dist/runtime/launchBroker.js +10 -2
  45. package/dist/runtime/runtimeDeadlines.js +14 -0
  46. package/dist/runtime/sessionTitle.js +24 -12
  47. package/dist/runtime/structuredProviderHost.js +7 -1
  48. package/dist/runtime/tmuxAdapters.js +10 -3
  49. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  50. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  51. package/dist/scheduler/wakeReason.js +2 -0
  52. package/dist/storage/sqliteStore.js +10 -1
  53. package/dist/storage/taskStore.js +12 -1
  54. package/dist/task/completionReadiness.js +91 -19
  55. package/dist/task/deliveryGuard.js +3 -1
  56. package/dist/task/nextAction.js +146 -51
  57. package/dist/task/publicationReference.js +1 -0
  58. package/dist/task/repairWave.js +14 -1
  59. package/dist/task/task.js +10 -0
  60. package/dist/web/webSnapshot.js +7 -1
  61. package/dist/workItem/workItem.js +12 -0
  62. package/dist/workspace/workItemChangeSetManager.js +2 -1
  63. package/i18n/README.zh-CN.md +28 -8
  64. package/package.json +1 -1
  65. package/skills/yui-leader/SKILL.md +79 -32
  66. package/skills/yui-operator/SKILL.md +51 -10
  67. package/skills/yui-reviewer/SKILL.md +23 -0
  68. package/skills/yui-runtime/SKILL.md +7 -2
@@ -2,7 +2,171 @@ 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
+ import { publicationExternalKey } from "../task/publicationReference.js";
7
+ import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
5
8
  import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
9
+ /**
10
+ * Verify the one supported ancestry waiver before `task complete` mutates any
11
+ * durable state. The explicit Publication must be the current verified merged
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.
15
+ */
16
+ export async function verifyTaskCompletionPublishedTree(taskId, publicationId, store, options = {}) {
17
+ const task = store.getTask(taskId);
18
+ if (task === null)
19
+ throw usageError(`Task not found: ${taskId}.`);
20
+ if (task.status !== "active") {
21
+ throw usageError(`Task is not active: ${task.id}.`);
22
+ }
23
+ const publication = requireCurrentVerifiedPublication(store, task.id, publicationId);
24
+ const binding = task.projectBindings.find(({ projectId }) => (projectId === publication.projectId));
25
+ if (binding === undefined) {
26
+ throw usageError(`Publication ${publication.id} Project is not bound to Task ${task.id}: `
27
+ + `${publication.projectId}.`);
28
+ }
29
+ if (publication.localCommit === undefined || publication.remoteCommit === undefined) {
30
+ throw usageError(`Publication ${publication.id} must record exact local and remote commits.`);
31
+ }
32
+ 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.`);
39
+ }
40
+ const git = options.git ?? new NodeGitWorkspace();
41
+ const actualHeads = new Map();
42
+ for (const taskBinding of task.projectBindings) {
43
+ const entry = workspaceProjectEntry(workspace, taskBinding.projectId);
44
+ if (entry === undefined || entry.access !== "write") {
45
+ throw usageError(`Task ${task.id} has no writable managed main workspace for Project ${taskBinding.projectId}.`);
46
+ }
47
+ const actualCommit = (await git.inspect(entry.path, "HEAD")).baseCommit;
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
+ }
57
+ }
58
+ actualHeads.set(taskBinding.projectId, actualCommit);
59
+ }
60
+ if (latestReview !== undefined
61
+ && actualHeads.size !== latestReview.taskCandidate.projects.length) {
62
+ throw usageError(`Task-final Review ${latestReview.id} Project set does not match Task ${task.id}.`);
63
+ }
64
+ const entry = workspaceProjectEntry(workspace, publication.projectId);
65
+ const localCommit = actualHeads.get(publication.projectId);
66
+ if (publication.localCommit !== localCommit) {
67
+ throw usageError(`Publication ${publication.id} local commit ${publication.localCommit} `
68
+ + `does not match Task head ${localCommit}.`);
69
+ }
70
+ let remoteCommit;
71
+ let localTree;
72
+ let remoteTree;
73
+ try {
74
+ remoteCommit = (await git.inspect(entry.path, publication.remoteCommit)).baseCommit;
75
+ [localTree, remoteTree] = await Promise.all([
76
+ git.resolveTree(entry.path, localCommit),
77
+ git.resolveTree(entry.path, remoteCommit)
78
+ ]);
79
+ }
80
+ catch (error) {
81
+ throw usageError(`Publication ${publication.id} commit/tree evidence is unavailable: `
82
+ + `${error instanceof Error ? error.message : String(error)}`);
83
+ }
84
+ if (remoteCommit !== publication.remoteCommit) {
85
+ throw usageError(`Publication ${publication.id} remote commit changed: expected `
86
+ + `${publication.remoteCommit}, found ${remoteCommit}.`);
87
+ }
88
+ const [localContainsRemote, remoteContainsLocal] = await Promise.all([
89
+ git.isAncestor(entry.path, remoteCommit, localCommit),
90
+ git.isAncestor(entry.path, localCommit, remoteCommit)
91
+ ]);
92
+ if (localContainsRemote || remoteContainsLocal) {
93
+ throw usageError(`Publication ${publication.id} is not ancestry-divergent from Task head ${localCommit}; `
94
+ + "use normal Task completion.");
95
+ }
96
+ if (localTree !== remoteTree) {
97
+ throw usageError(`Publication ${publication.id} Git trees differ: `
98
+ + `${localCommit}^{tree}=${localTree}, ${remoteCommit}^{tree}=${remoteTree}.`);
99
+ }
100
+ return {
101
+ taskId: task.id,
102
+ projectId: publication.projectId,
103
+ publicationId: publication.id,
104
+ ...(latestReview === undefined ? {} : { reviewRoundId: latestReview.id }),
105
+ localCommit,
106
+ remoteCommit,
107
+ tree: localTree
108
+ };
109
+ }
110
+ /** Re-derive every durable half of the asynchronous Git proof under the Task
111
+ * completion transaction. The physical heads are the CLI snapshot captured
112
+ * immediately before mutation; any record or head drift fails closed. */
113
+ export function assertTaskCompletionPublishedTreeProof(store, task, publicationId, proof, actualCandidate) {
114
+ if (proof === undefined
115
+ || proof.taskId !== task.id
116
+ || proof.publicationId !== publicationId) {
117
+ throw usageError(`Published-tree completion proof is missing or mismatched for ${task.id}/${publicationId}.`);
118
+ }
119
+ const publication = requireCurrentVerifiedPublication(store, task.id, publicationId);
120
+ if (publication.projectId !== proof.projectId
121
+ || publication.localCommit !== proof.localCommit
122
+ || publication.remoteCommit !== proof.remoteCommit) {
123
+ throw usageError(`Publication evidence changed before Task completion: ${publication.id}.`);
124
+ }
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}.`);
137
+ }
138
+ const actualCommit = actualCandidate.projects.find(({ projectId }) => (projectId === proof.projectId))?.commit;
139
+ if (actualCommit !== proof.localCommit) {
140
+ throw usageError(`Task head changed before published-tree completion: `
141
+ + `${proof.projectId}@${actualCommit ?? "missing"}.`);
142
+ }
143
+ return proof;
144
+ }
145
+ function requireCurrentVerifiedPublication(store, taskId, publicationId) {
146
+ const publication = store.getPublicationReference(taskId, publicationId);
147
+ if (publication === null) {
148
+ throw usageError(`Publication reference not found: ${taskId}/${publicationId}.`);
149
+ }
150
+ const current = store.findPublicationReferenceByExternalKey(publicationExternalKey(publication));
151
+ if (current === null || current.taskId !== taskId || current.id !== publication.id) {
152
+ throw usageError(`Publication ${publication.id} is not the current unsuperseded record for its external identity.`);
153
+ }
154
+ if (publication.state !== "merged" || publication.verification !== "verified") {
155
+ throw usageError(`Publication ${publication.id} must be merged and verified before Task completion.`);
156
+ }
157
+ return publication;
158
+ }
159
+ function latestTaskFinalReview(store, taskId) {
160
+ return store.listReviewRounds(taskId)
161
+ .filter((round) => (round.scope ?? "work-item") === "task")
162
+ .sort((left, right) => (left.id.localeCompare(right.id, undefined, { numeric: true })))
163
+ .at(-1);
164
+ }
165
+ function sameTaskCandidate(left, right) {
166
+ return left.projects.length === right.projects.length
167
+ && left.projects.every((project, index) => (project.projectId === right.projects[index]?.projectId
168
+ && project.commit === right.projects[index]?.commit));
169
+ }
6
170
  /**
7
171
  * Reconcile configured remote baselines before a Task completion attempt.
8
172
  *
@@ -169,7 +333,7 @@ async function hasCompletedTaskFinalReviewForCurrentCandidate(store, task, works
169
333
  .filter((round) => (round.scope ?? "work-item") === "task")
170
334
  .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
171
335
  const latest = rounds.at(-1);
172
- if (latest === undefined || latest.status !== "completed")
336
+ if (latest === undefined || !isSemanticReviewRound(latest, store))
173
337
  return false;
174
338
  if (latest.taskCandidate === undefined)
175
339
  return false;
@@ -202,7 +366,7 @@ function latestCommittedIntegration(store, taskId, projectId) {
202
366
  function hasFrozenTaskBaseline(store, taskId, projectId, currentCommit) {
203
367
  return store.listReviewRounds(taskId)
204
368
  .some((round) => ((round.scope ?? "work-item") === "task"
205
- && round.status === "completed"
369
+ && isSemanticReviewRound(round, store)
206
370
  && round.taskCandidate !== undefined
207
371
  && round.taskCandidate.projects.some((entry) => (entry.projectId === projectId && entry.commit === currentCommit))));
208
372
  }
@@ -4,6 +4,7 @@ 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";
@@ -51,6 +52,7 @@ export function runTaskContextCommand(args, store) {
51
52
  }
52
53
  return {
53
54
  task,
55
+ deliveryPath: taskDeliveryPath(task),
54
56
  execution,
55
57
  reviewConfig: reader.getReviewConfig(),
56
58
  brief: reader.getTaskBrief(task.id),
@@ -137,9 +139,12 @@ export function runTaskContextCommand(args, store) {
137
139
  ...(managedWorkspaces.length === 0
138
140
  ? [" None."]
139
141
  : managedWorkspaces.map((workspace) => (` ${managedWorkspaceLabel(workspace)}: ${workspace.root} (${workspace.entries.filter(({ access }) => access === "write").length} writable / ${workspace.entries.length} Projects)`))),
142
+ `Delivery: ${taskDeliveryPath(task)}`,
140
143
  `Completion evidence: ${task.requireIntegration
141
144
  ? "WorkItem, ChangeSet, and committed Integration required"
142
- : "delivery integration not required"}`,
145
+ : task.projectBindings.length > 0
146
+ ? "clean committed Task main required"
147
+ : "no Project evidence required"}`,
143
148
  `Global review: ${reviewConfig === null
144
149
  ? "disabled"
145
150
  : `${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,8 @@ 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";
5
7
  /**
6
8
  * Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
7
9
  * Folds the existing durable records into exactly one protocol-level next
@@ -55,7 +57,29 @@ export function runTaskNextActionCommand(args, store) {
55
57
  throw taskNotFound(taskId);
56
58
  completionReadiness = projectCompletionReadiness(readinessFacts);
57
59
  }
58
- return { action, repairWave, completionReadiness, knowledgeProposals };
60
+ const orchestration = projectTaskOrchestration({
61
+ task: reader.getTask(taskId),
62
+ runs: reader.listAgentRuns(taskId),
63
+ roleSessionSets: reader.listRoleSessionSets(taskId),
64
+ workItems: reader.listWorkItems(taskId),
65
+ changeSets: reader.listChangeSets(taskId),
66
+ reviewRounds: reader.listReviewRounds(taskId),
67
+ reviewFindings: reader.listReviewFindings(taskId),
68
+ integrations: reader.listIntegrationAttempts(taskId),
69
+ durableJobs: reader.listDurableJobs(taskId),
70
+ publications: reader.listPublicationReferences(taskId),
71
+ decisions: reader.listDecisions(taskId),
72
+ events: reader.listEvents(taskId),
73
+ managedWorkspaces: reader.listManagedWorkspaces(taskId)
74
+ });
75
+ return {
76
+ deliveryPath: taskDeliveryPath(facts.task),
77
+ action,
78
+ repairWave,
79
+ completionReadiness,
80
+ knowledgeProposals,
81
+ orchestration
82
+ };
59
83
  });
60
84
  if (asJson) {
61
85
  return {
@@ -66,7 +90,7 @@ export function runTaskNextActionCommand(args, store) {
66
90
  }
67
91
  return {
68
92
  kind: "output",
69
- output: renderNextAction(data.action, data.repairWave, data.completionReadiness, data.knowledgeProposals),
93
+ output: renderNextAction(data.action, data.deliveryPath, data.repairWave, data.completionReadiness, data.knowledgeProposals, data.orchestration.advisories),
70
94
  data
71
95
  };
72
96
  }
@@ -84,9 +108,10 @@ function repairWaveFor(action, facts) {
84
108
  return null;
85
109
  return planRepairWave(round.id, findings);
86
110
  }
87
- function renderNextAction(action, repairWave, completionReadiness, knowledgeProposals) {
111
+ function renderNextAction(action, deliveryPath, repairWave, completionReadiness, knowledgeProposals, orchestrationAdvisories) {
88
112
  const lines = [
89
113
  `Task: ${action.taskId}`,
114
+ `Delivery: ${deliveryPath}`,
90
115
  `Next action: ${action.kind}`,
91
116
  `Reason: ${action.reason}`,
92
117
  ...(action.refs.length === 0
@@ -131,6 +156,10 @@ function renderNextAction(action, repairWave, completionReadiness, knowledgeProp
131
156
  lines.push(`Completion readiness: ${completionReadiness.blockers.length} blocker(s)`, ...completionReadiness.blockers.map((blocker) => ` ${blocker.code} (${blocker.ref.kind} ${blocker.ref.id}): ${blocker.reason}`
132
157
  + ` — fix: ${blocker.fix}`));
133
158
  }
159
+ if (completionReadiness.advisories.length > 0) {
160
+ lines.push(`Completion advisories (non-blocking): ${completionReadiness.advisories.length}`, ...completionReadiness.advisories.map((advisory) => ` ${advisory.code} (${advisory.ref.kind} ${advisory.ref.id}): ${advisory.reason}`
161
+ + ` — fix before archive: ${advisory.fix}`));
162
+ }
134
163
  }
135
164
  if (repairWave !== null) {
136
165
  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 +170,9 @@ function renderNextAction(action, repairWave, completionReadiness, knowledgeProp
141
170
  lines.push(`Knowledge proposals (non-blocking): ${knowledgeProposals.length} pending`, ...knowledgeProposals.map((proposal) => ` ${proposal.projectId}/${proposal.proposalId}: ${proposal.title}`
142
171
  + ` — review: yui project knowledge proposals list ${proposal.projectId}`));
143
172
  }
173
+ if (orchestrationAdvisories.length > 0) {
174
+ lines.push(`Orchestration advisories (non-blocking): ${orchestrationAdvisories.length}`, ...orchestrationAdvisories.map((advisory) => ` ${advisory.code}: ${advisory.reason}`
175
+ + (advisory.refs.length === 0 ? "" : ` — refs ${advisory.refs.join(", ")}`)));
176
+ }
144
177
  return `${lines.join("\n")}\n`;
145
178
  }
@@ -1,3 +1,4 @@
1
+ import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
1
2
  import { RUN_BOOTSTRAP_MAX_DELTAS } from "./runContextContract.js";
2
3
  import { contextContentDigest, contextSnapshotRef, createContextSnapshot, validateContextSnapshot } from "./contextSnapshot.js";
3
4
  export const RUN_CONTEXT_PACK_SCHEMA_VERSION = 1;
@@ -123,11 +124,12 @@ export function buildRunContextPack(store, taskId, runId) {
123
124
  });
124
125
  return pack;
125
126
  }
126
- export function expandRunContextRef(store, taskId, runId, refId) {
127
+ export function expandRunContextRef(store, taskId, runId, refId, refStore) {
127
128
  const pack = buildRunContextPack(store, taskId, runId);
128
- const authorized = pack.pointers.filter((ref) => ref.refId === refId);
129
+ const authorized = pack.pointers.filter((ref) => (ref.refId === refId && (refStore === undefined || ref.store === refStore)));
130
+ const selector = refStore === undefined ? refId : `${refStore}/${refId}`;
129
131
  if (authorized.length !== 1) {
130
- throw new Error(`Run Context ref is not uniquely authorized: ${refId}.`);
132
+ throw new Error(`Run Context ref is not uniquely authorized: ${selector}.`);
131
133
  }
132
134
  const run = requireExactRun(store, taskId, runId);
133
135
  const snapshotRef = run.assignment.contextSnapshotRef;
@@ -135,7 +137,7 @@ export function expandRunContextRef(store, taskId, runId, refId) {
135
137
  ? collectAuthorizedContext(store, run).find(({ ref }) => (contextRefIdentity(ref) === contextRefIdentity(authorized[0])))
136
138
  : store.getContextSnapshot(taskId, snapshotRef.id)?.resources.find(({ ref }) => (contextRefIdentity(ref) === contextRefIdentity(authorized[0])));
137
139
  if (materialized === undefined || materialized.ref.digest !== authorized[0].digest) {
138
- throw new Error(`Run Context ref is unavailable or drifted: ${refId}.`);
140
+ throw new Error(`Run Context ref is unavailable or drifted: ${selector}.`);
139
141
  }
140
142
  const bytes = Buffer.byteLength(JSON.stringify(materialized.value), "utf8");
141
143
  if (bytes > RUN_CONTEXT_EXPAND_MAX_BYTES) {
@@ -261,6 +263,19 @@ function collectAuthorizedContext(store, run) {
261
263
  for (const message of store.listMessages(task.id).slice(-16)) {
262
264
  result.push(materialize("L4", "task-message", message.id, message));
263
265
  }
266
+ const publishedTreeAuthorizations = [];
267
+ const events = store.listEvents(task.id);
268
+ for (let index = events.length - 1; index >= 0; index -= 1) {
269
+ const event = events[index];
270
+ if (event.type === "task.completed" || event.type === "task.reopened")
271
+ break;
272
+ if (event.type === TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT) {
273
+ publishedTreeAuthorizations.push(event);
274
+ }
275
+ }
276
+ for (const event of publishedTreeAuthorizations.reverse().slice(-16)) {
277
+ result.push(materialize("L4", "task-event", event.id, event));
278
+ }
264
279
  for (const request of store.listOpenInputRequests([task.id])) {
265
280
  result.push(materialize("L4", "input-request", request.id, request));
266
281
  }
@@ -1,10 +1,79 @@
1
1
  import { createHash } from "node:crypto";
2
- import { chmodSync } from "node:fs";
2
+ import { chmodSync, readFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { exactControlPlaneCommandPrefix, 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
+ /** Read back one immutable Session Manifest and verify its content digest. */
9
+ export function readSessionBootstrapManifest(path) {
10
+ const source = resolve(path);
11
+ let parsed;
12
+ try {
13
+ parsed = JSON.parse(readFileSync(source, "utf8"));
14
+ }
15
+ catch (error) {
16
+ throw new Error(`Session Manifest is unreadable: ${source}.`, { cause: error });
17
+ }
18
+ if (parsed === null || typeof parsed !== "object") {
19
+ throw new Error("Session Manifest is invalid.");
20
+ }
21
+ const record = parsed;
22
+ const claimedDigest = requireDigest(record.digest, "Session Manifest digest");
23
+ const { digest: _digest, ...body } = record;
24
+ if (digest(body) !== claimedDigest) {
25
+ throw new Error("Session Manifest digest does not match its immutable content.");
26
+ }
27
+ if (record.schemaVersion !== SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION
28
+ || record.protocol !== SESSION_CONTEXT_PROTOCOL
29
+ || record.owner === null
30
+ || typeof record.owner !== "object"
31
+ || (record.owner.scope !== "global"
32
+ && record.owner.scope !== "task")
33
+ || typeof record.effectiveRevision !== "number"
34
+ || !Number.isSafeInteger(record.effectiveRevision)
35
+ || record.effectiveRevision < 1
36
+ || (record.roleKind !== "operator"
37
+ && record.roleKind !== "global"
38
+ && record.roleKind !== "leader"
39
+ && record.roleKind !== "worker"
40
+ && record.roleKind !== "reviewer")
41
+ || record.controlPlane === null
42
+ || typeof record.controlPlane !== "object"
43
+ || !Array.isArray(record.skills)
44
+ || record.roleProfileRef === null
45
+ || typeof record.roleProfileRef !== "object"
46
+ || record.contextProtocol === null
47
+ || typeof record.contextProtocol !== "object") {
48
+ throw new Error("Session Manifest shape is invalid.");
49
+ }
50
+ const owner = record.owner;
51
+ if (owner.scope === "task" && typeof owner.taskId !== "string") {
52
+ throw new Error("Task Session Manifest owner is invalid.");
53
+ }
54
+ const control = record.controlPlane;
55
+ requireText(control.descriptorPath, "Session Manifest control descriptor path");
56
+ requireText(control.sessionCliPath, "Session Manifest CLI path");
57
+ requireDigest(control.digest, "Session Manifest control-plane digest");
58
+ for (const skill of record.skills) {
59
+ if (skill === null || typeof skill !== "object") {
60
+ throw new Error("Session Manifest Skill entry is invalid.");
61
+ }
62
+ const entry = skill;
63
+ requireText(entry.id, "Session Manifest Skill id");
64
+ requireText(entry.path, "Session Manifest Skill path");
65
+ requireDigest(entry.digest, "Session Manifest Skill digest");
66
+ }
67
+ const profile = record.roleProfileRef;
68
+ requireDigest(profile.digest, "Session Manifest Role Profile digest");
69
+ requireText(profile.path, "Session Manifest Role Profile path");
70
+ const protocol = record.contextProtocol;
71
+ requireText(protocol.loadCommand, "Session Manifest Context load command");
72
+ if (protocol.expandCommand !== undefined) {
73
+ requireText(protocol.expandCommand, "Session Manifest Context expand command");
74
+ }
75
+ return Object.freeze(parsed);
76
+ }
8
77
  export function materializeSessionBootstrap(input) {
9
78
  const home = resolve(input.yuiHome);
10
79
  const controlDigest = exactControlPlaneDigest(input.controlPlane);
@@ -57,7 +126,7 @@ export function materializeSessionBootstrap(input) {
57
126
  }
58
127
  : {
59
128
  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> --mode full --json`
129
+ expandCommand: `\"${sessionCliPath}\" task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json`
61
130
  }
62
131
  };
63
132
  const manifest = Object.freeze({ ...body, digest: digest(body) });
@@ -79,3 +148,15 @@ function digest(value) {
79
148
  const bytes = typeof value === "string" ? value : JSON.stringify(value);
80
149
  return createHash("sha256").update(bytes).digest("hex");
81
150
  }
151
+ function requireDigest(value, label) {
152
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
153
+ throw new Error(`${label} is invalid.`);
154
+ }
155
+ return value;
156
+ }
157
+ function requireText(value, label) {
158
+ if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
159
+ throw new Error(`${label} is invalid.`);
160
+ }
161
+ return value.trim();
162
+ }
@@ -10,12 +10,9 @@ import { EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES } from "./domainIdentity.js";
10
10
  import { YUI_VERSION, yuiVersionIdentity } from "../version.js";
11
11
  import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
12
12
  import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
13
+ import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
13
14
  const STARTUP_TIMEOUT_MS = 5_000;
14
- // A lifecycle RPC may legitimately occupy the Controller for 30 seconds.
15
- // Restart must allow that request to drain before deciding shutdown is stuck.
16
- const SHUTDOWN_TIMEOUT_MS = 45_000;
17
15
  const POLL_INTERVAL_MS = 50;
18
- const LIFECYCLE_REQUEST_TIMEOUT_MS = 30_000;
19
16
  const ENVIRONMENT_REFRESH_TIMEOUT_MS = 500;
20
17
  const CONFIGURATION_REFRESH_TIMEOUT_MS = 500;
21
18
  const CONTROLLER_OPERATIONAL_ENVIRONMENT = [
@@ -168,7 +165,7 @@ function spawnDetachedFileTaskController(home, environment) {
168
165
  /** Stops the per-home Controller and waits until its owned discovery is gone. */
169
166
  export async function stopFileTaskController(home, options = {}) {
170
167
  const call = options.call ?? callController;
171
- const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
168
+ const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
172
169
  const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
173
170
  const expectedPid = options.expectedPid === undefined
174
171
  ? undefined
@@ -224,7 +221,7 @@ export async function stopFileTaskController(home, options = {}) {
224
221
  /** Restarts only the per-home Controller process; managed tmux sessions remain untouched. */
225
222
  export async function restartFileTaskController(home, options = {}) {
226
223
  const call = options.call ?? callController;
227
- const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
224
+ const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
228
225
  const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
229
226
  let current = null;
230
227
  let previousPid;
@@ -529,7 +526,10 @@ export class FileTaskWorkflowRuntime {
529
526
  && this.workspacePreparer !== undefined) {
530
527
  await this.workspacePreparer.prepareTaskWorkspace(taskId);
531
528
  }
532
- await callFileTaskController(this.home, "scheduler.scan", {}, this.clientOptions);
529
+ await callFileTaskController(this.home, "scheduler.scan", {}, {
530
+ ...this.clientOptions,
531
+ requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
532
+ });
533
533
  }
534
534
  }
535
535
  const MANAGED_RUNTIME_ENVIRONMENT = new Set(YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES);