@zq-silk/yui 0.8.3 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +66 -19
  3. package/dist/cli/commandCatalog.js +43 -14
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli/updatePorts.js +6 -0
  6. package/dist/cli.js +252 -37
  7. package/dist/commands/executionAuditCommands.js +30 -0
  8. package/dist/commands/globalRoleCommands.js +8 -4
  9. package/dist/commands/operatorCommands.js +42 -1
  10. package/dist/commands/taskCommands.js +527 -147
  11. package/dist/commands/taskCompletionGate.js +36 -24
  12. package/dist/commands/taskContextCommand.js +11 -4
  13. package/dist/commands/taskInputCommands.js +48 -10
  14. package/dist/commands/taskNextActionCommand.js +38 -3
  15. package/dist/commands/taskOverviewCommand.js +2 -1
  16. package/dist/commands/taskRoleRuntimeStatus.js +2 -1
  17. package/dist/context/runContextPack.js +9 -5
  18. package/dist/context/sessionBootstrapManifest.js +158 -11
  19. package/dist/context/wakeNotification.js +5 -3
  20. package/dist/controller/clientRuntime.js +15 -15
  21. package/dist/controller/controller.js +16 -8
  22. package/dist/controller/fileSchedulerStoreAdapter.js +67 -7
  23. package/dist/controller/handoverCandidate.js +10 -3
  24. package/dist/controller/sessionNotify.js +4 -22
  25. package/dist/executor/agentAdapter.js +2 -2
  26. package/dist/executor/agentExecutor.js +29 -9
  27. package/dist/executor/fileRoleLaunchPlanner.js +37 -45
  28. package/dist/integration/gitIntegrationService.js +50 -2
  29. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  30. package/dist/observability/executionAudit.js +47 -1
  31. package/dist/observability/faultClassification.js +6 -4
  32. package/dist/observability/orchestrationMetrics.js +196 -0
  33. package/dist/operator/operatorSessionHistory.js +36 -0
  34. package/dist/release/releaseHandover.js +7 -5
  35. package/dist/release/runtimeRelease.js +15 -0
  36. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  37. package/dist/review/deltaRecheck.js +3 -2
  38. package/dist/review/reviewFindingLedger.js +5 -4
  39. package/dist/review/reviewOutcomeClassifier.js +263 -54
  40. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  41. package/dist/review/taskFinalReviewContractRebind.js +367 -0
  42. package/dist/run/runIdentity.js +10 -70
  43. package/dist/runtime/agentHost.js +3 -4
  44. package/dist/runtime/codexAppServerRuntime.js +6 -0
  45. package/dist/runtime/exactControlPlane.js +47 -37
  46. package/dist/runtime/firstProgressStopLoss.js +54 -0
  47. package/dist/runtime/launchBroker.js +10 -2
  48. package/dist/runtime/runtimeDeadlines.js +14 -0
  49. package/dist/runtime/sessionTitle.js +24 -12
  50. package/dist/runtime/structuredProviderHost.js +7 -1
  51. package/dist/runtime/tmuxAdapters.js +10 -3
  52. package/dist/scheduler/actionability.js +4 -2
  53. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  54. package/dist/scheduler/activeTaskProgress.js +2 -1
  55. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  56. package/dist/scheduler/taskExecutionProjection.js +13 -4
  57. package/dist/scheduler/wakeReason.js +1 -0
  58. package/dist/storage/sqliteStore.js +18 -3
  59. package/dist/storage/taskStore.js +14 -3
  60. package/dist/task/completionReadiness.js +48 -19
  61. package/dist/task/deliveryGuard.js +3 -1
  62. package/dist/task/nextAction.js +153 -55
  63. package/dist/task/repairWave.js +14 -1
  64. package/dist/task/task.js +10 -0
  65. package/dist/task/taskRecordRetirement.js +72 -0
  66. package/dist/web/webSnapshot.js +7 -1
  67. package/dist/workItem/workItem.js +6 -4
  68. package/i18n/README.zh-CN.md +48 -9
  69. package/package.json +1 -1
  70. package/skills/yui-leader/SKILL.md +73 -31
  71. package/skills/yui-operator/SKILL.md +58 -10
  72. package/skills/yui-reviewer/SKILL.md +23 -0
  73. package/skills/yui-runtime/SKILL.md +6 -6
