@zq-silk/yui 0.14.0 → 0.14.2

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 (179) hide show
  1. package/ARCHITECTURE.md +64 -35
  2. package/README.md +258 -125
  3. package/dist/agent/argumentPolicy.js +2 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +5 -8
  5. package/dist/cli/commandCatalog.js +74 -109
  6. package/dist/cli/helpRenderer.js +1 -2
  7. package/dist/cli/interactionCandidates.js +10 -0
  8. package/dist/cli/interactionPolicy.js +76 -37
  9. package/dist/cli/roleWizard.js +5 -6
  10. package/dist/cli/updateOrchestrator.js +10 -8
  11. package/dist/cli/updatePorts.js +17 -13
  12. package/dist/cli/upgradeCommand.js +2 -5
  13. package/dist/cli.js +268 -231
  14. package/dist/commands/agentCommands.js +13 -0
  15. package/dist/commands/configCommands.js +7 -19
  16. package/dist/commands/configOverview.js +3 -2
  17. package/dist/commands/durableJobCommands.js +1 -1
  18. package/dist/commands/executionAuditCommands.js +4 -9
  19. package/dist/commands/globalRoleCommands.js +17 -0
  20. package/dist/commands/profileCommands.js +200 -30
  21. package/dist/commands/sessionCommands.js +1 -1
  22. package/dist/commands/taskActor.js +29 -127
  23. package/dist/commands/taskChangeSetCommands.js +1 -3
  24. package/dist/commands/taskCommands.js +1771 -2107
  25. package/dist/commands/taskCompletionGate.js +27 -103
  26. package/dist/commands/taskContextCommand.js +47 -80
  27. package/dist/commands/taskExecutionCommands.js +9 -27
  28. package/dist/commands/taskInputCommands.js +10 -10
  29. package/dist/commands/taskIntegrationCommands.js +69 -61
  30. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  31. package/dist/commands/taskNextActionCommand.js +8 -29
  32. package/dist/commands/taskOverviewCommand.js +25 -3
  33. package/dist/commands/taskPublicationCommands.js +151 -85
  34. package/dist/commands/taskPublicationVerifyCommand.js +181 -0
  35. package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
  36. package/dist/commands/taskRoleRuntimeStatus.js +5 -6
  37. package/dist/commands/taskUpstreamCommands.js +79 -95
  38. package/dist/config/configCatalog.js +1 -1
  39. package/dist/context/contextSnapshot.js +17 -2
  40. package/dist/context/sessionBootstrapManifest.js +18 -1
  41. package/dist/context/sourceTurnContext.js +30 -0
  42. package/dist/context/turnContextPack.js +107 -48
  43. package/dist/context/turnInputContract.js +11 -4
  44. package/dist/context/wakeNotification.js +84 -22
  45. package/dist/controller/agentRuntimeObserver.js +9 -12
  46. package/dist/controller/clientRuntime.js +13 -13
  47. package/dist/controller/controller.js +11 -11
  48. package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
  49. package/dist/controller/jobControl.js +11 -10
  50. package/dist/controller/resourceInventory.js +6 -6
  51. package/dist/controller/resourceInventoryLinux.js +2 -2
  52. package/dist/controller/runtime.js +18 -61
  53. package/dist/controller/runtimeEventInbox.js +57 -34
  54. package/dist/controller/runtimeEventProcessor.js +36 -96
  55. package/dist/controller/runtimeHookTurnFence.js +33 -57
  56. package/dist/controller/runtimeLaunchCoordinator.js +44 -40
  57. package/dist/controller/runtimeObservationHook.js +18 -12
  58. package/dist/controller/sessionNotify.js +43 -28
  59. package/dist/controller/sessionOwnerReconciliation.js +21 -21
  60. package/dist/controller/structuredProviderObservation.js +13 -16
  61. package/dist/coordination/workMailboxQueue.js +132 -0
  62. package/dist/core/controllerServer.js +1 -0
  63. package/dist/domain/agentResultTransport.js +60 -0
  64. package/dist/execution/executionHealth.js +111 -118
  65. package/dist/execution/reviewMainTurn.js +161 -0
  66. package/dist/execution/workItemExecution.js +454 -0
  67. package/dist/execution/workItemExecutionProjection.js +309 -0
  68. package/dist/execution/workItemMainTurn.js +164 -0
  69. package/dist/executor/agentExecutor.js +19 -19
  70. package/dist/executor/executorRegistry.js +7 -28
  71. package/dist/executor/fileRoleLaunchPlanner.js +51 -83
  72. package/dist/external/githubPublicationVerifier.js +114 -0
  73. package/dist/external/gitlabPublicationVerifier.js +158 -0
  74. package/dist/external/pinnedCommandRunner.js +73 -0
  75. package/dist/integration/changeSetManifest.js +0 -3
  76. package/dist/integration/deliveryObligation.js +43 -6
  77. package/dist/integration/gitIntegrationService.js +284 -101
  78. package/dist/integration/integrationAttempt.js +64 -14
  79. package/dist/integration/integrationQueueEntry.js +5 -35
  80. package/dist/integration/integrationQueueService.js +65 -194
  81. package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
  82. package/dist/lifecycle/exactTurnTerminalization.js +100 -97
  83. package/dist/message/message.js +16 -0
  84. package/dist/observability/executionAudit.js +4 -25
  85. package/dist/observability/faultClassification.js +58 -70
  86. package/dist/observability/orchestrationMetrics.js +8 -50
  87. package/dist/profile/agentProfile.js +60 -15
  88. package/dist/profile/agentProfileRuntime.js +100 -0
  89. package/dist/release/releaseWorkflowPorts.js +5 -68
  90. package/dist/repository/executionLaneGitSnapshot.js +80 -0
  91. package/dist/repository/gitWorkspace.js +9 -67
  92. package/dist/repository/taskBaseFreshness.js +3 -1
  93. package/dist/repository/taskWorkspaceCoordinator.js +32 -4
  94. package/dist/repository/taskWorkspacePreparer.js +252 -411
  95. package/dist/resources/liveReferences.js +6 -55
  96. package/dist/resources/resourceDiscovery.js +1 -30
  97. package/dist/resources/resourceRegistrar.js +0 -23
  98. package/dist/review/deltaRecheck.js +8 -65
  99. package/dist/review/reviewAcceptance.js +22 -19
  100. package/dist/review/reviewConfig.js +0 -10
  101. package/dist/review/reviewDecision.js +2 -2
  102. package/dist/review/reviewRound.js +147 -273
  103. package/dist/review/reviewerAvailability.js +13 -23
  104. package/dist/role/role.js +3 -0
  105. package/dist/runtime/agentDriverObservation.js +1 -1
  106. package/dist/runtime/agentHost.js +51 -51
  107. package/dist/runtime/builtinAgentDrivers.js +9 -2
  108. package/dist/runtime/codexAppServerRuntime.js +23 -5
  109. package/dist/runtime/exactControlPlane.js +2 -267
  110. package/dist/runtime/firstProgressAdvisory.js +1 -1
  111. package/dist/runtime/index.js +1 -1
  112. package/dist/runtime/launchBroker.js +11 -11
  113. package/dist/runtime/lifecycleReservation.js +2 -2
  114. package/dist/runtime/managedCaller.js +111 -0
  115. package/dist/runtime/ports.js +16 -3
  116. package/dist/runtime/preallocatedNativeSession.js +2 -2
  117. package/dist/runtime/processExitObservation.js +3 -2
  118. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  119. package/dist/runtime/runtimeBinding.js +1 -1
  120. package/dist/runtime/runtimeObservation.js +33 -16
  121. package/dist/runtime/runtimeSessionCandidate.js +2 -2
  122. package/dist/runtime/runtimeStopReceipt.js +11 -11
  123. package/dist/runtime/sessionLaunchRequest.js +2 -2
  124. package/dist/runtime/sessionOwnerIdentity.js +9 -9
  125. package/dist/runtime/sessionOwnerRegistry.js +13 -13
  126. package/dist/runtime/sessionReconciliation.js +4 -4
  127. package/dist/runtime/sessionTerminationGuard.js +9 -9
  128. package/dist/runtime/sessionTokenMetrics.js +5 -7
  129. package/dist/runtime/structuredProviderHost.js +5 -5
  130. package/dist/runtime/taskRuntimeIsolation.js +24 -24
  131. package/dist/runtime/tmuxAdapters.js +32 -30
  132. package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
  133. package/dist/scheduler/leaderWakeupProcessor.js +78 -55
  134. package/dist/scheduler/roleTurnLiveness.js +6 -6
  135. package/dist/scheduler/roleTurnStall.js +8 -9
  136. package/dist/scheduler/taskExecutionProjection.js +19 -41
  137. package/dist/scheduler/taskObservabilityProjection.js +75 -122
  138. package/dist/storage/sqliteSchema.js +1 -21
  139. package/dist/storage/sqliteStore.js +62 -47
  140. package/dist/storage/storageSchema.js +1 -8
  141. package/dist/storage/storageVersions.js +7 -4
  142. package/dist/storage/taskStore.js +8 -8
  143. package/dist/storage/upgrade/upgradeOrchestrator.js +112 -83
  144. package/dist/task/completionReadiness.js +17 -98
  145. package/dist/task/deliveryGuard.js +9 -11
  146. package/dist/task/draftPlan.js +142 -0
  147. package/dist/task/nextAction.js +179 -477
  148. package/dist/task/publicationVerification.js +1 -0
  149. package/dist/task/remoteDelivery.js +233 -0
  150. package/dist/task/task.js +108 -4
  151. package/dist/task/taskRecordReference.js +0 -1
  152. package/dist/turn/turn.js +109 -16
  153. package/dist/verification/verificationGateService.js +0 -26
  154. package/dist/web/assets/client/components.js +99 -121
  155. package/dist/web/assets/client/i18n.js +136 -58
  156. package/dist/web/assets/client/view.js +44 -0
  157. package/dist/web/assets/styles/cards.js +3 -15
  158. package/dist/web/assets/styles/widgets.js +4 -0
  159. package/dist/web/webSnapshot.js +29 -14
  160. package/dist/workItem/dependencyGate.js +34 -0
  161. package/dist/workItem/workItem.js +99 -266
  162. package/dist/workspace/workItemChangeSetManager.js +15 -59
  163. package/docs/task-local-identity.md +1 -1
  164. package/i18n/README.zh-CN.md +165 -59
  165. package/package.json +1 -1
  166. package/skills/yui-leader/SKILL.md +272 -782
  167. package/skills/yui-operator/SKILL.md +190 -365
  168. package/skills/yui-reviewer/SKILL.md +40 -33
  169. package/skills/yui-runtime/SKILL.md +27 -6
  170. package/skills/yui-worker/SKILL.md +19 -6
  171. package/dist/execution/candidateConvergence.js +0 -623
  172. package/dist/execution/executionGroup.js +0 -854
  173. package/dist/execution/resourceBroker.js +0 -426
  174. package/dist/executor/launchPlan.js +0 -16
  175. package/dist/review/reviewFinding.js +0 -240
  176. package/dist/review/reviewFindingLedger.js +0 -550
  177. package/dist/review/reviewOutcomeClassifier.js +0 -255
  178. package/dist/scheduler/resourceQueueProjection.js +0 -71
  179. package/dist/task/repairWave.js +0 -150
