agent-dealer 1.2.7 → 1.2.9

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 (112) hide show
  1. package/bundle/server/dist/adapters/agent-deck-bind.js +16 -6
  2. package/bundle/server/dist/adapters/agent-deck-bind.test.js +74 -0
  3. package/bundle/server/dist/adapters/agent-health.js +14 -3
  4. package/bundle/server/dist/adapters/github.js +84 -5
  5. package/bundle/server/dist/adapters/github.test.js +83 -1
  6. package/bundle/server/dist/adapters/muse-capability.js +443 -24
  7. package/bundle/server/dist/adapters/muse-capability.test.js +469 -25
  8. package/bundle/server/dist/adapters/muse-visual-qa.js +114 -0
  9. package/bundle/server/dist/adapters/muse-visual-qa.test.js +68 -0
  10. package/bundle/server/dist/adapters/worktree-deps.js +96 -0
  11. package/bundle/server/dist/adapters/worktree-deps.test.js +144 -0
  12. package/bundle/server/dist/capacity/muse-probe.js +56 -9
  13. package/bundle/server/dist/capacity/muse-probe.test.js +188 -1
  14. package/bundle/server/dist/coordinator/admission.js +14 -1
  15. package/bundle/server/dist/coordinator/admission.test.js +197 -4
  16. package/bundle/server/dist/coordinator/auto-merge.integration.test.js +276 -0
  17. package/bundle/server/dist/coordinator/auto-merge.js +244 -4
  18. package/bundle/server/dist/coordinator/checks-wait-config.js +19 -0
  19. package/bundle/server/dist/coordinator/commands.js +284 -23
  20. package/bundle/server/dist/coordinator/commands.test.js +47 -0
  21. package/bundle/server/dist/coordinator/deck-outage.integration.test.js +4 -2
  22. package/bundle/server/dist/coordinator/developer-effect.js +224 -4
  23. package/bundle/server/dist/coordinator/developer-effect.test.js +288 -3
  24. package/bundle/server/dist/coordinator/execution-report.js +6 -0
  25. package/bundle/server/dist/coordinator/execution-report.test.js +7 -0
  26. package/bundle/server/dist/coordinator/failure-cause.js +64 -1
  27. package/bundle/server/dist/coordinator/failure-cause.test.js +92 -1
  28. package/bundle/server/dist/coordinator/failure-reason.js +7 -0
  29. package/bundle/server/dist/coordinator/failure-reason.test.js +14 -0
  30. package/bundle/server/dist/coordinator/human-resolution.js +36 -1
  31. package/bundle/server/dist/coordinator/human-resolution.test.js +33 -0
  32. package/bundle/server/dist/coordinator/merge-conflict-sync.js +647 -0
  33. package/bundle/server/dist/coordinator/merge-conflict-sync.test.js +121 -0
  34. package/bundle/server/dist/coordinator/muse-developer.integration.test.js +9 -4
  35. package/bundle/server/dist/coordinator/muse-spawn.js +122 -29
  36. package/bundle/server/dist/coordinator/muse-spawn.test.js +210 -0
  37. package/bundle/server/dist/coordinator/operator-criteria.js +168 -0
  38. package/bundle/server/dist/coordinator/operator-criteria.test.js +78 -0
  39. package/bundle/server/dist/coordinator/operator-verification.integration.test.js +347 -0
  40. package/bundle/server/dist/coordinator/playbook-feedback.js +693 -0
  41. package/bundle/server/dist/coordinator/playbook-feedback.test.js +702 -0
  42. package/bundle/server/dist/coordinator/projection.js +10 -4
  43. package/bundle/server/dist/coordinator/projection.test.js +13 -0
  44. package/bundle/server/dist/coordinator/prompts-execution-contract.test.js +107 -0
  45. package/bundle/server/dist/coordinator/prompts.js +211 -2
  46. package/bundle/server/dist/coordinator/prompts.test.js +231 -0
  47. package/bundle/server/dist/coordinator/recovery.js +57 -3
  48. package/bundle/server/dist/coordinator/recovery.test.js +67 -8
  49. package/bundle/server/dist/coordinator/reflect-trigger.js +33 -169
  50. package/bundle/server/dist/coordinator/reflect-trigger.test.js +149 -200
  51. package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +1 -0
  52. package/bundle/server/dist/coordinator/reviewer-effect.js +16 -1
  53. package/bundle/server/dist/coordinator/reviewer-result.js +6 -0
  54. package/bundle/server/dist/coordinator/routing.js +45 -6
  55. package/bundle/server/dist/coordinator/routing.test.js +76 -15
  56. package/bundle/server/dist/coordinator/session-timeouts.js +30 -0
  57. package/bundle/server/dist/coordinator/usage-cap-defer.js +61 -0
  58. package/bundle/server/dist/coordinator/usage-cap.integration.test.js +1 -1
  59. package/bundle/server/dist/coordinator/worker-loop.js +18 -4
  60. package/bundle/server/dist/db/ci-attempts-migration.test.js +70 -0
  61. package/bundle/server/dist/db/index.js +18 -0
  62. package/bundle/server/dist/db/schema.sql +5 -0
  63. package/bundle/server/dist/docs-execution-analysis.test.js +1 -0
  64. package/bundle/server/dist/repository/artifacts-for-issue.js +3 -3
  65. package/bundle/server/dist/repository/human-actions.js +14 -0
  66. package/bundle/server/dist/repository/issues.js +67 -6
  67. package/bundle/server/dist/repository/work-items.js +7 -0
  68. package/bundle/server/dist/repository/worker-sessions.js +51 -2
  69. package/bundle/server/dist/routes/human-actions.js +10 -8
  70. package/bundle/server/dist/routes/issues-execution-contract.test.js +321 -0
  71. package/bundle/server/dist/routes/issues.js +19 -2
  72. package/bundle/server/dist/runners/muse-code-args.js +25 -0
  73. package/bundle/server/dist/runners/muse-code-args.test.js +30 -0
  74. package/bundle/server/dist/runners/muse-code-jsonl.js +106 -11
  75. package/bundle/server/dist/runners/muse-config-core.js +18 -1
  76. package/bundle/server/dist/runners/muse-config.test.js +37 -0
  77. package/bundle/server/dist/runners/muse-serve-session.js +5 -0
  78. package/bundle/server/dist/runners/spawn-cli.js +116 -0
  79. package/bundle/server/dist/runners/spawn-cli.test.js +124 -0
  80. package/bundle/server/package.json +2 -2
  81. package/bundle/server/static-ui/assets/index-CU6fNntg.js +64 -0
  82. package/bundle/server/static-ui/assets/{index-DyAJNyfV.css → index-K_YcYkQU.css} +1 -1
  83. package/bundle/server/static-ui/index.html +2 -2
  84. package/bundle/shared/dist/execution-analysis.d.ts +22 -22
  85. package/bundle/shared/dist/execution-contract.d.ts +117 -0
  86. package/bundle/shared/dist/execution-contract.js +307 -0
  87. package/bundle/shared/dist/execution-contract.test.d.ts +2 -0
  88. package/bundle/shared/dist/execution-contract.test.js +499 -0
  89. package/bundle/shared/dist/execution-report.d.ts +9 -9
  90. package/bundle/shared/dist/execution-report.js +2 -0
  91. package/bundle/shared/dist/failure-cause.d.ts +4 -4
  92. package/bundle/shared/dist/failure-cause.js +2 -0
  93. package/bundle/shared/dist/human-actions.d.ts +4 -4
  94. package/bundle/shared/dist/human-actions.js +17 -0
  95. package/bundle/shared/dist/index.d.ts +85 -84
  96. package/bundle/shared/dist/index.js +3 -0
  97. package/bundle/shared/dist/issues.d.ts +176 -16
  98. package/bundle/shared/dist/issues.js +22 -0
  99. package/bundle/shared/dist/issues.test.js +3 -0
  100. package/bundle/shared/dist/outbound-draft.d.ts +12 -12
  101. package/bundle/shared/dist/worker-sessions.d.ts +10 -0
  102. package/bundle/shared/dist/worker-sessions.js +8 -0
  103. package/bundle/shared/dist/worker-sessions.test.js +1 -0
  104. package/bundle/shared/dist/workflow.d.ts +4 -4
  105. package/bundle/shared/dist/workflow.js +16 -0
  106. package/bundle/shared/dist/workflow.test.js +3 -0
  107. package/bundle/shared/package.json +1 -1
  108. package/dist/action.js +4 -1
  109. package/dist/action.test.js +42 -0
  110. package/dist/index.js +1 -1
  111. package/package.json +1 -1
  112. package/bundle/server/static-ui/assets/index-yLyxRd-7.js +0 -64
