@zq-silk/yui 0.8.3 → 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 (60) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +46 -16
  3. package/dist/cli/commandCatalog.js +23 -11
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli.js +154 -16
  6. package/dist/commands/executionAuditCommands.js +30 -0
  7. package/dist/commands/operatorCommands.js +42 -1
  8. package/dist/commands/taskCommands.js +386 -138
  9. package/dist/commands/taskCompletionGate.js +36 -24
  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/sessionBootstrapManifest.js +82 -1
  14. package/dist/controller/clientRuntime.js +7 -7
  15. package/dist/controller/controller.js +16 -8
  16. package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
  17. package/dist/controller/handoverCandidate.js +10 -3
  18. package/dist/controller/sessionNotify.js +4 -22
  19. package/dist/executor/agentAdapter.js +2 -2
  20. package/dist/executor/agentExecutor.js +25 -5
  21. package/dist/executor/fileRoleLaunchPlanner.js +16 -11
  22. package/dist/integration/gitIntegrationService.js +50 -2
  23. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  24. package/dist/observability/executionAudit.js +47 -1
  25. package/dist/observability/faultClassification.js +6 -4
  26. package/dist/observability/orchestrationMetrics.js +196 -0
  27. package/dist/operator/operatorSessionHistory.js +36 -0
  28. package/dist/release/releaseHandover.js +7 -5
  29. package/dist/release/runtimeRelease.js +15 -0
  30. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  31. package/dist/review/deltaRecheck.js +3 -2
  32. package/dist/review/reviewFindingLedger.js +5 -4
  33. package/dist/review/reviewOutcomeClassifier.js +252 -54
  34. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  35. package/dist/review/taskFinalReviewContractRebind.js +350 -0
  36. package/dist/run/runIdentity.js +10 -70
  37. package/dist/runtime/agentHost.js +3 -4
  38. package/dist/runtime/codexAppServerRuntime.js +6 -0
  39. package/dist/runtime/firstProgressStopLoss.js +52 -0
  40. package/dist/runtime/launchBroker.js +10 -2
  41. package/dist/runtime/runtimeDeadlines.js +14 -0
  42. package/dist/runtime/sessionTitle.js +24 -12
  43. package/dist/runtime/structuredProviderHost.js +7 -1
  44. package/dist/runtime/tmuxAdapters.js +10 -3
  45. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  46. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  47. package/dist/scheduler/wakeReason.js +1 -0
  48. package/dist/storage/sqliteStore.js +8 -1
  49. package/dist/storage/taskStore.js +10 -1
  50. package/dist/task/completionReadiness.js +48 -19
  51. package/dist/task/deliveryGuard.js +3 -1
  52. package/dist/task/nextAction.js +145 -52
  53. package/dist/task/repairWave.js +14 -1
  54. package/dist/task/task.js +10 -0
  55. package/dist/web/webSnapshot.js +7 -1
  56. package/i18n/README.zh-CN.md +28 -8
  57. package/package.json +1 -1
  58. package/skills/yui-leader/SKILL.md +73 -31
  59. package/skills/yui-operator/SKILL.md +51 -10
  60. package/skills/yui-reviewer/SKILL.md +23 -0
@@ -1,8 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
- import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
3
+ import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleProviderRuntimeForCleanup, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
4
4
  import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, markProviderTurnDeliveryUnknown, rejectProviderTurn, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
5
5
  import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
6
+ import { boundProviderRetryBeforeFirstProgress, projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
6
7
  import { hasRecentTurnId } from "../executor/turnCompletion.js";
7
8
  import { createTaskEvent } from "../event/taskEvent.js";
8
9
  import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
@@ -674,6 +675,9 @@ export class FileSchedulerStoreAdapter {
674
675
  : sessions?.sessions[agentId];
675
676
  return session === undefined ? null : mapSession(session);
676
677
  }
678
+ getTaskRoleSessionSet(taskId, roleName) {
679
+ return this.store.getTaskRoleSessionSet(taskId, roleName);
680
+ }
677
681
  listEvents(taskId) {
678
682
  return this.#taskReadProjection(taskId).events;
679
683
  }
@@ -1222,7 +1226,14 @@ export class FileSchedulerStoreAdapter {
1222
1226
  startedAt: now.toISOString()
1223
1227
  }), batchId);
1224
1228
  }