@@ -335,7 +335,7 @@ function calculateHealth(role, activeTurn, lastTurn, nativeSession, runtimeClean
335
335
  : { health: "idle", healthReason: "there is no active work or live tmux pane" };
336
336
  }
337
337
  function projectTaskRoleRuntime(run, session, tmux, events, _mailbox, store, taskId, roleName, now) {
338
- if (run === null || session?.launchId === undefined)
338
+ if (run === null || session?.runtimeGenerationId === undefined)
339
339
  return null;
340
340
  let driverId;
341
341
  try {
@@ -350,8 +350,7 @@ function projectTaskRoleRuntime(run, session, tmux, events, _mailbox, store, tas
350
350
  turnId: run.id,
351
351
  agentId: run.effective.agentId,
352
352
  driverId,
353
- launchId: session.launchId,
354
- sessionGenerationId: session.launchId,
353
+ runtimeGenerationId: session.runtimeGenerationId,
355
354
  nativeSessionId: session.nativeSessionId,
356
355
  nativeTurnId: runtimeNativeTurnId(events, {
357
356
  taskId: run.taskId,
@@ -359,7 +358,7 @@ function projectTaskRoleRuntime(run, session, tmux, events, _mailbox, store, tas
359
358
  turnId: run.id,
360
359
  agentId: run.effective.agentId,
361
360
  driverId,
362
- launchId: session.launchId,
361
+ runtimeGenerationId: session.runtimeGenerationId,
363
362
  nativeSessionId: session.nativeSessionId,
364
363
  receiptId: store.getTaskRoleSessionSet(taskId, roleName)?.providerBinding?.turn?.attemptId
365
364
  ?? formatTurnReceiptId(run.taskId, run.id)
@@ -369,7 +368,7 @@ function projectTaskRoleRuntime(run, session, tmux, events, _mailbox, store, tas
369
368
  };
370
369
  let projection = projectRuntimeTaskEvents(fence, run.createdAt, events);
371
370
  projection = projectRuntimeObservation(projection, createRuntimeObservation({
372
- schemaVersion: 2,
371
+ schemaVersion: 4,
373
372
  eventId: `runtime-host-${run.id}`,
374
373
  semanticKey: `runtime-host-${run.id}`,
375
374
  kind: "host.observed",
@@ -421,7 +420,7 @@ function runtimeNativeTurnId(events, expected) {
421
420
  && observation.fence.turnId === expected.turnId
422
421
  && observation.fence.agentId === expected.agentId
423
422
  && observation.fence.driverId === expected.driverId
424
- && observation.fence.launchId === expected.launchId
423
+ && observation.fence.runtimeGenerationId === expected.runtimeGenerationId
425
424
  && observation.fence.nativeSessionId === expected.nativeSessionId
426
425
  && observation.fence.receiptId === expected.receiptId
427
426
  && observation.fence.nativeTurnId !== undefined)
@@ -1,136 +1,120 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
- import { createTaskEvent } from "../event/taskEvent.js";
2
+ import { GitIntegrationService } from "../integration/gitIntegrationService.js";
3
+ import { createIntegrationAttempt } from "../integration/integrationAttempt.js";
3
4
  import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
4
- import { acquireProjectMaintenanceLock } from "../repository/projectMaintenanceLock.js";
5
+ import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
5
6
  import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
6
- export async function runTaskUpstreamCommand(args, store, options = {}) {
7
+ import { parseRepeatable } from "./taskIntegrationCommands.js";
8
+ import { taskLocalActor } from "./taskActor.js";
9
+ export async function runTaskUpstreamCommand(args, store, home, options = {}) {
7
10
  const [command, ...rest] = args;
8
- if (command === "integrate") {
9
- return integrateUpstream(rest, store, options);
10
- }
11
+ if (command === "integrate")
12
+ return integrateUpstream(rest, store, home, options);
11
13
  throw usageError(command === undefined
12
14
  ? "Task upstream command is required."
13
15
  : `Unknown command: task upstream ${command}`);
14
16
  }
15
- async function integrateUpstream(args, store, options) {
16
- const usage = "Task upstream integrate usage: yui task upstream integrate <task> [--latest] [--project <project>].";
17
- const taskId = args[0];
18
- if (taskId === undefined)
19
- throw usageError(usage);
20
- const flags = new Set(args.slice(1));
21
- if (flags.size !== args.length - 1) {
22
- throw usageError(usage);
17
+ async function integrateUpstream(args, store, home, options) {
18
+ const usage = "Task upstream integrate usage: yui task upstream integrate <task> (--latest|--project <project>) [--check <command> ...].";
19
+ let latest = false;
20
+ const normalized = [];
21
+ for (const arg of args) {
22
+ if (arg === "--latest") {
23
+ if (latest)
24
+ throw usageError("Option may only be specified once: --latest.", usage);
25
+ latest = true;
26
+ continue;
27
+ }
28
+ normalized.push(...(arg.startsWith("--project=")
29
+ ? ["--project", arg.slice("--project=".length)]
30
+ : [arg]));
23
31
  }
24
- const latest = flags.has("--latest");
25
- const projectFlag = args.find((arg) => arg.startsWith("--project="));
26
- const projectRef = projectFlag?.slice("--project=".length);
32
+ const parsed = parseRepeatable(normalized, new Set(["--check"]), new Set(["--project"]), usage);
33
+ if (parsed.positionals.length !== 1)
34
+ throw usageError(usage);
35
+ const taskId = parsed.positionals[0];
36
+ const projectRef = parsed.one.get("--project");
27
37
  if (!latest && projectRef === undefined) {
28
- throw usageError("Specify --latest to integrate the remote development head, or --project=<project> to target one Project.");
38
+ throw usageError("Specify --latest for every Task Project, or --project <project>.");
29
39
  }
30
40
  if (latest && projectRef !== undefined) {
31
41
  throw usageError("--latest and --project are mutually exclusive.");
32
42
  }
33
- for (const flag of flags) {
34
- if (flag !== "--latest" && !flag.startsWith("--project=")) {
35
- throw usageError(usage);
36
- }
37
- }
38
- const task = store.getTask(taskId);
43
+ let task = store.getTask(taskId);
39
44
  if (task === null)
40
45
  throw usageError(`Task not found: ${taskId}.`);
41
46
  if (task.status !== "active") {
42
47
  throw usageError(`Task must be active to integrate upstream: ${taskId}/${task.status}.`);
43
48
  }
44
- // RFC Phase 4: integrating upstream rewrites the Task worktree HEAD. Refuse
45
- // while the Leader has an active Turn so the merge never lands under a
46
- // running Session.
47
- const activeLeaderRun = store.getActiveTurn(taskId, "leader");
48
- if (activeLeaderRun !== null) {
49
- throw usageError(`Task has an active Leader Turn (${activeLeaderRun.id}); stop it before integrating upstream: ${taskId}.`);
50
- }
51
- const workspace = store.getTaskWorkspace(taskId);
52
- if (workspace === null) {
53
- throw usageError(`Task has no workspace: ${taskId}. Activate it first.`);
54
- }
49
+ taskLocalActor(store, options.environment, task.id);
55
50
  const git = options.git ?? new NodeGitWorkspace();
56
51
  const now = options.now ?? (() => new Date());
52
+ await new FileTaskWorkspacePreparer(home, store, git, now)
53
+ .prepareTaskWorkspace(task.id);
54
+ task = store.getTask(task.id);
55
+ if (task === null || task.status !== "active") {
56
+ throw usageError(`Task is no longer active: ${taskId}.`);
57
+ }
58
+ const workspace = store.getTaskWorkspace(task.id);
59
+ if (workspace === null || workspace.owner.type !== "task") {
60
+ throw usageError(`Task has no authoritative main clone: ${task.id}.`);
61
+ }
62
+ const service = new GitIntegrationService(store.rootDirectory(), store, git, now);
57
63
  const results = [];
58
64
  for (const binding of task.projectBindings) {
59
65
  if (projectRef !== undefined && binding.projectId !== projectRef)
60
66
  continue;
61
67
  const project = store.getProject(binding.projectId);
62
- if (project === null) {
68
+ if (project === null)
63
69
  throw usageError(`Project not found: ${binding.projectId}.`);
64
- }
65
70
  if (project.remoteUrl === undefined) {
66
71
  throw usageError(`Project has no remote URL: ${project.id}.`);
67
72
  }
73
+ if (binding.baseCommit === undefined || binding.currentCommit === undefined) {
74
+ throw usageError(`Task Project has no activated commit boundary: ${task.id}/${project.id}.`);
75
+ }
68
76
  const entry = workspaceProjectEntry(workspace, project.id);
69
77
  if (entry === undefined) {
70
78
  throw usageError(`Task workspace has no entry for Project: ${project.id}.`);
71
79
  }
72
- // Hold the per-Project maintenance fence while resolving the remote
73
- // baseline so a concurrent `project refresh` cannot interleave with the
74
- // fetch.
75
- const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
76
- let oldHead;
77
- let newHead;
78
- let upstreamCommit;
79
- try {
80
- // Resolve the exact upstream commit.
81
- const upstream = await git.resolveRemoteBaseline({
82
- repositoryPath: project.path,
83
- remoteUrl: project.remoteUrl,
84
- developmentRef: project.developmentBranch
85
- });
86
- upstreamCommit = upstream.commit;
87
- // Record the current HEAD as a backup before merging.
88
- const current = await git.inspect(entry.path, "HEAD");
89
- oldHead = current.baseCommit;
90
- try {
91
- // mergeWorktree always creates a merge commit (--no-ff); record the
92
- // actual resulting HEAD rather than assuming a fast-forward.
93
- await git.mergeWorktree({
94
- targetPath: entry.path,
95
- sourceRefs: [upstream.commit]
96
- });
97
- }
98
- catch (error) {
99
- // Restore the original HEAD on failure. mergeWorktree already aborts
100
- // a conflicted merge, so HEAD is usually already at oldHead; reset
101
- // only when the merge left HEAD moved or the tree dirty.
102
- let restoreNote;
103
- try {
104
- await git.resetWorktree({
105
- targetPath: entry.path,
106
- expectedHead: oldHead,
107
- restoreHead: oldHead
108
- });
109
- restoreNote = ` The workspace has been restored to ${oldHead}.`;
110
- }
111
- catch {
112
- restoreNote = ` The workspace may be left in a conflicted state; inspect ${entry.path} manually.`;
113
- }
114
- const message = error instanceof Error ? error.message : String(error);
115
- throw new Error(`Upstream integration failed for Project ${project.id}: ${message}.` + restoreNote);
116
- }
117
- const after = await git.inspect(entry.path, "HEAD");
118
- newHead = after.baseCommit;
119
- }
120
- finally {
121
- releaseMaintenance();
80
+ const head = (await git.inspect(entry.path, entry.branch)).baseCommit;
81
+ if (head !== binding.currentCommit) {
82
+ throw usageError(`Task Project current commit diverged from its main clone: ${task.id}/${project.id}.`);
122
83
  }
123
- // Record the integration event immediately so a later Project's failure
124
- // does not lose this Project's audit trail.
125
- const result = { projectId: project.id, oldHead, newHead, upstreamCommit };
126
- store.transaction((tx) => {
127
- tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, "task.upstream-integrated", result, now()));
84
+ const remote = await git.resolveRemoteHead({
85
+ remoteUrl: project.remoteUrl,
86
+ branch: binding.baseRef
128
87
  });
88
+ const attempt = store.transaction((tx) => {
89
+ const created = createIntegrationAttempt({
90
+ id: tx.nextIntegrationAttemptId(task.id),
91
+ taskId: task.id,
92
+ projectId: project.id,
93
+ targetRef: entry.branch,
94
+ beforeCommit: binding.currentCommit,
95
+ source: {
96
+ kind: "upstream",
97
+ branch: remote.branch,
98
+ remoteCommit: remote.commit,
99
+ taskBaseCommit: binding.baseCommit,
100
+ strategy: "rebase"
101
+ },
102
+ checkCommands: parsed.many.get("--check") ?? []
103
+ }, now());
104
+ tx.saveIntegrationAttempt(task.id, created);
105
+ return created;
106
+ });
107
+ const result = await service.integrate(task.id, attempt.id);
129
108
  results.push(result);
109
+ if (result.status !== "committed")
110
+ break;
111
+ }
112
+ if (projectRef !== undefined && results.length === 0) {
113
+ throw usageError(`Task Project not found: ${task.id}/${projectRef}.`);
130
114
  }
131
- const lines = results.map((r) => ` ${r.projectId}: ${r.oldHead.slice(0, 12)} -> ${r.newHead.slice(0, 12)} (upstream: ${r.upstreamCommit.slice(0, 12)})`);
115
+ const lines = results.map(({ attempt, status }) => (` ${attempt.projectId}: ${attempt.beforeCommit.slice(0, 12)} -> ${(attempt.afterCommit ?? attempt.candidateCommit ?? attempt.beforeCommit).slice(0, 12)} (${status})`));
132
116
  return {
133
- output: `Integrated upstream for Task ${taskId}:\n${lines.join("\n")}\n`,
134
- data: { taskId, integrations: results }
117
+ output: `Upstream Integration results for Task ${task.id}:\n${lines.join("\n")}\n`,
118
+ data: { taskId: task.id, integrations: results }
135
119
  };
136
120
  }
@@ -16,7 +16,7 @@ export const CONFIG_DEFINITIONS = Object.freeze([
16
16
  { key: "delivery-timeout-seconds", domain: "runtime", property: "deliveryTimeoutSeconds", label: "Delivery timeout", summary: "Total control-plane delivery retry budget, 5-600 seconds (default: 120).", takesEffect: "After the Controller restarts; internal retry cadence remains automatic." },
17
17
  { key: "leader-next-action", domain: "workflow", property: "leaderNextActionMode", label: "Leader next-action mode", summary: "Leader next-action mode: display, warn, or enforce (default: display).", takesEffect: "The next Leader next-action projection or gate." },
18
18
  { key: "leader-semantic-budget-turns", domain: "workflow", property: "leaderSemanticBudgetTurns", label: "Leader semantic budget", summary: "Consecutive completed Leader turns without durable delivery progress before warning, 1-20 (default: 3).", takesEffect: "The next Leader delivery guard evaluation." },
19
- { key: "review", domain: "workflow", property: "review", label: "Review", summary: "Optional global WorkItem review rule: a configured Role plus always, leader, or final trigger and optional finding-ledger behavior. Setup leaves it disabled so the Leader may review directly or delegate selectively.", takesEffect: "The next Candidate snapshot; in-flight Candidates and ReviewRounds keep their policy." },
19
+ { key: "review", domain: "workflow", property: "review", label: "Review", summary: "Optional global WorkItem review rule: a configured Role plus always, leader, or final trigger. Setup leaves it disabled so the Leader may review directly or delegate selectively.", takesEffect: "The next Candidate snapshot; in-flight Candidates and ReviewRounds keep their policy." },
20
20
  { key: "resources-gc-mode", domain: "resources", property: "resourcesGcMode", label: "Resources GC mode", summary: "Resource GC mode: report or quarantine (default: report).", takesEffect: "The next resource GC operation." },
21
21
  { key: "resources-gc-auto-quarantine", domain: "resources", property: "resourcesGcAutoQuarantine", label: "Resources GC auto-quarantine", summary: "Automatically quarantine eligible terminal Task resources (default: false).", takesEffect: "The next eligible automatic resource GC operation." },
22
22
  { key: "resources-quarantine-ttl-hours", domain: "resources", property: "resourcesQuarantineTtlHours", label: "Resource quarantine TTL", summary: "Default observation window before purge, 1-720 hours (default: 24).", takesEffect: "The next resource GC command without an explicit TTL override." },
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { normalizedUniqueText, optionalText, requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
3
+ import { MAX_CONTEXT_SOURCE_TURN_BYTES, MAX_CONTEXT_SOURCE_TURNS } from "./sourceTurnContext.js";
3
4
  export const CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
4
5
  export const CONTEXT_SNAPSHOT_MAX_RESOURCES = 256;
5
6
  export const CONTEXT_SNAPSHOT_MAX_RESOURCE_BYTES = 4 * 1024 * 1024;
@@ -184,7 +185,16 @@ function validateContextRef(value) {
184
185
  function normalizeSnapshotResources(values, refs) {
185
186
  if (!Array.isArray(values))
186
187
  throw new Error("Context Snapshot resources must be an array.");
187
- if (values.length > CONTEXT_SNAPSHOT_MAX_RESOURCES) {
188
+ const sourceTurnCount = values.filter((resource) => (resource !== null
189
+ && typeof resource === "object"
190
+ && !Array.isArray(resource)
191
+ && resource.ref !== null
192
+ && typeof resource.ref === "object"
193
+ && resource.ref.store === "source-turn")).length;
194
+ if (sourceTurnCount > MAX_CONTEXT_SOURCE_TURNS) {
195
+ throw new Error(`Context Snapshot exceeds ${MAX_CONTEXT_SOURCE_TURNS} source Turns.`);
196
+ }
197
+ if (values.length - sourceTurnCount > CONTEXT_SNAPSHOT_MAX_RESOURCES) {
188
198
  throw new Error(`Context Snapshot exceeds ${CONTEXT_SNAPSHOT_MAX_RESOURCES} resources.`);
189
199
  }
190
200
  const byRef = new Map(refs.map((ref) => [contextRefKey(ref), ref]));
@@ -210,9 +220,14 @@ function normalizeSnapshotResources(values, refs) {
210
220
  || new Set(resources.map(({ ref }) => contextRefKey(ref))).size !== refs.length) {
211
221
  throw new Error("Context Snapshot resources must cover every ref exactly once.");
212
222
  }
213
- if (Buffer.byteLength(JSON.stringify(resources), "utf8") > CONTEXT_SNAPSHOT_MAX_BYTES) {
223
+ const ordinary = resources.filter(({ ref }) => ref.store !== "source-turn");
224
+ const sourceTurns = resources.filter(({ ref }) => ref.store === "source-turn");
225
+ if (Buffer.byteLength(JSON.stringify(ordinary), "utf8") > CONTEXT_SNAPSHOT_MAX_BYTES) {
214
226
  throw new Error(`Context Snapshot resources exceed ${CONTEXT_SNAPSHOT_MAX_BYTES} bytes.`);
215
227
  }
228
+ if (Buffer.byteLength(JSON.stringify(sourceTurns), "utf8") > MAX_CONTEXT_SOURCE_TURN_BYTES) {
229
+ throw new Error(`Context Snapshot source Turns exceed ${MAX_CONTEXT_SOURCE_TURN_BYTES} bytes.`);
230
+ }
216
231
  return Object.freeze(resources);
217
232
  }
218
233
  function contextRefKey(ref) {
@@ -130,7 +130,10 @@ export function materializeSessionBootstrap(input) {
130
130
  roleProfileRef: { digest: profileDigest, path: roleProfilePath },
131
131
  contextProtocol: input.owner.scope === "global"
132
132
  ? {
133
- loadCommand: "\"$YUI_SESSION_CLI\" session context \"$YUI_ROLE\" --json"
133
+ // A Global Session may move between Yui's remote TUI and Desktop.
134
+ // Carry its read entry in the Thread-visible Manifest instead of
135
+ // depending on the client process that happened to create it.
136
+ loadCommand: renderGlobalContextCommand(input.controlPlane, home, input.role.name)
134
137
  }
135
138
  : {
136
139
  loadCommand: "\"$YUI_SESSION_CLI\" task turn context \"$YUI_TASK_ID/<turn-id>\" --json",
@@ -148,6 +151,20 @@ export function materializeSessionBootstrap(input) {
148
151
  descriptorPath
149
152
  });
150
153
  }
154
+ function renderGlobalContextCommand(controlPlane, home, roleName) {
155
+ return [
156
+ `YUI_HOME=${quoteShellWord(home)}`,
157
+ quoteShellWord(controlPlane.executable),
158
+ quoteShellWord(controlPlane.cliEntry),
159
+ "session",
160
+ "context",
161
+ quoteShellWord(roleName),
162
+ "--json"
163
+ ].join(" ");
164
+ }
165
+ function quoteShellWord(value) {
166
+ return `'${value.replaceAll("'", "'\"'\"'")}'`;
167
+ }
151
168
  /**
152
169
  * Retargets known managed wrappers to the current resolved control plane after
153
170
  * a compatible update. Only a valid Session Manifest may nominate a wrapper,
@@ -0,0 +1,30 @@
1
+ import { MAX_TURN_RESULT_OUTPUT_BYTES } from "../domain/agentResultTransport.js";
2
+ export const MAX_SYNTHESIS_SOURCE_TURNS = 8;
3
+ export const MAX_CONTEXT_SOURCE_TURNS = MAX_SYNTHESIS_SOURCE_TURNS + 1;
4
+ export const MAX_CONTEXT_SOURCE_TURN_BYTES = MAX_CONTEXT_SOURCE_TURNS * (MAX_TURN_RESULT_OUTPUT_BYTES + 16 * 1024);
5
+ /**
6
+ * Frozen input for synthesis. The main Agent needs the producer identity and
7
+ * exact result, not another copy of its prompt history, launch configuration,
8
+ * or workspace descriptor.
9
+ */
10
+ export function sourceTurnContextValue(turn) {
11
+ if (turn.result === undefined) {
12
+ throw new Error(`Source Turn has no result: ${turn.id}.`);
13
+ }
14
+ return Object.freeze({
15
+ schemaVersion: 1,
16
+ id: turn.id,
17
+ taskId: turn.taskId,
18
+ roleName: turn.roleName,
19
+ purpose: turn.purpose,
20
+ ...(turn.workItemId === undefined ? {} : { workItemId: turn.workItemId }),
21
+ ...(turn.reviewRoundId === undefined ? {} : { reviewRoundId: turn.reviewRoundId }),
22
+ ...(turn.executionGroupId === undefined
23
+ ? {}
24
+ : { executionGroupId: turn.executionGroupId }),
25
+ ...(turn.executionLaneId === undefined ? {} : { executionLaneId: turn.executionLaneId }),
26
+ result: turn.result,
27
+ createdAt: turn.createdAt,
28
+ updatedAt: turn.updatedAt
29
+ });
30
+ }
@@ -1,7 +1,10 @@
1
1
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
2
2
  import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
3
3
  import { TURN_INPUT_MAX_DELTAS } from "./turnInputContract.js";
4
+ import { assertWorkItemDependenciesCompleted } from "../workItem/dependencyGate.js";
5
+ import { workItemExecutionGroupById } from "../workItem/workItem.js";
4
6
  import { contextContentDigest, contextSnapshotRef, createContextSnapshot, validateContextSnapshot } from "./contextSnapshot.js";
7
+ import { sourceTurnContextValue } from "./sourceTurnContext.js";
5
8
  export const TURN_CONTEXT_PACK_SCHEMA_VERSION = 1;
6
9
  export const TURN_CONTEXT_PACK_MAX_REFS = 256;
7
10
  export const TURN_CONTEXT_PACK_MAX_BYTES = 8 * 1024 * 1024;
@@ -20,7 +23,10 @@ export function freezeTurnContextSnapshot(store, run, now, frozenBy = "controlle
20
23
  throw new Error(`Turn Context baseline is missing or drifted: ${baselineRef.id}.`);
21
24
  }
22
25
  validateContextSnapshot(baseline);
23
- const overlays = collectTurnContextOverlays(store, run);
26
+ const overlays = [
27
+ ...collectTurnContextOverlays(store, run),
28
+ ...collectSourceTurnContext(store, run)
29
+ ];
24
30
  const resources = [...new Map([...baseline.resources, ...overlays].map((entry) => [
25
31
  contextRefIdentity(entry.ref),
26
32
  entry
@@ -76,11 +82,11 @@ export function freezeTurnContextSnapshot(store, run, now, frozenBy = "controlle
76
82
  }
77
83
  /**
78
84
  * Freeze the shared, role-neutral ContextSnapshot anchored by one WorkItem
79
- * exploration stage Group. Turn snapshots remain role-specific; this
80
- * record is the durable stage baseline and derives from a fresh WorkItem
81
- * snapshot so the Group never depends on ambient latest state.
85
+ * ExecutionAssignment. Turn snapshots remain role-specific; this record is
86
+ * the durable Assignment baseline and freezes the current WorkItem facts so
87
+ * the Group never depends on ambient latest state.
82
88
  */
83
- export function freezeExecutionStageContextSnapshot(store, input, now) {
89
+ export function freezeWorkItemExecutionAssignmentContextSnapshot(store, input, now) {
84
90
  const task = store.getTask(input.taskId);
85
91
  if (task === null)
86
92
  throw new Error(`Task not found: ${input.taskId}.`);
@@ -91,11 +97,11 @@ export function freezeExecutionStageContextSnapshot(store, input, now) {
91
97
  materialize("L2", "task", task.id, task),
92
98
  materialize("L3", "work-item", workItem.id, workItem)
93
99
  ];
100
+ assertWorkItemDependenciesCompleted(store, workItem);
94
101
  for (const dependencyId of workItem.dependsOn) {
95
102
  const dependency = store.getWorkItem(task.id, dependencyId);
96
- if (dependency === null || dependency.status !== "completed") {
97
- throw new Error(`Exploration stage dependency is not accepted: ${dependencyId}.`);
98
- }
103
+ if (dependency === null)
104
+ throw new Error(`WorkItem dependency disappeared: ${dependencyId}.`);
99
105
  materialized.push(materialize("L3", "accepted-work-item", dependency.id, dependency));
100
106
  }
101
107
  for (const binding of task.projectBindings) {
@@ -112,42 +118,20 @@ export function freezeExecutionStageContextSnapshot(store, input, now) {
112
118
  contextRefIdentity(entry.ref),
113
119
  entry
114
120
  ])).values()].sort((left, right) => (contextRefIdentity(left.ref).localeCompare(contextRefIdentity(right.ref))));
115
- const previousWorkItem = store.listContextSnapshots(task.id)
116
- .filter((candidate) => candidate.scope === "workitem"
117
- && candidate.scopeRef === workItem.id)
118
- .sort((left, right) => left.sequence - right.sequence)
119
- .at(-1);
120
- const workItemSnapshot = createContextSnapshot({
121
- id: store.nextContextSnapshotId(task.id),
122
- taskId: task.id,
123
- scope: "workitem",
124
- scopeRef: workItem.id,
125
- sequence: (previousWorkItem?.sequence ?? 0) + 1,
126
- refs: resources.map(({ ref }) => ref),
127
- resources,
128
- acceptRefs: [`work-item:${workItem.id}:acceptance`],
129
- ...(previousWorkItem === undefined
130
- ? {}
131
- : { parentRef: contextSnapshotRef(previousWorkItem) }),
132
- frozenAt: now,
133
- frozenBy: "controller"
134
- });
135
- store.saveContextSnapshot(workItemSnapshot);
136
- const stageSnapshot = createContextSnapshot({
121
+ const assignmentSnapshot = createContextSnapshot({
137
122
  id: store.nextContextSnapshotId(task.id),
138
123
  taskId: task.id,
139
124
  scope: "stage",
140
125
  scopeRef: input.executionGroupId,
141
- sequence: workItemSnapshot.sequence + 1,
126
+ sequence: 1,
142
127
  refs: resources.map(({ ref }) => ref),
143
128
  resources,
144
129
  acceptRefs: [`work-item:${workItem.id}:acceptance`],
145
- parentRef: contextSnapshotRef(workItemSnapshot),
146
130
  frozenAt: now,
147
131
  frozenBy: "controller"
148
132
  });
149
- store.saveContextSnapshot(stageSnapshot);
150
- return stageSnapshot;
133
+ store.saveContextSnapshot(assignmentSnapshot);
134
+ return assignmentSnapshot;
151
135
  }
152
136
  /**
153
137
  * Freeze the role-neutral baseline for one Reviewer ExecutionGroup before
@@ -282,10 +266,35 @@ export function buildTurnContextPack(store, taskId, turnId) {
282
266
  returnedBytes: preliminaryBytes,
283
267
  truncated: false
284
268
  }),
285
- digest
269
+ digest,
270
+ liveTaskState: readLiveTaskState(store, taskId)
286
271
  });
287
272
  return pack;
288
273
  }
274
+ /** Reads the Task's current in-flight execution, outside the frozen contract. */
275
+ function readLiveTaskState(store, taskId) {
276
+ const activeTurns = store.listTurns(taskId)
277
+ .filter((turn) => turn.status === "active")
278
+ .map((turn) => Object.freeze({
279
+ turnId: turn.id,
280
+ roleName: turn.roleName,
281
+ purpose: turn.purpose,
282
+ ...(turn.workItemId === undefined ? {} : { workItemId: turn.workItemId }),
283
+ ...(turn.reviewRoundId === undefined ? {} : { reviewRoundId: turn.reviewRoundId })
284
+ }));
285
+ const activeTaskReviews = store.listReviewRounds(taskId)
286
+ .filter((round) => ((round.scope ?? "work-item") === "task"
287
+ && (round.status === "pending" || round.status === "running")))
288
+ .map((round) => Object.freeze({
289
+ reviewRoundId: round.id,
290
+ reviewerRoleName: round.reviewerRoleName,
291
+ status: round.status
292
+ }));
293
+ return Object.freeze({
294
+ activeTurns: Object.freeze(activeTurns),
295
+ activeTaskReviews: Object.freeze(activeTaskReviews)
296
+ });
297
+ }
289
298
  export function expandTurnContextRef(store, taskId, turnId, refId, refStore) {
290
299
  const pack = buildTurnContextPack(store, taskId, turnId);
291
300
  const authorized = pack.pointers.filter((ref) => (ref.refId === refId && (refStore === undefined || ref.store === refStore)));
@@ -352,20 +361,18 @@ function collectAuthorizedContext(store, run) {
352
361
  result.push(materialize("L2", "task-brief", task.id, brief));
353
362
  }
354
363
  result.push(...collectTurnContextOverlays(store, run));
364
+ result.push(...collectSourceTurnContext(store, run));
355
365
  if (run.workItemId !== undefined) {
356
366
  const item = store.getWorkItem(task.id, run.workItemId);
357
367
  if (item === null)
358
368
  throw new Error(`Turn WorkItem not found: ${run.workItemId}.`);
359
369
  result.push(materialize("L3", "work-item", item.id, item));
360
370
  if (view === "worker") {
371
+ assertWorkItemDependenciesCompleted(store, item);
361
372
  for (const dependencyId of item.dependsOn) {
362
373
  const dependency = store.getWorkItem(task.id, dependencyId);
363
- if (dependency === null
364
- || (dependency.status !== "completed" && dependency.status !== "retired")) {
365
- throw new Error(`Turn WorkItem dependency is not accepted: ${dependencyId}.`);
366
- }
367
- if (dependency.status === "retired")
368
- continue;
374
+ if (dependency === null)
375
+ throw new Error(`WorkItem dependency disappeared: ${dependencyId}.`);
369
376
  result.push(materialize("L3", "accepted-work-item", dependency.id, dependency));
370
377
  }
371
378
  }
@@ -375,10 +382,21 @@ function collectAuthorizedContext(store, run) {
375
382
  if (round === null)
376
383
  throw new Error(`Turn ReviewRound not found: ${run.reviewRoundId}.`);
377
384
  result.push(materialize("L3", "review-round", round.id, round));
378
- for (const finding of store.listReviewFindings(task.id).filter((candidate) => (candidate.firstReviewRoundId === round.id
379
- || candidate.lastReviewRoundId === round.id
380
- || candidate.repair?.workItemId === round.workItemId))) {
381
- result.push(materialize("L3", "review-finding", finding.id, finding));
385
+ if (round.deltaRecheck !== undefined) {
386
+ const previous = store.getReviewRound(task.id, round.deltaRecheck.previousReviewRoundId);
387
+ const previousTurn = previous?.reviewerTurnId === undefined
388
+ ? null
389
+ : store.getTurn(task.id, previous.reviewerTurnId);
390
+ if (previous === null
391
+ || previous === undefined
392
+ || previous.status !== "completed"
393
+ || previousTurn === null
394
+ || previousTurn.status !== "completed"
395
+ || previousTurn.result === undefined) {
396
+ throw new Error(`Delta recheck source Review result is unavailable: ${round.deltaRecheck.previousReviewRoundId}.`);
397
+ }
398
+ result.push(materialize("L3", "review-round", previous.id, previous));
399
+ result.push(materialize("L4", "source-turn", previousTurn.id, sourceTurnContextValue(previousTurn)));
382
400
  }
383
401
  }
384
402
  for (const binding of task.projectBindings) {
@@ -405,9 +423,6 @@ function collectAuthorizedContext(store, run) {
405
423
  for (const round of store.listReviewRounds(task.id).slice(-16)) {
406
424
  result.push(materialize("L3", "review-round", round.id, round));
407
425
  }
408
- for (const finding of store.listReviewFindings(task.id)) {
409
- result.push(materialize("L3", "review-finding", finding.id, finding));
410
- }
411
426
  for (const turn of operationalTaskRecords(store.listTurns(task.id), events, "turn").slice(-24)) {
412
427
  result.push(materialize("L4", "turn", turn.id, turn));
413
428
  }
@@ -454,6 +469,50 @@ function collectTurnContextOverlays(store, run) {
454
469
  : [])
455
470
  ];
456
471
  }
472
+ function collectSourceTurnContext(store, run) {
473
+ if (run.sourceExecutionGroupId === undefined)
474
+ return [];
475
+ const group = run.purpose === "execution"
476
+ ? (() => {
477
+ if (run.workItemId === undefined) {
478
+ throw new Error("Execution synthesis source requires a WorkItem.");
479
+ }
480
+ const item = store.getWorkItem(run.taskId, run.workItemId);
481
+ if (item === null)
482
+ throw new Error(`Turn WorkItem not found: ${run.workItemId}.`);
483
+ return workItemExecutionGroupById(item, run.sourceExecutionGroupId);
484
+ })()
485
+ : (() => {
486
+ if (run.reviewRoundId === undefined) {
487
+ throw new Error("Review synthesis source requires a ReviewRound.");
488
+ }
489
+ const round = store.getReviewRound(run.taskId, run.reviewRoundId);
490
+ if (round === null)
491
+ throw new Error(`Turn ReviewRound not found: ${run.reviewRoundId}.`);
492
+ return round.executionGroup?.id === run.sourceExecutionGroupId
493
+ ? round.executionGroup
494
+ : undefined;
495
+ })();
496
+ if (group === undefined) {
497
+ throw new Error(`Source ExecutionGroup not found: ${run.sourceExecutionGroupId}.`);
498
+ }
499
+ return [...group.lanes]
500
+ .sort((left, right) => left.ordinal - right.ordinal || left.id.localeCompare(right.id))
501
+ .flatMap((lane) => {
502
+ if (lane.disposition !== "succeeded" || lane.successfulTurnId === undefined)
503
+ return [];
504
+ const source = store.getTurn(run.taskId, lane.successfulTurnId);
505
+ if (source === null
506
+ || source.status !== "completed"
507
+ || source.result === undefined
508
+ || source.executionGroupId !== group.id
509
+ || source.executionLaneId !== lane.id
510
+ || source.roleName !== lane.roleName) {
511
+ throw new Error(`Successful source Turn is missing or drifted: ${group.id}/${lane.id}.`);
512
+ }
513
+ return [materialize("L4", "source-turn", source.id, sourceTurnContextValue(source))];
514
+ });
515
+ }
457
516
  function materialize(layer, store, refId, value) {
458
517
  const digest = contextContentDigest(value);
459
518
  const record = value;