@@ -11,15 +11,16 @@ 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";
20
21
  import { effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
21
- import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor, exactControlPlaneCommandPrefix, exactControlPlaneDigest, exactTaskRuntimeDescriptorPath, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
22
- import { readActiveReleasePointer } from "../release/runtimeRelease.js";
22
+ import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor, exactControlPlaneDigest, exactTaskRuntimeDescriptorPath, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
23
+ import { detectRunningRelease } from "../release/runtimeRelease.js";
23
24
  import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
24
25
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
25
26
  import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
@@ -52,21 +53,19 @@ export class FileRoleLaunchPlanner {
52
53
  this.#createNativeSessionId = options.createNativeSessionId ?? randomUUID;
53
54
  this.#cliPath = canonicalPath(options.cliPath
54
55
  ?? fileURLToPath(new URL("../cli.js", import.meta.url)));
55
- // Freeze the complete control identity before any native process exists.
56
- // This value is immutable for the planner/Controller lifetime and is never
57
- // rewritten through a shared PATH launcher.
58
- // Issue 02: bind the active release build ID so a Session created after a
59
- // handover cannot mutate a different control plane.
60
- const activeRelease = readActiveReleasePointer(this.home);
56
+ // Internal callbacks retain one exact command identity for receipt fencing.
57
+ // Interactive Role commands use ordinary `yui`; their continuity is the
58
+ // Session Manifest plus protocol/storage and durable runtime identity.
59
+ const runningRelease = detectRunningRelease(this.#cliPath);
61
60
  this.#controlPlane = createExactControlPlaneDescriptor({
62
61
  executable: process.execPath,
63
62
  cliEntry: this.#cliPath,
64
63
  yuiHome: this.home,
65
- ...(activeRelease === null
64
+ ...(runningRelease === null
66
65
  ? {}
67
66
  : {
68
- buildId: activeRelease.buildId,
69
- activeReleaseDigest: activeRelease.packageDigest
67
+ buildId: runningRelease.manifest.buildId,
68
+ activeReleaseDigest: runningRelease.manifest.packageDigest
70
69
  })
71
70
  });
72
71
  }
@@ -224,7 +223,7 @@ export class FileRoleLaunchPlanner {
224
223
  if (input.mode === "resume" && !compatibleExisting) {
225
224
  throw new Error(`Task Role resume effective snapshot drifted: ${task.id}/${role.name}.`);
226
225
  }
227
- return this.#compile(role, input, { scope: "task", taskId: task.id }, taskRoleSessionTitle(task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
226
+ 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
227
  purpose: activeRun?.purpose ?? "execution"
229
228
  });
230
229
  }
@@ -246,7 +245,7 @@ export class FileRoleLaunchPlanner {
246
245
  if (input.mode === "resume" && !compatibleExisting) {
247
246
  throw new Error(`Global Role resume effective snapshot drifted: ${role.name}.`);
248
247
  }
249
- return this.#compile(role, input, { scope: "global" }, undefined, compatibleExisting ? existing.nativeSessionId : undefined, undefined, effective, { purpose: "execution" });
248
+ return this.#compile(role, input, { scope: "global" }, undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, undefined, effective, { purpose: "execution" });
250
249
  }
251
250
  #compile(role, input, owner, sessionTitle, knownNativeSessionId, workspaceOverride, effective, sessionPolicy) {
252
251
  const launchRole = effectiveRoleForLaunch(role, effective);