@@ -18,16 +18,22 @@ import os from "node:os";
18
18
  import path from "node:path";
19
19
  import { parsePhaseBudget, parseProfileSnapshot, roleCeiling } from "@agent-dealer/shared";
20
20
  import { getTaskSnapshot } from "./commands.js";
21
+ import { extractOperatorCriteria } from "./operator-criteria.js";
21
22
  import { buildDeveloperPrompt } from "./prompts.js";
22
23
  import { guidanceForNextSession } from "./guidance.js";
23
24
  import { realDeveloperSpawn, developerSessionLogPath } from "./spawn.js";
25
+ import { ensureWorktreeDeps, worktreeDepsTimeoutMs, } from "../adapters/worktree-deps.js";
24
26
  import { DEFAULT_BASE_FETCH_TIMEOUT_MS, fastForwardLocalBranchToSha, resolveDeveloperWorktree, safeRemoveWorktree, isWorktreeClean, commitsAhead, pushBranch, mergeBase, branchExists, pushBranchRef, revParseHead, revParseRef, fetchRef, salvageDirtyWorktree, dirtyWorktreeRecoveryCommands, withRepoLock, leftoverFingerprint, } from "../adapters/git-worktree.js";
25
27
  import { recordIssueBaseSha } from "../repository/issues.js";