1225
- markRuntimeOwnerSessionStopped(store, runtimeOwnerFromTarget(target), now);
1229
+ const owner = runtimeOwnerFromTarget(target);
1230
+ markRuntimeOwnerSessionStopped(store, owner, now);
1231
+ if (owner.scope === "task") {
1232
+ const sessions = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
1233
+ if (sessions !== null && sessions.providerBinding !== null) {
1234
+ store.saveTaskRoleSessionSet(clearTaskRoleProviderRuntimeForCleanup(sessions, now));
1235
+ }
1236
+ }
1226
1237
  saveRuntimeLifecycleMailbox(store, mailbox);
1227
1238
  return true;
1228
1239
  });
@@ -1288,6 +1299,40 @@ export class FileSchedulerStoreAdapter {
1288
1299
  clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
1289
1300
  getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
1290
1301
  getOperatorNotification(taskId) { return this.store.getOperatorNotification(taskId); }
1302
+ saveLeaderFirstProgressStopLoss(input) {
1303
+ return this.store.transaction((store) => {
1304
+ const task = store.getTask(input.taskId);
1305
+ const role = store.getRole(input.taskId, input.roleName);
1306
+ if (task === null || task.status !== "active" || role === null
1307
+ || store.getLeaderFailure(input.taskId) !== null) {
1308
+ return "state-changed";
1309
+ }
1310
+ const stopLoss = projectFirstProgressStopLoss({
1311
+ sessions: store.getTaskRoleSessionSet(input.taskId, input.roleName),
1312
+ events: store.listEvents(input.taskId),
1313
+ workItems: store.listWorkItems(input.taskId),
1314
+ reviewRounds: store.listReviewRounds(input.taskId),
1315
+ integrations: store.listIntegrationAttempts(input.taskId)
1316
+ });
1317
+ if (!stopLoss.exhausted || stopLoss.fingerprint !== input.expectedFingerprint) {
1318
+ return "state-changed";
1319
+ }
1320
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
1321
+ const lastSession = sessions === null
1322
+ ? undefined
1323
+ : [...(sessions.history ?? []), ...Object.values(sessions.sessions)]
1324
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt))
1325
+ .at(-1);
1326
+ const message = `Leader first-progress stop-loss: ${stopLoss.reason}`;
1327
+ store.saveRole(input.taskId, updateRoleStatus(role, "failed", input.now));
1328
+ store.saveLeaderFailure(recordLeaderFailure(input.taskId, lastSession?.nativeSessionId ?? "(unregistered)", message, input.now, null));
1329
+ store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, message, input.now, store.getOperatorNotification(input.taskId)));
1330
+ enqueueWork(store, { kind: "operator" }, "leader-first-progress-stop-loss", input.now, [
1331
+ { type: "task", id: input.taskId }
1332
+ ]);
1333
+ return "recorded";
1334
+ });
1335
+ }
1291
1336
  saveLeaderDispatch(input) {
1292
1337
  return this.store.transaction((store) => {
1293
1338
  const task = store.getTask(input.task.id);
@@ -2241,7 +2286,7 @@ export class FileSchedulerStoreAdapter {
2241
2286
  failedNativeTurnId: input.nativeTurnId,
2242
2287
  lastErrorSummary: summary,
2243
2288
  ...(input.retryAfterMs === undefined ? {} : { retryAfterMs: input.retryAfterMs })
2244
- }, now, configuredProviderRetryPolicy(store));
2289
+ }, now, configuredProviderRetryPolicy(store, input.taskId, input.roleName));
2245
2290
  if (retryDecision.outcome === "exhausted") {
2246
2291
  this.recordProviderRetryClassified(store, input, classification.errorClass, {
2247
2292
  wouldRetry: "false",
@@ -2253,7 +2298,7 @@ export class FileSchedulerStoreAdapter {
2253
2298
  if (run.providerRetry === undefined)
2254
2299
  return null;
2255
2300
  const finalized = finalizeProviderRetryDeadline(store, run, retryDecision.reason === "attempts"
2256
- ? `Provider retry failed after all ${config.delaysMs.length} in-Session continuation attempts.`
2301
+ ? `Provider retry failed after all ${run.providerRetry.maxRetries} in-Session continuation attempts.`
2257
2302
  : retryDecision.reason === "retry-after-window"
2258
2303
  ? `Provider Retry-After exceeded the bounded ${config.maxWindowMs / 1_000}-second episode window.`
2259
2304
  : `Provider retry did not recover within the bounded ${config.maxWindowMs / 1_000}-second episode window.`, retryDecision.reason === "attempts" ? "attempts-exhausted" : "episode-window-exhausted", now);
@@ -3616,6 +3661,7 @@ function mapSession(session) {
3616
3661
  adapterId: session.adapterId,
3617
3662
  nativeSessionId: session.nativeSessionId,
3618
3663
  ...(session.launchId === undefined ? {} : { launchId: session.launchId }),
3664
+ ...(session.title === undefined ? {} : { title: session.title }),
3619
3665
  status: session.status,
3620
3666
  effective: session.effective,
3621
3667
  updatedAt: session.updatedAt
@@ -3629,6 +3675,7 @@ function saveTaskSession(store, role, session, status, now, launchId) {
3629
3675
  adapterId: session.adapterId,
3630
3676
  nativeSessionId: session.nativeSessionId,
3631
3677
  ...(launchId === undefined ? {} : { launchId }),
3678
+ ...(session.title === undefined ? {} : { title: session.title }),
3632
3679
  policy: "fixed",
3633
3680
  status,
3634
3681
  effective: session.effective
@@ -4021,7 +4068,19 @@ function compareCanonicalObservationOrder(left, right) {
4021
4068
  || (left.ordinal ?? -1) - (right.ordinal ?? -1)
4022
4069
  || left.eventId.localeCompare(right.eventId);
4023
4070
  }
4024
- function configuredProviderRetryPolicy(store) {
4071
+ function configuredProviderRetryPolicy(store, taskId, roleName) {
4025
4072
  const config = providerRetryConfig(store.getConfig());
4073
+ if (taskId !== undefined && roleName === "leader") {
4074
+ const progress = projectFirstProgressStopLoss({
4075
+ sessions: store.getTaskRoleSessionSet(taskId, roleName),
4076
+ events: store.listEvents(taskId),
4077
+ workItems: store.listWorkItems(taskId),
4078
+ reviewRounds: store.listReviewRounds(taskId),
4079
+ integrations: store.listIntegrationAttempts(taskId)
4080
+ });
4081
+ if (progress.firstProgressAt === undefined) {
4082
+ return boundProviderRetryBeforeFirstProgress(config, progress);
4083
+ }
4084
+ }
4026
4085
  return { delaysMs: config.delaysMs, maxWindowMs: config.maxWindowMs };
4027
4086
  }
@@ -19,6 +19,7 @@ import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
19
19
  import { detectRunningRelease, isOwnerLive, readActiveReleasePointer, readHandoverFence, removeCandidateDiscovery, removeHandoverFence, writeCandidateDiscovery, writeHandoverFence, writeHandoverReceipt, writeRuntimeIdentity } from "../release/runtimeRelease.js";
20
20
  import { startFileTaskControllerRuntime } from "./runtime.js";
21
21
  import { readLinuxProcessStartIdentity } from "./domainIdentity.js";
22
+ import { RELEASE_HANDOVER_OLD_OWNER_GRACE_MS } from "../runtime/runtimeDeadlines.js";
22
23
  export const CONTROLLER_CANDIDATE_ENV = "YUI_CONTROLLER_CANDIDATE";
23
24
  export const CONTROLLER_HANDOVER_ID_ENV = "YUI_CONTROLLER_HANDOVER_ID";
24
25
  const DEFAULT_POLL_INTERVAL_MS = 100;
@@ -29,7 +30,7 @@ const DEFAULT_POLL_INTERVAL_MS = 100;
29
30
  * a second independent grace (see `DEFAULT_DUAL_OWNER_GRACE_MS` in
30
31
  * `releaseHandover.ts`, which is only an optional confirmation debounce).
31
32
  */
32
- export const DEFAULT_DUAL_OWNER_GRACE_MS = 30_000;
33
+ export const DEFAULT_DUAL_OWNER_GRACE_MS = RELEASE_HANDOVER_OLD_OWNER_GRACE_MS;
33
34
  /** Reads the candidate configuration from the process environment. */
34
35
  export function handoverCandidateFromEnvironment(environment) {
35
36
  if (environment[CONTROLLER_CANDIDATE_ENV] !== "1")
@@ -85,7 +86,6 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
85
86
  dualOwner: false
86
87
  }));
87
88
  advanceFence(home, fence, "candidate-ready", now().toISOString());
88
- const committedAt = Date.now();
89
89
  let dualOwnerReported = false;
90
90
  for (;;) {
91
91
  const current = readHandoverFence(home);
@@ -111,7 +111,7 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
111
111
  }
112
112
  if (current.phase === "committed"
113
113
  && !oldDead
114
- && Date.now() - committedAt > dualOwnerGraceMs
114
+ && committedFenceAgeMs(current, now()) > dualOwnerGraceMs
115
115
  && !dualOwnerReported) {
116
116
  // The old Controller was told to exit but is still live. Stay read-only
117
117
  // and make the dual-owner condition visible; never write concurrently.
@@ -129,6 +129,13 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
129
129
  await delay(pollIntervalMs);
130
130
  }
131
131
  }
132
+ function committedFenceAgeMs(fence, observedAt) {
133
+ const committedAt = Date.parse(fence.updatedAt);
134
+ if (!Number.isFinite(committedAt)) {
135
+ throw new Error("Committed handover fence timestamp is invalid.");
136
+ }
137
+ return observedAt.getTime() - committedAt;
138
+ }
132
139
  async function promote(home, fence, environment, now) {
133
140
  removeCandidateDiscovery(home);
134
141
  const controller = await startFileTaskControllerRuntime(home, { environment });
@@ -1,6 +1,5 @@
1
1
  import { callController } from "../core/controllerClient.js";
2
2
  import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
3
- import { yuiRunBodyFromInputMessage, yuiRunIdFromInputMessages } from "../run/runIdentity.js";
4
3
  import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
5
4
  import { setCodexThreadName } from "../execution/codexThreadNaming.js";
6
5
  import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
@@ -50,9 +49,11 @@ export function parseCodexSessionNotification(payloadArgument, environment) {
50
49
  const nativeSessionId = requireText(payload["thread-id"], "Codex thread-id");
51
50
  const turnId = requireText(payload["turn-id"], "Codex turn-id");
52
51
  const lastAssistantMessage = requireAssistantMessage(payload["last-assistant-message"]);
53
- const runId = yuiRunIdFromInputMessages(payload["input-messages"]);
52
+ const runId = environment.YUI_RUN_ID === undefined
53
+ ? undefined
54
+ : requireText(environment.YUI_RUN_ID, "YUI_RUN_ID");
54
55
  const title = environment.YUI_SESSION_TITLE === undefined
55
- ? sessionTitleFromInputMessages(payload["input-messages"])
56
+ ? undefined
56
57
  : requireText(environment.YUI_SESSION_TITLE, "YUI_SESSION_TITLE");
57
58
  const scope = environment.YUI_SESSION_SCOPE;
58
59
  if (scope !== "task" && scope !== "global") {
@@ -117,19 +118,6 @@ function requireAssistantMessage(value) {
117
118
  }
118
119
  return text;
119
120
  }
120
- function sessionTitleFromInputMessages(value) {
121
- if (!Array.isArray(value))
122
- return undefined;
123
- for (const entry of value) {
124
- if (typeof entry !== "string")
125
- continue;
126
- const body = yuiRunBodyFromInputMessage(entry);
127
- const normalized = body.trim().replaceAll(/\s+/g, " ");
128
- if (normalized.length > 0)
129
- return truncateSessionText(normalized);
130
- }
131
- return undefined;
132
- }
133
121
  function shouldSetThreadName(home, params) {
134
122
  if (params.scope !== "task"
135
123
  || params.title === undefined
@@ -168,12 +156,6 @@ function threadNameRequest(params, environment) {
168
156
  return null;
169
157
  }
170
158
  }
171
- function truncateSessionText(value) {
172
- const truncated = value.slice(0, 1_024);
173
- return /[\uD800-\uDBFF]$/.test(truncated)
174
- ? truncated.slice(0, -1)
175
- : truncated;
176
- }
177
159
  function isObject(value) {
178
160
  return typeof value === "object" && value !== null && !Array.isArray(value);
179
161
  }
@@ -276,7 +276,7 @@ class ClaudeAdapter extends BaseAdapter {
276
276
  return ["--append-system-prompt-file", input.managedContextFile];
277
277
  }
278
278
  compileResume(input) {
279
- const launch = super.compileNew(input);
279
+ const launch = this.compileNew(input);
280
280
  return { ...launch, argv: [...launch.argv, "--resume", nativeId(input.nativeSessionId)] };
281
281
  }
282
282
  compileManagedControl(input, mode, nativeSessionId) {
@@ -285,7 +285,7 @@ class ClaudeAdapter extends BaseAdapter {
285
285
  }
286
286
  const sessionId = nativeId(nativeSessionId);
287
287
  const launch = mode === "new"
288
- ? super.compileNew(input)
288
+ ? this.compileNew(input)
289
289
  : this.compileResume({ ...input, nativeSessionId: sessionId });
290
290
  return {
291
291
  ...launch,
@@ -173,9 +173,29 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
173
173
  // receive a fresh identity after the Role workspace changes.
174
174
  history: [...(set.history ?? []), ...Object.values(set.sessions)],
175
175
  sessions: {},
176
+ providerBinding: null,
176
177
  updatedAt: timestamp
177
178
  });
178
179
  }
180
+ /**
181
+ * Clears the Provider transport identity after its physical runtime is proven
182
+ * stopped, without retiring workspace-bound Session records. Workspace
183
+ * retirement remains a separate, stricter transaction after every supported
184
+ * placeholder has been terminalized.
185
+ */
186
+ export function clearTaskRoleProviderRuntimeForCleanup(set, now) {
187
+ validateRoleSessionSet(set);
188
+ if (set.inFlight !== null) {
189
+ throw new Error("Cannot clear a Task Role Provider runtime with unsettled Run state.");
190
+ }
191
+ if (set.providerBinding === null)
192
+ return set;
193
+ return validateRoleSessionSet({
194
+ ...set,
195
+ providerBinding: null,
196
+ updatedAt: requireDate(now, "Provider Runtime cleanup timestamp")
197
+ });
198
+ }
179
199
  /**
180
200
  * Terminalizes only the aggregate-16 Claude placeholder shape after the
181
201
  * caller has fenced the Task store and proved that the exact Role has no live
@@ -261,16 +281,16 @@ export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
261
281
  }
262
282
  return "new";
263
283
  }
284
+ if (session.status === "stopped" || session.status === "broken") {
285
+ return "new";
286
+ }
264
287
  const compatible = set.owner.scope === "task"
265
288
  ? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, desired, workspace)
266
289
  : effectiveLaunchSnapshotsCompatible(session.effective, desired);
267
290
  if (compatible)
268
291
  return "resume";
269
- if (session.status !== "stopped" && session.status !== "broken") {
270
- throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
271
- + "Stop the existing native process before starting a fresh Session.");
272
- }
273
- return "new";
292
+ throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
293
+ + "Stop the existing native process before starting a fresh Session.");
274
294
  }
275
295
  export function bindTaskRoleRun(set, fence, preparedAt) {
276
296
  validateRoleSessionSet(set);
@@ -11,9 +11,10 @@ import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessi
11
11
  import { materializeSessionBootstrap } from "../context/sessionBootstrapManifest.js";
12
12
  import { serializeRunBootstrapEnvelope, serializeRunHostRecoveryEnvelope } from "../context/runContextContract.js";
13
13
  import { serializeProviderRetryEnvelope } from "../run/providerRetry.js";
14
+ import { prefixYuiTitleInput } from "../run/runIdentity.js";
14
15
  import { resolveAgentAdapter } from "./agentAdapter.js";
15
16
  import { inspectCodexLaunchConfig } from "./codexConfigConflict.js";
16
- import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
17
+ import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
17
18
  import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
18
19
  import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
19
20
  import { activeLiveRoleAgentSession } from "./agentExecutor.js";
@@ -224,7 +225,7 @@ export class FileRoleLaunchPlanner {
224
225
  if (input.mode === "resume" && !compatibleExisting) {
225
226
  throw new Error(`Task Role resume effective snapshot drifted: ${task.id}/${role.name}.`);
226
227
  }
227
- return this.#compile(role, input, { scope: "task", taskId: task.id }, taskRoleSessionTitle(task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
228
+ return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
228
229
  purpose: activeRun?.purpose ?? "execution"
229
230
  });
230
231
  }
@@ -506,12 +507,13 @@ export class FileRoleLaunchPlanner {
506
507
  ...(providerNativeSessionId === undefined
507
508
  ? {}
508
509
  : { nativeSessionId: providerNativeSessionId }),
510
+ ...(sessionTitle === undefined ? {} : { sessionTitle }),
509
511
  authority: providerAuthority,
510
512
  ...(carriesInitialTurn
511
513
  ? {
512
514
  initialTurn: {
513
515
  attemptId: formatAgentRunReceiptId(owner.taskId, input.runId),
514
- boundedText: managedRunLaunchEnvelope(managedRun, input.mode)
516
+ boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionTitle)
515
517
  }
516
518
  }
517
519
  : {})
@@ -578,6 +580,7 @@ export class FileRoleLaunchPlanner {
578
580
  },
579
581
  launch: scopedLaunch,
580
582
  session,
583
+ ...(sessionTitle === undefined ? {} : { sessionTitle }),
581
584
  ...(carriesInitialTurn && input.runId !== undefined
582
585
  ? { initialTurnRunId: input.runId }
583
586
  : {})
@@ -645,18 +648,20 @@ export class FileRoleLaunchPlanner {
645
648
  return selectEnvironment(source, names);
646
649
  }
647
650
  }
648
- function managedRunLaunchEnvelope(run, mode) {
649
- if (run.providerRetry?.state === "dispatching") {
650
- return serializeProviderRetryEnvelope({
651
+ function managedRunLaunchEnvelope(run, mode, title) {
652
+ const body = run.providerRetry?.state === "dispatching"
653
+ ? serializeProviderRetryEnvelope({
651
654
  taskId: run.taskId,
652
655
  runId: run.id,
653
656
  roleName: run.roleName,
654
657
  retry: run.providerRetry
655
- });
656
- }
657
- return mode === "resume" && run.pushedAt !== undefined
658
- ? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
659
- : serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
658
+ })
659
+ : mode === "resume" && run.pushedAt !== undefined
660
+ ? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
661
+ : serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
662
+ return title === undefined
663
+ ? body
664
+ : prefixYuiTitleInput(body, title);
660
665
  }
661
666
  export function nativeAgentWorkspace(workspace) {
662
667
  return workspace.entries.length === 1
@@ -1,5 +1,6 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
+ import { existsSync } from "node:fs";
3
4
  import { lstat, mkdir, open, rm, stat } from "node:fs/promises";
4
5
  import { dirname, join, relative, resolve, sep } from "node:path";
5
6
  import { promisify } from "node:util";
@@ -18,6 +19,8 @@ import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
18
19
  import { recordIntegrationCheckJob, requireLeaderDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
19
20
  import { createManagedWorkspace } from "../worktree/managedWorkspace.js";
20
21
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
22
+ import { readRuntimeIdentity } from "../release/runtimeRelease.js";
23
+ import { findReusableIntegrationCheckEvidence, INTEGRATION_RUNTIME_RELEASE_ENV } from "./integrationCheckEvidenceReuse.js";
21
24
  const executeFile = promisify(execFile);
22
25
  const INTEGRATION_OPERATIONAL_ENVIRONMENT_NAMES = [
23
26
  "PATH",
@@ -300,8 +303,6 @@ export class GitIntegrationService {
300
303
  */
301
304
  async #startCheckJob(attempt, workspace, path, managedWorkspace, repositoryPath, gate) {
302
305
  const runtime = this.#runtimePreparation(attempt, managedWorkspace);
303
- this.runtimeIsolation.activate(runtime);
304
- const environment = await integrationCheckEnvironment(this.environment, runtime);
305
306
  const head = await gitLine(["-C", path, "rev-parse", "HEAD^{commit}"]);
306
307
  const steps = gate === undefined
307
308
  ? attempt.checkCommands.map((command, index) => ({
@@ -319,6 +320,30 @@ export class GitIntegrationService {
319
320
  timeoutMs: 30 * 60_000
320
321
  }))
321
322
  ];
323
+ const releaseId = integrationRuntimeReleaseIdentity(this.home);
324
+ if (gate === undefined && releaseId !== null) {
325
+ const reusable = findReusableIntegrationCheckEvidence({
326
+ taskId: attempt.taskId,
327
+ projectId: attempt.projectId,
328
+ currentAttemptId: attempt.id,
329
+ candidateCommit: head,
330
+ checkCommands: attempt.checkCommands,
331
+ runtimeReleaseId: releaseId,
332
+ attempts: this.store.listIntegrationAttempts(attempt.taskId),
333
+ jobs: this.store.listDurableJobs(attempt.taskId),
334
+ logExists: (homeRelativePath) => existsSync(join(this.home, homeRelativePath)),
335
+ logPathFor: (job, relativeLogPath) => (join(job.artifactsLocator, "logs", relativeLogPath))
336
+ });
337
+ if (reusable !== null) {
338
+ return this.#finalizeGateSuccess(attempt, workspace, repositoryPath, head, [...reusable.checks]);
339
+ }
340
+ }
341
+ this.runtimeIsolation.activate(runtime);
342
+ const baseEnvironment = await integrationCheckEnvironment(this.environment, runtime);
343
+ const environment = Object.freeze({
344
+ ...baseEnvironment,
345
+ ...(releaseId === null ? {} : { [INTEGRATION_RUNTIME_RELEASE_ENV]: releaseId })
346
+ });
322
347
  // Persist the gate identity before starting the job so a plan edit
323
348
  // during the gate never misattributes the evidence on resume.
324
349
  let persisted = attempt;
@@ -906,6 +931,29 @@ function integrationRuntimeLaunchId(home, integrationId) {
906
931
  .digest("hex");
907
932
  return `${integrationId}-${homeDigest}`;
908
933
  }
934
+ /**
935
+ * Reuse is enabled only for an immutable installed release. Development
936
+ * checkouts have no content-addressed release identity, so they rerun.
937
+ */
938
+ function integrationRuntimeReleaseIdentity(home) {
939
+ try {
940
+ const receipt = readRuntimeIdentity(home);
941
+ const release = receipt?.activeRelease;
942
+ if (receipt === null
943
+ || receipt.mode !== "primary"
944
+ || receipt.dualOwner
945
+ || release === null
946
+ || release === undefined
947
+ || receipt.packageDigest !== release.packageDigest
948
+ || receipt.buildId !== release.buildId) {
949
+ return null;
950
+ }
951
+ return release.releaseId;
952
+ }
953
+ catch {
954
+ return null;
955
+ }
956
+ }
909
957
  function checkFailureReason(completion) {
910
958
  if (completion.timedOut)
911
959
  return "Command timed out after 1800 seconds.";
@@ -0,0 +1,53 @@
1
+ /** Internal, Controller-derived release fence carried by Integration jobs. */
2
+ export const INTEGRATION_RUNTIME_RELEASE_ENV = "YUI_INTEGRATION_RUNTIME_RELEASE_ID";
3
+ /**
4
+ * Find exact reusable evidence without creating cache state. Every identity
5
+ * field is read from the existing IntegrationAttempt and DurableJob records.
6
+ */
7
+ export function findReusableIntegrationCheckEvidence(input) {
8
+ const attempts = [...input.attempts]
9
+ .filter((attempt) => (attempt.taskId === input.taskId
10
+ && attempt.id !== input.currentAttemptId
11
+ && attempt.projectId === input.projectId
12
+ && sameOrderedText(attempt.checkCommands, input.checkCommands)))
13
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
14
+ for (const attempt of attempts) {
15
+ if (attempt.jobId === undefined)
16
+ continue;
17
+ const job = input.jobs.find((candidate) => (candidate.id === attempt.jobId
18
+ && candidate.taskId === input.taskId
19
+ && candidate.owner.kind === "integration-attempt"
20
+ && candidate.owner.integrationAttemptId === attempt.id));
21
+ const resultSteps = job?.result?.steps ?? [];
22
+ const logPaths = resultSteps.map((step) => (input.logPathFor?.(job, step.logPath) ?? step.logPath));
23
+ if (job === undefined
24
+ || job.projectId !== input.projectId
25
+ || job.head !== input.candidateCommit
26
+ || job.status !== "succeeded"
27
+ || job.result?.outcome !== "succeeded"
28
+ || job.env[INTEGRATION_RUNTIME_RELEASE_ENV] !== input.runtimeReleaseId
29
+ || job.steps.length !== input.checkCommands.length
30
+ || job.steps.some((step, index) => (step.name !== `check-${index + 1}`
31
+ || step.command !== input.checkCommands[index]))
32
+ || resultSteps.length !== input.checkCommands.length
33
+ || resultSteps.some((step, index) => (step.name !== `check-${index + 1}`
34
+ || step.exitCode !== 0
35
+ || step.signal !== null
36
+ || step.timedOut
37
+ || step.head !== input.candidateCommit
38
+ || (input.logExists !== undefined && !input.logExists(logPaths[index]))))) {
39
+ continue;
40
+ }
41
+ const checks = resultSteps.map((step, index) => ({
42
+ name: input.checkCommands[index],
43
+ outcome: "passed",
44
+ details: `Reused successful check evidence from ${attempt.id}/${job.id}.`,
45
+ logPath: logPaths[index]
46
+ }));
47
+ return Object.freeze({ sourceAttempt: attempt, sourceJob: job, checks });
48
+ }
49
+ return null;
50
+ }
51
+ function sameOrderedText(left, right) {
52
+ return left.length === right.length && left.every((value, index) => value === right[index]);
53
+ }
@@ -18,6 +18,7 @@ import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.j
18
18
  import { classifyAgentRunFailure, classifyIntegrationAttempt, classifyReviewRound, classifyWakeReasons, countFaultClasses } from "./faultClassification.js";
19
19
  import { RUNTIME_LAUNCH_KINDS, RUNTIME_LAUNCH_PHASES } from "../runtime/launchDiagnostics.js";
20
20
  import { UNSUPPORTED } from "./runtimeIdentity.js";
21
+ import { projectTaskOrchestration } from "./orchestrationMetrics.js";
21
22
  export function createProductionExecutionAuditPorts() {
22
23
  return {
23
24
  openStore: (home) => openCompatibleFileTaskStore(home),
@@ -206,6 +207,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
206
207
  events: section,
207
208
  providerRetries: section,
208
209
  workItems: section,
210
+ orchestration: section,
209
211
  storage: section,
210
212
  runtimeProtocol: section,
211
213
  topLongRunning: section
@@ -443,7 +445,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
443
445
  else if (round.deltaRecheck.disposition === "requires-full-review")
444
446
  deltaEscalated += 1;
445
447
  }
446
- const classification = classifyReviewRound(round);
448
+ const classification = classifyReviewRound(round, store);
447
449
  if (classification.faultClass === "review-infra")
448
450
  infraFailed += 1;
449
451
  else if (classification.faultClass === "review-semantic-negative") {
@@ -674,6 +676,37 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
674
676
  return failed(error);
675
677
  }
676
678
  })();
679
+ const orchestration = (() => {
680
+ try {
681
+ const metrics = taskIds.flatMap((taskId) => {
682
+ const task = store.getTask(taskId);
683
+ if (task === null)
684
+ return [];
685
+ return [projectTaskOrchestration({
686
+ task,
687
+ runs: withinWindow(store.listAgentRuns(taskId), options),
688
+ roleSessionSets: sessionSetsWithinWindow(store.listRoleSessionSets(taskId), options),
689
+ workItems: withinWindow(store.listWorkItems(taskId), options),
690
+ changeSets: withinWindow(store.listChangeSets(taskId), options),
691
+ reviewRounds: withinWindow(store.listReviewRounds(taskId), options),
692
+ reviewFindings: withinWindow(store.listReviewFindings(taskId), options),
693
+ integrations: withinWindow(store.listIntegrationAttempts(taskId), options),
694
+ durableJobs: withinWindow(store.listDurableJobs(taskId), options),
695
+ publications: withinWindow(store.listPublicationReferences(taskId), options),
696
+ decisions: withinWindow(store.listDecisions(taskId), options),
697
+ events: withinWindow(store.listEvents(taskId), options),
698
+ managedWorkspaces: withinWindow(store.listManagedWorkspaces(taskId), options)
699
+ })];
700
+ });
701
+ return ok({
702
+ tasks: metrics,
703
+ advisoryCount: metrics.reduce((total, task) => total + task.advisories.length, 0)
704
+ });
705
+ }
706
+ catch (error) {
707
+ return failed(error);
708
+ }
709
+ })();
677
710
  const storage = (() => {
678
711
  try {
679
712
  let stateJsonBytes = UNSUPPORTED;
@@ -834,8 +867,21 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
834
867
  events,
835
868
  providerRetries,
836
869
  workItems,
870
+ orchestration,
837
871
  storage,
838
872
  runtimeProtocol,
839
873
  topLongRunning
840
874
  };
841
875
  }
876
+ function withinWindow(records, options) {
877
+ return records.filter((record) => inWindow(record.createdAt, options));
878
+ }
879
+ function sessionSetsWithinWindow(sets, options) {
880
+ if (options.since === undefined && options.until === undefined)
881
+ return sets;
882
+ return sets.map((set) => ({
883
+ ...set,
884
+ sessions: Object.fromEntries(Object.entries(set.sessions).filter(([, session]) => (inWindow(session.createdAt, options)))),
885
+ history: (set.history ?? []).filter((session) => inWindow(session.createdAt, options))
886
+ }));
887
+ }
@@ -1,3 +1,4 @@
1
+ import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
1
2
  export const FAULT_CLASSES = [
2
3
  "provider-transient",
3
4
  "policy-denied",
@@ -73,15 +74,16 @@ export function classifyAgentRunFailure(run, structured) {
73
74
  * Review execution failure (the Round itself failed to execute/deliver) is
74
75
  * `review-infra`; a completed Round with failed checks is a semantic negative.
75
76
  */
76
- export function classifyReviewRound(round) {
77
- if (round.status === "failed") {
77
+ export function classifyReviewRound(round, evidence) {
78
+ const outcome = classifyReviewRoundOutcome(round, evidence);
79
+ if (outcome?.kind === "non-semantic") {
78
80
  return {
79
81
  faultClass: "review-infra",
80
82
  basis: "structured",
81
- evidence: "round status=failed"
83
+ evidence: outcome.reason
82
84
  };
83
85
  }
84
- if (round.status === "completed" && (round.checks ?? []).some((c) => c.outcome === "failed")) {
86
+ if (outcome?.kind === "semantic" && (round.checks ?? []).some((c) => c.outcome === "failed")) {
85
87
  const failed = (round.checks ?? [])
86
88
  .filter((c) => c.outcome === "failed")
87
89
  .map((c) => c.name)