@@ -346,7 +345,7 @@ export class FileRoleLaunchPlanner {
346
345
  const roleConfig = binding.config.adapterId === "claude"
347
346
  && owner.scope === "task"
348
347
  && input.runId !== undefined
349
- ? managedClaudeControlPlaneConfig(binding.config, owner.taskId, managedRun?.workItemId, input.runId, this.#controlPlane, sessionContext.sessionCliPath)
348
+ ? managedClaudeControlPlaneConfig(binding.config, owner.taskId, managedRun?.workItemId, input.runId)
350
349
  : binding.config;
351
350
  const effectiveConfig = withNativeProjectDirectories(roleConfig, nativeAdditionalDirectories(effective.workspace, agentWorkspace));
352
351
  const compileInput = {
@@ -382,15 +381,14 @@ export class FileRoleLaunchPlanner {
382
381
  CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "codex_exec"
383
382
  }
384
383
  : {};
385
- const preallocatedManagedNativeSessionId = managedControl
386
- && binding.adapterId === "claude"
384
+ const preallocatedNativeSessionId = binding.adapterId === "claude"
387
385
  && resumeNativeSessionId === undefined
388
386
  ? requireText(input.launchId === undefined
389
387
  ? this.#createNativeSessionId()
390
388
  : nativeSessionIdForLaunch(this.home, input.launchId, input.agentId, input.adapterId), "Native session id")
391
389
  : resumeNativeSessionId;
392
390
  const managedCompiled = managedControl
393
- ? adapter.compileManagedControl(compileInput, launchMode, preallocatedManagedNativeSessionId)
391
+ ? adapter.compileManagedControl(compileInput, launchMode, preallocatedNativeSessionId)
394
392
  : undefined;
395
393
  const compiled = managedCompiled !== undefined
396
394
  ? managedCompiled
@@ -439,7 +437,7 @@ export class FileRoleLaunchPlanner {
439
437
  else if (launchMode === "new") {
440
438
  if (managedControl)
441
439
  args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
442
- const nativeSessionId = requireText(preallocatedManagedNativeSessionId, "Native session id");
440
+ const nativeSessionId = requireText(preallocatedNativeSessionId, "Native session id");
443
441
  if (!managedControl)
444
442
  args.push("--session-id", nativeSessionId);
445
443
  else if (!args.includes("--session-id"))
@@ -495,7 +493,7 @@ export class FileRoleLaunchPlanner {
495
493
  ? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId)
496
494
  : undefined;
497
495
  const providerNativeSessionId = binding.adapterId === "claude"
498
- ? preallocatedManagedNativeSessionId
496
+ ? preallocatedNativeSessionId
499
497
  : resumeNativeSessionId;
500
498
  const providerControl = managedControl
501
499
  ? {
@@ -506,12 +504,13 @@ export class FileRoleLaunchPlanner {
506
504
  ...(providerNativeSessionId === undefined
507
505
  ? {}
508
506
  : { nativeSessionId: providerNativeSessionId }),
507
+ ...(sessionTitle === undefined ? {} : { sessionTitle }),
509
508
  authority: providerAuthority,
510
509
  ...(carriesInitialTurn
511
510
  ? {
512
511
  initialTurn: {
513
512
  attemptId: formatAgentRunReceiptId(owner.taskId, input.runId),
514
- boundedText: managedRunLaunchEnvelope(managedRun, input.mode)
513
+ boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionTitle)
515
514
  }
516
515
  }
517
516
  : {})
@@ -578,6 +577,7 @@ export class FileRoleLaunchPlanner {
578
577
  },
579
578
  launch: scopedLaunch,
580
579
  session,
580
+ ...(sessionTitle === undefined ? {} : { sessionTitle }),
581
581
  ...(carriesInitialTurn && input.runId !== undefined
582
582
  ? { initialTurnRunId: input.runId }
583
583
  : {})
@@ -645,18 +645,20 @@ export class FileRoleLaunchPlanner {
645
645
  return selectEnvironment(source, names);
646
646
  }
647
647
  }
648
- function managedRunLaunchEnvelope(run, mode) {
649
- if (run.providerRetry?.state === "dispatching") {
650
- return serializeProviderRetryEnvelope({
648
+ function managedRunLaunchEnvelope(run, mode, title) {
649
+ const body = run.providerRetry?.state === "dispatching"
650
+ ? serializeProviderRetryEnvelope({
651
651
  taskId: run.taskId,
652
652
  runId: run.id,
653
653
  roleName: run.roleName,
654
654
  retry: run.providerRetry
655
- });
656
- }
657
- return mode === "resume" && run.pushedAt !== undefined
658
- ? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
659
- : serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
655
+ })
656
+ : mode === "resume" && run.pushedAt !== undefined
657
+ ? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
658
+ : serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
659
+ return title === undefined
660
+ ? body
661
+ : prefixYuiTitleInput(body, title);
660
662
  }
661
663
  export function nativeAgentWorkspace(workspace) {
662
664
  return workspace.entries.length === 1
@@ -740,18 +742,17 @@ function ensureManagedClaudeLifecyclePlugin(home, cliPath) {
740
742
  }, null, 2)}\n`);
741
743
  return root;
742
744
  }
743
- function managedClaudeControlPlaneConfig(config, taskId, workItemId, runId, controlPlane, sessionCliPath) {
745
+ function managedClaudeControlPlaneConfig(config, taskId, workItemId, runId) {
744
746
  if (config.permission.strategy !== "configured")
745
747
  return config;
746
- const exact = exactControlPlaneCommandPrefix(controlPlane);
747
748
  const managed = [
748
- `Bash(${sessionCliPath} task run context ${taskId}/${runId}:*)`,
749
- `Bash(${exact} --json task context ${taskId})`,
750
- `Bash(${exact} --json task work list ${taskId})`,
749
+ `Bash(yui task run context ${taskId}/${runId}:*)`,
750
+ `Bash(yui --json task context ${taskId})`,
751
+ `Bash(yui --json task work list ${taskId})`,
751
752
  ...(workItemId === undefined
752
753
  ? []
753
- : [`Bash(${exact} --json task work show ${workItemId})`]),
754
- `Bash(${exact} task run yield ${runId} --summary-file -:*)`
754
+ : [`Bash(yui --json task work show ${workItemId})`]),
755
+ `Bash(yui task run yield ${runId} --summary-file -:*)`
755
756
  ];
756
757
  const existing = (config.permission.allowedTools ?? [])
757
758
  .filter((rule) => !isManagedYuiBashRule(rule));
@@ -768,15 +769,6 @@ function isManagedYuiBashRule(rule) {
768
769
  return /^Bash\(yui(?:\s|:\*|\*|\))/u.test(normalized)
769
770
  || /^Bash\(.*\s--yui-control\s/u.test(normalized);
770
771
  }
771
- function renderExactControlPlaneInstructions(descriptor) {
772
- const command = exactControlPlaneCommandPrefix(descriptor);
773
- return [
774
- "Exact Yui control-plane command prefix for this managed Task session:",
775
- `\`${command}\``,
776
- "Replace the portable bare `yui` token in Yui Role Skills and dispatch instructions with this exact prefix.",
777
- "Bare `yui`, a PATH launcher, another checkout CLI, another YUI_HOME, or a changed schema/Controller identity is invalid and fails before Task state is read or written."
778
- ].join("\n");
779
- }
780
772
  function canonicalPath(path) {
781
773
  const absolute = resolve(path);
782
774
  try {
@@ -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)
@@ -0,0 +1,196 @@
1
+ import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
2
+ import { projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
3
+ import { taskDeliveryPath } from "../task/task.js";
4
+ /** One Task's orchestration cost and advisory projection, with no writes. */
5
+ export function projectTaskOrchestration(facts) {
6
+ const evidence = {
7
+ listAgentRuns: () => facts.runs,
8
+ listReviewFindings: () => facts.reviewFindings,
9
+ listEvents: () => facts.events
10
+ };
11
+ const fullRounds = facts.reviewRounds.filter((round) => round.deltaRecheck === undefined);
12
+ const deltaRounds = facts.reviewRounds.filter((round) => round.deltaRecheck !== undefined);
13
+ const classifications = new Map(facts.reviewRounds.map((round) => [round.id, classifyReviewRoundOutcome(round, evidence)]));
14
+ const semanticRounds = facts.reviewRounds.filter((round) => (classifications.get(round.id)?.kind === "semantic"));
15
+ const p1P2Findings = facts.reviewFindings.filter((finding) => ((finding.severity === "p1" || finding.severity === "p2")
16
+ && semanticRounds.some((round) => round.id === finding.firstReviewRoundId))).length;
17
+ const candidateTimes = [
18
+ ...facts.changeSets.map(({ createdAt }) => createdAt),
19
+ ...facts.workItems.flatMap((item) => item.candidates
20
+ .filter((candidate) => candidate.gitSnapshot !== undefined
21
+ || candidate.taskMainSnapshot?.projects.some((project) => (project.baseCommit !== project.headCommit)))
22
+ .map(({ createdAt }) => createdAt))
23
+ ].sort();
24
+ const firstCommitAt = candidateTimes[0];
25
+ const integrationIdentities = new Map();
26
+ for (const job of facts.durableJobs) {
27
+ if (job.owner.kind !== "integration-attempt")
28
+ continue;
29
+ const integrationAttemptId = job.owner.integrationAttemptId;
30
+ const attempt = facts.integrations.find(({ id }) => id === integrationAttemptId);
31
+ if (attempt === undefined || attempt.jobId !== job.id)
32
+ continue;
33
+ const identity = `${attempt.projectId}\0${job.head}\0${JSON.stringify(attempt.checkCommands)}`;
34
+ integrationIdentities.set(identity, (integrationIdentities.get(identity) ?? 0) + 1);
35
+ }
36
+ const repeatedIdentities = [...integrationIdentities.values()]
37
+ .reduce((total, count) => total + Math.max(0, count - 1), 0);
38
+ const leaderSessions = facts.roleSessionSets.find(({ owner }) => owner.roleName === "leader") ?? null;
39
+ const firstProgress = projectFirstProgressStopLoss({
40
+ sessions: leaderSessions,
41
+ events: facts.events,
42
+ workItems: facts.workItems,
43
+ reviewRounds: facts.reviewRounds,
44
+ integrations: facts.integrations
45
+ });
46
+ const advisories = projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgress.exhausted);
47
+ const publicationAt = facts.task.completedAt === undefined
48
+ ? undefined
49
+ : facts.publications
50
+ .map((reference) => reference.mergedAt ?? reference.createdAt)
51
+ .filter((timestamp) => timestamp <= facts.task.completedAt)
52
+ .sort()
53
+ .at(-1);
54
+ return Object.freeze({
55
+ taskId: facts.task.id,
56
+ deliveryPath: taskDeliveryPath(facts.task),
57
+ timeToFirstProjectCommitMs: firstCommitAt === undefined
58
+ ? null
59
+ : Math.max(0, Date.parse(firstCommitAt) - Date.parse(facts.task.createdAt)),
60
+ runs: {
61
+ total: facts.runs.length,
62
+ byStatus: counts(facts.runs.map(({ status }) => status)),
63
+ byRole: counts(facts.runs.map(({ roleName }) => roleName))
64
+ },
65
+ workItems: facts.workItems.length,
66
+ reviews: {
67
+ full: fullRounds.length,
68
+ delta: deltaRounds.length,
69
+ nonSemantic: [...classifications.values()].filter((value) => value?.kind === "non-semantic").length,
70
+ ambiguous: [...classifications.values()].filter((value) => value?.kind === "ambiguous").length,
71
+ p1P2Findings,
72
+ p1P2FindingsPerSemanticReview: semanticRounds.length === 0
73
+ ? 0
74
+ : p1P2Findings / semanticRounds.length
75
+ },
76
+ integrations: {
77
+ attempts: facts.integrations.length,
78
+ failed: facts.integrations.filter(({ status }) => status === "failed").length,
79
+ repeatedIdentities,
80
+ evidenceReuses: facts.integrations.filter((attempt) => ((attempt.checks ?? []).some(({ details }) => details?.startsWith("Reused successful check evidence from ")))).length
81
+ },
82
+ providerGenerationsBeforeFirstProgress: firstProgress.generationsBeforeFirstProgress,
83
+ publicationToCompletionMs: publicationAt === undefined || facts.task.completedAt === undefined
84
+ ? null
85
+ : Math.max(0, Date.parse(facts.task.completedAt) - Date.parse(publicationAt)),
86
+ terminalWorkspaceCount: terminalWorkspaceCount(facts),
87
+ advisories
88
+ });
89
+ }
90
+ function projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, stopLoss) {
91
+ const result = [];
92
+ if (taskDeliveryPath(facts.task) === "direct"
93
+ && (facts.workItems.length > 0 || facts.reviewRounds.length > 0 || facts.integrations.length > 0)) {
94
+ result.push({
95
+ code: "direct-protocol-overhead",
96
+ reason: "Direct delivery accumulated WorkItem, Review, or Integration protocol overhead; keep the fix Leader-direct or explicitly promote it to integrated delivery.",
97
+ refs: [
98
+ ...facts.workItems.map(({ id }) => `work-item:${id}`),
99
+ ...facts.reviewRounds.map(({ id }) => `review-round:${id}`),
100
+ ...facts.integrations.map(({ id }) => `integration-attempt:${id}`)
101
+ ]
102
+ });
103
+ }
104
+ const initial = facts.workItems.filter(({ dependsOn }) => dependsOn.length === 0);
105
+ if (taskDeliveryPath(facts.task) === "integrated" && initial.length > 1) {
106
+ result.push({
107
+ code: "guarded-workitem-fanout",
108
+ reason: `${initial.length} initial WorkItems were created for an integrated Task; start with one bounded fix unless independence is explicit.`,
109
+ refs: initial.map(({ id }) => `work-item:${id}`)
110
+ });
111
+ }
112
+ const repairItems = facts.workItems.filter((item) => (item.acceptance.some((line) => line.startsWith("review-finding:"))));
113
+ const byRound = new Map();
114
+ for (const item of repairItems) {
115
+ const roundIds = new Set(item.acceptance.flatMap((line) => {
116
+ const findingId = line.startsWith("review-finding:") ? line.slice("review-finding:".length) : "";
117
+ const finding = facts.reviewFindings.find(({ id }) => id === findingId);
118
+ return finding === undefined ? [] : [finding.firstReviewRoundId];
119
+ }));
120
+ for (const roundId of roundIds)
121
+ byRound.set(roundId, [...(byRound.get(roundId) ?? []), item]);
122
+ }
123
+ for (const [roundId, items] of byRound) {
124
+ if (items.length < 2 || hasRepairFanoutDecision(facts.decisions, roundId, items))
125
+ continue;
126
+ result.push({
127
+ code: "review-repair-fanout",
128
+ reason: `Findings from Review ${roundId} were split across ${items.length} WorkItems without a durable Decision explaining independent ownership.`,
129
+ refs: [`review-round:${roundId}`, ...items.map(({ id }) => `work-item:${id}`)]
130
+ });
131
+ }
132
+ if (repeatedIdentities > 0) {
133
+ result.push({
134
+ code: "repeated-integration-check",
135
+ reason: `${repeatedIdentities} Integration DurableJob(s) reran the same candidate commit and ordered checks.`,
136
+ refs: facts.durableJobs
137
+ .filter(({ owner }) => owner.kind === "integration-attempt")
138
+ .map(({ id }) => `durable-job:${id}`)
139
+ });
140
+ }
141
+ const semanticFull = fullRounds.filter((round) => classifications.get(round.id)?.kind === "semantic")
142
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt));
143
+ const recent = semanticFull.slice(-3);
144
+ const recentIds = new Set(recent.map(({ id }) => id));
145
+ const newFinding = facts.reviewFindings.some(({ firstReviewRoundId }) => recentIds.has(firstReviewRoundId));
146
+ if (semanticFull.length > 2 && !newFinding) {
147
+ result.push({
148
+ code: "review-budget-exhausted",
149
+ reason: `${semanticFull.length} full semantic Reviews ran and the latest three produced no new finding; stop repeating full rounds without a changed head or new risk.`,
150
+ refs: recent.map(({ id }) => `review-round:${id}`)
151
+ });
152
+ }
153
+ if (stopLoss) {
154
+ result.push({
155
+ code: "provider-first-progress-stop-loss",
156
+ reason: "Two fresh Leader generations produced no first durable progress; hand off to the unique Operator before another generation.",
157
+ refs: [facts.task.id]
158
+ });
159
+ }
160
+ return result;
161
+ }
162
+ function hasRepairFanoutDecision(decisions, roundId, items) {
163
+ return decisions.some((decision) => {
164
+ const text = `${decision.title}\n${decision.rationale}`;
165
+ return text.includes(roundId) && items.every(({ id }) => text.includes(id));
166
+ });
167
+ }
168
+ function terminalWorkspaceCount(facts) {
169
+ return facts.managedWorkspaces.filter(({ owner }) => {
170
+ if (owner.type === "task")
171
+ return false;
172
+ if (owner.type === "work-item") {
173
+ return terminalStatus(facts.workItems.find(({ id }) => id === owner.workItemId)?.status);
174
+ }
175
+ if (owner.type === "review-round") {
176
+ return terminalStatus(facts.reviewRounds.find(({ id }) => id === owner.reviewRoundId)?.status);
177
+ }
178
+ if (owner.type === "integration-attempt") {
179
+ return terminalStatus(facts.integrations.find(({ id }) => id === owner.integrationAttemptId)?.status);
180
+ }
181
+ if (owner.workItemId !== undefined) {
182
+ return terminalStatus(facts.workItems.find(({ id }) => id === owner.workItemId)?.status);
183
+ }
184
+ return owner.reviewRoundId !== undefined
185
+ && terminalStatus(facts.reviewRounds.find(({ id }) => id === owner.reviewRoundId)?.status);
186
+ }).length;
187
+ }
188
+ function terminalStatus(status) {
189
+ return status !== undefined && !["pending", "running", "awaiting_acceptance", "blocked", "validating"].includes(status);
190
+ }
191
+ function counts(values) {
192
+ const result = {};
193
+ for (const value of values)
194
+ result[value] = (result[value] ?? 0) + 1;
195
+ return Object.freeze(result);
196
+ }