26
28
  import { ensureIssueRepoCheckout, roleWorktreePathForResolution, resolveCheckoutBaseBranch, } from "../adapters/managed-repo.js";
27
29
  import { baseRefCandidates, inspectBranchProgress } from "./branch-progress.js";
28
30
  import { prepareWorkerDeckConnection, releaseWorkerDeckConnection, verifyWorkerDeckConnection } from "../adapters/agent-deck-bind.js";
29
- import { realGithubAdapter, pollPrChecks } from "../adapters/github.js";
30
- import { getWorkerSession, patchRunningSession, recordSessionProcess, setSessionInputSha } from "../repository/worker-sessions.js";
31
+ import { realGithubAdapter, pollPrChecksDetailed } from "../adapters/github.js";
32
+ import { checksWaitCeilingExceeded, checksWaitCeilingMs } from "./checks-wait-config.js";
33
+ import { museCapabilitySafetyNetAfterSession } from "../adapters/muse-capability.js";
34
+ import { getOrAssignSessionCorrelationId, getWorkerSession, mergeSessionMetadata, patchRunningSession, recordSessionProcess, setSessionInputSha } from "../repository/worker-sessions.js";
35
+ import { museIdleMinutes, museStallMetadata } from "./muse-spawn.js";
36
+ import { museIdleTimeoutMs } from "./session-timeouts.js";
31
37
  import { COORDINATOR_PROCESS_OWNER, readProcessStartTime } from "./process-liveness.js";
32
38
  import { checkDeveloperWorktreeOwnerLiveness } from "./worktree-owner-liveness.js";
33
39
  import { developerSessionTimeoutMs } from "./session-timeouts.js";
@@ -87,6 +93,23 @@ async function crashOrTimeoutOutcome(opts) {
87
93
  ...(opts.logPath ? { logPath: opts.logPath } : {}),
88
94
  };
89
95
  }
96
+ /**
97
+ * NOT-307: idle-kill description for the timeout reason, or null for anything else
98
+ * (wall-clock timeout, crash, success). Minutes prefer the measured last-activity
99
+ * time and fall back to the configured bound; the last tool name rides along so the
100
+ * recorded reason — and the `muse_no_progress` classifier — names the stall point.
101
+ */
102
+ function idleCrashInfoFor(spawned) {
103
+ if (!spawned.idleTimedOut)
104
+ return null;
105
+ return {
106
+ minutes: museIdleMinutes({
107
+ lastActivityAt: spawned.muse?.lastActivityAt ?? null,
108
+ idleTimeoutMs: museIdleTimeoutMs(),
109
+ }),
110
+ lastToolName: spawned.muse?.lastToolName ?? null,
111
+ };
112
+ }
90
113
  export const developerEffectConfig = {
91
114
  get sessionTimeoutMs() {
92
115
  return developerSessionTimeoutMs();
@@ -97,10 +120,18 @@ export const developerEffectConfig = {
97
120
  get checksPollIntervalMs() {
98
121
  return num("CHECKS_POLL_INTERVAL_MS", 15_000);
99
122
  },
123
+ /** NOT-311: overall CI-wait ceiling measured from the first poll, across deferrals. */
124
+ get checksWaitCeilingMs() {
125
+ return checksWaitCeilingMs();
126
+ },
100
127
  /** NOT-197: bound for the pre-branch `git fetch origin <base>` on a fresh issue branch. */
101
128
  get baseFetchTimeoutMs() {
102
129
  return num("BASE_FETCH_TIMEOUT_MS", DEFAULT_BASE_FETCH_TIMEOUT_MS);
103
130
  },
131
+ /** NOT-315: bound for `npm ci` in the fresh worktree (coordinator side, with network). */
132
+ get worktreeDepsTimeoutMs() {
133
+ return worktreeDepsTimeoutMs();
134
+ },
104
135
  /** NOT-110: bounded window to let a lagging `gh pr view` catch up to a just-pushed HEAD. */
105
136
  get headReconcileTimeoutMs() {
106
137
  return num("HEAD_RECONCILE_TIMEOUT_MS", 30_000);
@@ -270,6 +301,69 @@ async function checksFailedOutcome(opts) {
270
301
  });
271
302
  return details ? { kind: "checks_failed", details } : { kind: "checks_failed" };
272
303
  }
304
+ /**
305
+ * NOT-311: the `checksWaitStartedAt` persisted by a prior `checks_pending` deferral,
306
+ * anchoring the overall wait ceiling across re-polls. Null on a first wait (or an
307
+ * unparseable payload) — the caller then anchors at its own poll start.
308
+ */
309
+ function checksWaitStartedAtFromPayload(payloadJson) {
310
+ if (!payloadJson)
311
+ return null;
312
+ try {
313
+ const v = JSON.parse(payloadJson).checksWaitStartedAt;
314
+ return typeof v === "string" && v ? v : null;
315
+ }
316
+ catch {
317
+ return null;
318
+ }
319
+ }
320
+ /**
321
+ * NOT-311: decide what a finished checks poll means. Returns a `checks_pending`
322
+ * outcome when CI is still queued/running at the timeout (or reports no checks while
323
+ * the base branch has a `pull_request` workflow — "not yet registered", never "no
324
+ * CI"), and null when the caller stays on today's path (success, failure, an aborted
325
+ * wait, or `none` with no workflow configured). Also yields the `checks.completed`
326
+ * milestone reason for the wait cases.
327
+ *
328
+ * A missing/unknown workflow answer (no adapter method, unreadable base ref) waits
329
+ * fail-closed: sending an unverified PR to review is the outcome this exists to
330
+ * prevent, and the wait ceiling still bounds it.
331
+ */
332
+ async function resolveChecksWait(opts) {
333
+ const waitStartedAt = checksWaitStartedAtFromPayload(opts.payloadJson) ?? opts.pollStartedAt;
334
+ const base = {
335
+ kind: "checks_pending",
336
+ branch: opts.branch,
337
+ prNumber: opts.prNumber,
338
+ headSha: opts.headSha,
339
+ waitStartedAt,
340
+ };
341
+ const noChecks = opts.poll.result === "none" || (opts.poll.result === "timeout" && opts.poll.lastSnapshot === "none");
342
+ if (noChecks) {
343
+ let hasWorkflow = null;
344
+ try {
345
+ hasWorkflow = (await opts.github.hasPullRequestWorkflow?.({ cwd: opts.cwd, baseRef: opts.baseRef })) ?? null;
346
+ }
347
+ catch {
348
+ hasWorkflow = null;
349
+ }
350
+ if (hasWorkflow === false) {
351
+ return { pending: null, milestoneReason: "none_no_workflow" };
352
+ }
353
+ return {
354
+ pending: { ...base, reason: "Waiting for CI (queued) - no attempt spent" },
355
+ milestoneReason: "none_with_workflow",
356
+ };
357
+ }
358
+ if (opts.poll.result === "timeout" && opts.poll.lastSnapshot === "pending" && !opts.poll.aborted && !opts.signal?.aborted) {
359
+ const milestoneReason = checksWaitCeilingExceeded(waitStartedAt) ? "pending_ceiling" : undefined;
360
+ return {
361
+ pending: { ...base, reason: "Waiting for CI (running) - no attempt spent" },
362
+ ...(milestoneReason ? { milestoneReason } : {}),
363
+ };
364
+ }
365
+ return { pending: null };
366
+ }
273
367
  /**
274
368
  * Infra retry that skips the agent: publish whatever the branch already carries. Uses the
275
369
  * issue repo as `gh` cwd and `origin/<branch>` as the verified head (any worktree was
@@ -508,17 +602,35 @@ async function runPublishOnlyHandoff(ctx, deps, branchName) {
508
602
  prNumber: prView.number,
509
603
  headSha: prView.headRefOid,
510
604
  });
511
- const checks = await pollPrChecks(deps.github, {
605
+ const pollStartedAt = new Date().toISOString();
606
+ const poll = await pollPrChecksDetailed(deps.github, {
512
607
  cwd,
513
608
  timeoutMs: developerEffectConfig.checksPollTimeoutMs,
514
609
  intervalMs: developerEffectConfig.checksPollIntervalMs,
515
610
  signal: ctx.signal,
516
611
  number: prView.number,
517
612
  });
613
+ // NOT-311: pending/queued CI at the timeout (or no checks yet with a
614
+ // `pull_request` workflow on the base) waits with backoff instead of spending
615
+ // an infra attempt — resolved here so the milestone carries the wait reason.
616
+ const wait = await resolveChecksWait({
617
+ github: deps.github,
618
+ cwd,
619
+ prNumber: prView.number,
620
+ baseRef: prView.baseRefName,
621
+ headSha: prView.headRefOid,
622
+ branch: branchName,
623
+ poll,
624
+ signal: ctx.signal,
625
+ payloadJson: workItem.payloadJson,
626
+ pollStartedAt,
627
+ });
628
+ const checks = poll.result;
518
629
  milestone("checks.completed", `Developer · checks ${checks}`, {
519
630
  snapshot: checks,
520
631
  prNumber: prView.number,
521
632
  headSha: prView.headRefOid,
633
+ ...(wait.milestoneReason ? { reason: wait.milestoneReason } : {}),
522
634
  });
523
635
  const postPollView = await deps.github.viewPr({ cwd, branch: branchName });
524
636
  if (!postPollView) {
@@ -555,6 +667,10 @@ async function runPublishOnlyHandoff(ctx, deps, branchName) {
555
667
  author: "system",
556
668
  content: { snapshot: checks, prNumber: prView.number, headSha: prView.headRefOid },
557
669
  });
670
+ // NOT-311: still-waiting CI defers with backoff (no attempt spent) instead of
671
+ // timing out the session. An aborted wait keeps today's `timed_out`.
672
+ if (wait.pending)
673
+ return wait.pending;
558
674
  if (checks === "timeout")
559
675
  return { kind: "timed_out" };
560
676
  await fetchRef(cwd, prView.baseRefName);
@@ -601,6 +717,9 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
601
717
  }
602
718
  const session = getWorkerSession(sessionId);
603
719
  const snapshot = parseProfileSnapshot(session?.profileSnapshotJson);
720
+ // NOT-305: opaque per-session Deck correlation UUID, persisted before spawn and
721
+ // carried as observability metadata in every runtime launch config below.
722
+ const deckCorrelationId = session?.deckCorrelationId ?? getOrAssignSessionCorrelationId(sessionId);
604
723
  let taskSnapshot = getTaskSnapshot(issue);
605
724
  const runtime = snapshot?.runtime ?? "claude_code";
606
725
  const round = workItem.round;
@@ -750,6 +869,18 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
750
869
  recordIssueBaseSha(issue.id, resolved.baseSha);
751
870
  issue.baseSha = resolved.baseSha;
752
871
  }
872
+ // NOT-315: provision dependencies here, outside the sandbox, with network — the
873
+ // builder sandbox has none, so an in-session install can never work. A throw
874
+ // lands in the worktree-setup adapter_failure below: visible and
875
+ // infra-retryable, never a silent hour-long stall. The session is not spawned.
876
+ const depsResult = await ensureWorktreeDeps(worktreePath, {
877
+ runner: deps.depsRunner,
878
+ timeoutMs: developerEffectConfig.worktreeDepsTimeoutMs,
879
+ });
880
+ milestone("worktree.deps_ready", `Developer · dependencies ready (round ${round})`, {
881
+ ...(depsResult.ran ? { ran: true } : { ran: false, reason: depsResult.reason }),
882
+ durationMs: depsResult.durationMs,
883
+ });
753
884
  }
754
885
  catch (err) {
755
886
  return { kind: "adapter_failure", reason: `worktree setup failed: ${String(err)}` };
@@ -807,6 +938,7 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
807
938
  worktreePath,
808
939
  runtime,
809
940
  policy,
941
+ correlationId: deckCorrelationId,
810
942
  verifyCallTool: deps.deckCallTool,
811
943
  });
812
944
  if (!prepared.ok) {
@@ -862,6 +994,30 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
862
994
  const scopeDecisionNote = typeof payload.scopeDecisionNote === "string" && payload.scopeDecisionNote.trim()
863
995
  ? payload.scopeDecisionNote.trim()
864
996
  : undefined;
997
+ // NOT-314: same one-shot ride for an operator_verification repair note.
998
+ const operatorRepairNote = typeof payload.operatorRepairNote === "string" && payload.operatorRepairNote.trim()
999
+ ? payload.operatorRepairNote.trim()
1000
+ : undefined;
1001
+ // NOT-314: the frozen snapshot's operator criteria, parsed here so the
1002
+ // worker never re-derives them — the section renders only when non-empty.
1003
+ const operatorCriteria = extractOperatorCriteria(taskSnapshot.acceptanceCriteria);
1004
+ const hasOperatorCriteria = operatorCriteria.length > 0 ? operatorCriteria : undefined;
1005
+ // NOT-310: the conflict directive rides only the work item queued by the merge
1006
+ // path — exactly this round reads it; nothing is re-read from the DB. Shaped
1007
+ // defensively: a malformed directive renders nothing rather than breaking the
1008
+ // prompt (conflictRepairSection also guards on empty base/branch).
1009
+ const conflictRepair = payload.conflictRepair &&
1010
+ typeof payload.conflictRepair === "object" &&
1011
+ typeof payload.conflictRepair.baseBranch === "string" &&
1012
+ typeof payload.conflictRepair.branch === "string"
1013
+ ? {
1014
+ baseBranch: payload.conflictRepair.baseBranch,
1015
+ branch: payload.conflictRepair.branch,
1016
+ files: Array.isArray(payload.conflictRepair.files)
1017
+ ? payload.conflictRepair.files.filter((f) => typeof f === "string")
1018
+ : [],
1019
+ }
1020
+ : undefined;
865
1021
  let priorConclusion;
866
1022
  let priorVerificationReceipt;
867
1023
  if (retryReason) {
@@ -897,6 +1053,9 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
897
1053
  museDeveloper: isMuse,
898
1054
  guidance: guidance.length ? guidance : undefined,
899
1055
  scopeDecisionNote,
1056
+ conflictRepair,
1057
+ operatorCriteria: hasOperatorCriteria,
1058
+ operatorRepairNote,
900
1059
  });
901
1060
  // NOT-83 review finding: the session is marked `running` (worker-loop.ts) before this
902
1061
  // handler ever runs, so an abort landing during worktree setup/deck-bind above already
@@ -998,6 +1157,9 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
998
1157
  // NOT-278: frozen profile deck → the Muse exec attempt (deck headers + pre-spawn
999
1158
  // verify). Other runtimes carry their deck via mcpConfigPath/mcpEnv and ignore this.
1000
1159
  deckId: snapshot?.deckId ?? null,
1160
+ // NOT-305: opaque per-session Deck correlation UUID → the Muse settings
1161
+ // `x-agent-deck-correlation-id` observability header.
1162
+ deckCorrelationId,
1001
1163
  prompt,
1002
1164
  cwd: worktreePath,
1003
1165
  timeoutMs: developerEffectConfig.sessionTimeoutMs,
@@ -1069,6 +1231,18 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
1069
1231
  // here on (usage, receipt, push, PR verify, checks poll) is post-spawn and must
1070
1232
  // not be mislabeled as a session that could not start.
1071
1233
  sessionStarted = true;
1234
+ // NOT-307: stall evidence for every Muse session (timed out or not) — merged
1235
+ // into metadata_json while the row is still running, so the classifier and the
1236
+ // operator can read last-activity evidence whatever the outcome. Best-effort:
1237
+ // evidence must never fail the attempt it describes.
1238
+ if (spawned.muse) {
1239
+ try {
1240
+ mergeSessionMetadata(sessionId, museStallMetadata(spawned.muse));
1241
+ }
1242
+ catch {
1243
+ // ignore
1244
+ }
1245
+ }
1072
1246
  // NOT-169: exactly one agent.completed per spawned process, at child exit and before
1073
1247
  // any receipt mining, usage extraction, or validation. Raw outcome only — no
1074
1248
  // failure classification.
@@ -1094,6 +1268,26 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
1094
1268
  // ignore
1095
1269
  }
1096
1270
  }
1271
+ // NOT-308 safety net: a Muse session that left the worktree dirty without a
1272
+ // single shell tool call is what a shell-less build looks like from the outside —
1273
+ // when the running version is not yet the confirmed baseline, re-verify it in the
1274
+ // background immediately (bypassing the error backoff) and escalate a `missing`
1275
+ // verdict on this issue. Fire-and-forget: never throws, never changes this outcome.
1276
+ // Dirtiness is read here, before salvage/commit handling below can clean the tree.
1277
+ if (runtime === "muse_code") {
1278
+ try {
1279
+ const dirtyAtSessionEnd = !(await isWorktreeClean(worktreePath).catch(() => true));
1280
+ museCapabilitySafetyNetAfterSession({
1281
+ issueId: issue.id,
1282
+ runtime,
1283
+ logPath: spawned.logPath,
1284
+ dirty: dirtyAtSessionEnd,
1285
+ });
1286
+ }
1287
+ catch {
1288
+ // Observational only — the session outcome stands on its own.
1289
+ }
1290
+ }
1097
1291
  // NOT-130: record suite evidence even when the session later fails/times out — an
1098
1292
  // interrupted-but-verified tip must carry the receipt into the retry prompt.
1099
1293
  const verificationReceipt = await persistVerificationReceiptIfAny({
@@ -1287,6 +1481,7 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
1287
1481
  timedOut: Boolean(spawned.timedOut),
1288
1482
  logPath: spawned.logPath,
1289
1483
  runtime,
1484
+ idle: idleCrashInfoFor(spawned),
1290
1485
  });
1291
1486
  const salvageNote = `Salvaged uncommitted work as ${salvaged.message} (${salvaged.commitSha.slice(0, 7)}).`;
1292
1487
  const crashOutcome = await crashOrTimeoutOutcome({
@@ -1341,6 +1536,7 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
1341
1536
  timedOut: Boolean(spawned.timedOut),
1342
1537
  logPath: spawned.logPath,
1343
1538
  runtime,
1539
+ idle: idleCrashInfoFor(spawned),
1344
1540
  }),
1345
1541
  worktreePath,
1346
1542
  logPath: spawned.logPath,
@@ -1536,7 +1732,8 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
1536
1732
  prNumber: prView.number,
1537
1733
  headSha: prView.headRefOid,
1538
1734
  });
1539
- const checks = await pollPrChecks(deps.github, {
1735
+ const pollStartedAt = new Date().toISOString();
1736
+ const poll = await pollPrChecksDetailed(deps.github, {
1540
1737
  cwd: worktreePath,
1541
1738
  timeoutMs: developerEffectConfig.checksPollTimeoutMs,
1542
1739
  intervalMs: developerEffectConfig.checksPollIntervalMs,
@@ -1546,10 +1743,27 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
1546
1743
  // back to a bare `gh pr view` either).
1547
1744
  number: prView.number,
1548
1745
  });
1746
+ // NOT-311: pending/queued CI at the timeout (or no checks yet with a
1747
+ // `pull_request` workflow on the base) waits with backoff instead of spending
1748
+ // an infra attempt — resolved here so the milestone carries the wait reason.
1749
+ const wait = await resolveChecksWait({
1750
+ github: deps.github,
1751
+ cwd: worktreePath,
1752
+ prNumber: prView.number,
1753
+ baseRef: prView.baseRefName,
1754
+ headSha: prView.headRefOid,
1755
+ branch: branchName,
1756
+ poll,
1757
+ signal: ctx.signal,
1758
+ payloadJson: workItem.payloadJson,
1759
+ pollStartedAt,
1760
+ });
1761
+ const checks = poll.result;
1549
1762
  milestone("checks.completed", `Developer · checks ${checks}`, {
1550
1763
  snapshot: checks,
1551
1764
  prNumber: prView.number,
1552
1765
  headSha: prView.headRefOid,
1766
+ ...(wait.milestoneReason ? { reason: wait.milestoneReason } : {}),
1553
1767
  });
1554
1768
  // The poll can run for up to checksPollTimeoutMs (default 10 minutes) — re-fetch and
1555
1769
  // re-validate identity against the SAME localHead before trusting either the checks
@@ -1597,6 +1811,12 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
1597
1811
  author: "system",
1598
1812
  content: { snapshot: checks, prNumber: prView.number, headSha: prView.headRefOid },
1599
1813
  });
1814
+ // NOT-311: still-waiting CI defers with backoff (no attempt spent) instead of
1815
+ // timing out the session. An aborted wait keeps today's `timed_out`.
1816
+ if (wait.pending) {
1817
+ await bestEffortRemove(repoPath, worktreePath);
1818
+ return wait.pending;
1819
+ }
1600
1820
  if (checks === "timeout") {
1601
1821
  await bestEffortRemove(repoPath, worktreePath);
1602
1822
  return { kind: "timed_out" };