agent-dealer 1.2.8 → 1.2.10

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/bundle/server/dist/adapters/github.js +93 -4
  2. package/bundle/server/dist/adapters/github.test.js +96 -1
  3. package/bundle/server/dist/adapters/worktree-deps.js +96 -0
  4. package/bundle/server/dist/adapters/worktree-deps.test.js +144 -0
  5. package/bundle/server/dist/coordinator/auto-merge.js +205 -1
  6. package/bundle/server/dist/coordinator/checks-wait-config.js +19 -0
  7. package/bundle/server/dist/coordinator/commands.js +341 -18
  8. package/bundle/server/dist/coordinator/commands.test.js +47 -0
  9. package/bundle/server/dist/coordinator/developer-effect.js +237 -3
  10. package/bundle/server/dist/coordinator/developer-effect.test.js +460 -3
  11. package/bundle/server/dist/coordinator/human-resolution.js +19 -0
  12. package/bundle/server/dist/coordinator/human-resolution.test.js +33 -0
  13. package/bundle/server/dist/coordinator/merge-conflict-sync.js +100 -12
  14. package/bundle/server/dist/coordinator/merge-conflict-sync.test.js +190 -0
  15. package/bundle/server/dist/coordinator/muse-spawn.js +22 -4
  16. package/bundle/server/dist/coordinator/muse-spawn.test.js +166 -10
  17. package/bundle/server/dist/coordinator/operator-criteria.js +168 -0
  18. package/bundle/server/dist/coordinator/operator-criteria.test.js +78 -0
  19. package/bundle/server/dist/coordinator/operator-verification.integration.test.js +347 -0
  20. package/bundle/server/dist/coordinator/playbook-feedback.js +3 -0
  21. package/bundle/server/dist/coordinator/projection.js +10 -4
  22. package/bundle/server/dist/coordinator/projection.test.js +13 -0
  23. package/bundle/server/dist/coordinator/prompts.js +134 -1
  24. package/bundle/server/dist/coordinator/prompts.test.js +148 -0
  25. package/bundle/server/dist/coordinator/recovery.js +57 -3
  26. package/bundle/server/dist/coordinator/recovery.test.js +67 -8
  27. package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +1 -0
  28. package/bundle/server/dist/coordinator/reviewer-effect.js +6 -0
  29. package/bundle/server/dist/coordinator/routing.js +55 -6
  30. package/bundle/server/dist/coordinator/routing.test.js +62 -15
  31. package/bundle/server/dist/coordinator/session-timeouts.js +22 -0
  32. package/bundle/server/dist/coordinator/usage-cap-defer.js +61 -0
  33. package/bundle/server/dist/db/ci-attempts-migration.test.js +70 -0
  34. package/bundle/server/dist/db/index.js +13 -0
  35. package/bundle/server/dist/db/schema.sql +2 -0
  36. package/bundle/server/dist/repository/issues.js +39 -3
  37. package/bundle/server/dist/repository/work-items.js +7 -0
  38. package/bundle/server/dist/runners/muse-code-args.js +25 -0
  39. package/bundle/server/dist/runners/muse-code-args.test.js +30 -0
  40. package/bundle/server/dist/runners/muse-code-jsonl.js +61 -0
  41. package/bundle/server/dist/runners/muse-code.test.js +13 -1
  42. package/bundle/server/dist/runners/spawn-cli.js +93 -20
  43. package/bundle/server/dist/runners/spawn-cli.test.js +38 -0
  44. package/bundle/server/package.json +2 -2
  45. package/bundle/server/static-ui/assets/index-CU6fNntg.js +64 -0
  46. package/bundle/server/static-ui/index.html +1 -1
  47. package/bundle/shared/dist/human-actions.d.ts +4 -4
  48. package/bundle/shared/dist/human-actions.js +8 -0
  49. package/bundle/shared/dist/issues.d.ts +30 -0
  50. package/bundle/shared/dist/issues.js +14 -0
  51. package/bundle/shared/dist/issues.test.js +2 -0
  52. package/bundle/shared/dist/workflow.d.ts +4 -4
  53. package/bundle/shared/dist/workflow.js +6 -0
  54. package/bundle/shared/dist/workflow.test.js +1 -0
  55. package/bundle/shared/package.json +1 -1
  56. package/dist/action.js +4 -1
  57. package/dist/action.test.js +42 -0
  58. package/dist/index.js +1 -1
  59. package/package.json +1 -1
  60. package/bundle/server/static-ui/assets/index-B6SVCzMR.js +0 -64
@@ -17,16 +17,20 @@ import fs from "node:fs";
17
17
  import os from "node:os";
18
18
  import path from "node:path";
19
19
  import { parsePhaseBudget, parseProfileSnapshot, roleCeiling } from "@agent-dealer/shared";
20
+ import { runMergeConflictSync } from "./merge-conflict-sync.js";
20
21
  import { getTaskSnapshot } from "./commands.js";
22
+ import { extractOperatorCriteria } from "./operator-criteria.js";
21
23
  import { buildDeveloperPrompt } from "./prompts.js";
22
24
  import { guidanceForNextSession } from "./guidance.js";
23
25
  import { realDeveloperSpawn, developerSessionLogPath } from "./spawn.js";
26
+ import { ensureWorktreeDeps, worktreeDepsTimeoutMs, } from "../adapters/worktree-deps.js";
24
27
  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
28
  import { recordIssueBaseSha } from "../repository/issues.js";
26
29
  import { ensureIssueRepoCheckout, roleWorktreePathForResolution, resolveCheckoutBaseBranch, } from "../adapters/managed-repo.js";
27
30
  import { baseRefCandidates, inspectBranchProgress } from "./branch-progress.js";
28
31
  import { prepareWorkerDeckConnection, releaseWorkerDeckConnection, verifyWorkerDeckConnection } from "../adapters/agent-deck-bind.js";
29
- import { realGithubAdapter, pollPrChecks } from "../adapters/github.js";
32
+ import { realGithubAdapter, pollPrChecksDetailed, } from "../adapters/github.js";
33
+ import { checksWaitCeilingExceeded, checksWaitCeilingMs } from "./checks-wait-config.js";
30
34
  import { museCapabilitySafetyNetAfterSession } from "../adapters/muse-capability.js";
31
35
  import { getOrAssignSessionCorrelationId, getWorkerSession, mergeSessionMetadata, patchRunningSession, recordSessionProcess, setSessionInputSha } from "../repository/worker-sessions.js";
32
36
  import { museIdleMinutes, museStallMetadata } from "./muse-spawn.js";
@@ -117,10 +121,18 @@ export const developerEffectConfig = {
117
121
  get checksPollIntervalMs() {
118
122
  return num("CHECKS_POLL_INTERVAL_MS", 15_000);
119
123
  },
124
+ /** NOT-311: overall CI-wait ceiling measured from the first poll, across deferrals. */
125
+ get checksWaitCeilingMs() {
126
+ return checksWaitCeilingMs();
127
+ },
120
128
  /** NOT-197: bound for the pre-branch `git fetch origin <base>` on a fresh issue branch. */
121
129
  get baseFetchTimeoutMs() {
122
130
  return num("BASE_FETCH_TIMEOUT_MS", DEFAULT_BASE_FETCH_TIMEOUT_MS);
123
131
  },
132
+ /** NOT-315: bound for `npm ci` in the fresh worktree (coordinator side, with network). */
133
+ get worktreeDepsTimeoutMs() {
134
+ return worktreeDepsTimeoutMs();
135
+ },
124
136
  /** NOT-110: bounded window to let a lagging `gh pr view` catch up to a just-pushed HEAD. */
125
137
  get headReconcileTimeoutMs() {
126
138
  return num("HEAD_RECONCILE_TIMEOUT_MS", 30_000);
@@ -290,6 +302,138 @@ async function checksFailedOutcome(opts) {
290
302
  });
291
303
  return details ? { kind: "checks_failed", details } : { kind: "checks_failed" };
292
304
  }
305
+ /**
306
+ * NOT-311: the `checksWaitStartedAt` persisted by a prior `checks_pending` deferral,
307
+ * anchoring the overall wait ceiling across re-polls. Null on a first wait (or an
308
+ * unparseable payload) — the caller then anchors at its own poll start.
309
+ */
310
+ function checksWaitStartedAtFromPayload(payloadJson) {
311
+ if (!payloadJson)
312
+ return null;
313
+ try {
314
+ const v = JSON.parse(payloadJson).checksWaitStartedAt;
315
+ return typeof v === "string" && v ? v : null;
316
+ }
317
+ catch {
318
+ return null;
319
+ }
320
+ }
321
+ /**
322
+ * NOT-311: decide what a finished checks poll means. Returns a `checks_pending`
323
+ * outcome when CI is still queued/running at the timeout (or reports no checks while
324
+ * the base branch has a `pull_request` workflow — "not yet registered", never "no
325
+ * CI"), and null when the caller stays on today's path (success, failure, an aborted
326
+ * wait, or `none` with no workflow configured). Also yields the `checks.completed`
327
+ * milestone reason for the wait cases.
328
+ *
329
+ * A missing/unknown workflow answer (no adapter method, unreadable base ref) waits
330
+ * fail-closed: sending an unverified PR to review is the outcome this exists to
331
+ * prevent, and the wait ceiling still bounds it.
332
+ *
333
+ * NOT-354: "no checks" on a CONFLICTING PR is not a queued wait — GitHub creates no
334
+ * merge ref, so the workflow never starts. That case returns `conflicting` (and the
335
+ * `conflicting` milestone reason) so the caller runs the base sync instead of
336
+ * deferring. UNKNOWN mergeability is "not computed yet" and keeps today's wait.
337
+ */
338
+ async function resolveChecksWait(opts) {
339
+ const waitStartedAt = checksWaitStartedAtFromPayload(opts.payloadJson) ?? opts.pollStartedAt;
340
+ const base = {
341
+ kind: "checks_pending",
342
+ branch: opts.branch,
343
+ prNumber: opts.prNumber,
344
+ headSha: opts.headSha,
345
+ waitStartedAt,
346
+ };
347
+ const noChecks = opts.poll.result === "none" || (opts.poll.result === "timeout" && opts.poll.lastSnapshot === "none");
348
+ if (noChecks) {
349
+ let hasWorkflow = null;
350
+ try {
351
+ hasWorkflow = (await opts.github.hasPullRequestWorkflow?.({ cwd: opts.cwd, baseRef: opts.baseRef })) ?? null;
352
+ }
353
+ catch {
354
+ hasWorkflow = null;
355
+ }
356
+ if (hasWorkflow === false) {
357
+ return { pending: null, milestoneReason: "none_no_workflow" };
358
+ }
359
+ let mergeable = "UNKNOWN";
360
+ try {
361
+ mergeable = (await opts.github.prMergeableState?.({ cwd: opts.cwd, number: opts.prNumber })) ?? "UNKNOWN";
362
+ }
363
+ catch {
364
+ mergeable = "UNKNOWN";
365
+ }
366
+ if (mergeable === "CONFLICTING") {
367
+ return { pending: null, milestoneReason: "conflicting", conflicting: { waitStartedAt } };
368
+ }
369
+ return {
370
+ pending: { ...base, reason: "Waiting for CI (queued) - no attempt spent" },
371
+ milestoneReason: "none_with_workflow",
372
+ };
373
+ }
374
+ if (opts.poll.result === "timeout" && opts.poll.lastSnapshot === "pending" && !opts.poll.aborted && !opts.signal?.aborted) {
375
+ const milestoneReason = checksWaitCeilingExceeded(waitStartedAt) ? "pending_ceiling" : undefined;
376
+ return {
377
+ pending: { ...base, reason: "Waiting for CI (running) - no attempt spent" },
378
+ ...(milestoneReason ? { milestoneReason } : {}),
379
+ };
380
+ }
381
+ return { pending: null };
382
+ }
383
+ /**
384
+ * NOT-354: a conflicting PR's CI wait runs the NOT-310 base sync (`checks_wait`
385
+ * entry: plain push, never force/rebase, one sync + one repair round per episode)
386
+ * from the effect, which still holds the lease. applyCompletion acts on the
387
+ * result. A refusal (`skipped`) becomes the single escalation, never a wait.
388
+ */
389
+ async function checksConflictOutcome(ctx, deps, opts) {
390
+ const sync = deps.conflictSync ?? runMergeConflictSync;
391
+ const conflictReason = `PR #${opts.prNumber} conflicts with ${opts.baseBranch} (mergeable: CONFLICTING), ` +
392
+ `so its CI cannot start.`;
393
+ const base = {
394
+ kind: "checks_conflicting",
395
+ branch: opts.branch,
396
+ baseBranch: opts.baseBranch,
397
+ prNumber: opts.prNumber,
398
+ headSha: opts.headSha,
399
+ waitStartedAt: opts.waitStartedAt,
400
+ };
401
+ let result;
402
+ try {
403
+ result = await sync({
404
+ issueId: ctx.issue.id,
405
+ instanceId: ctx.instance.id,
406
+ repo: ctx.issue.repo,
407
+ branch: opts.branch,
408
+ baseBranch: opts.baseBranch,
409
+ prNumber: opts.prNumber,
410
+ mergeReason: conflictReason,
411
+ entry: "checks_wait",
412
+ });
413
+ }
414
+ catch (err) {
415
+ const detail = err instanceof Error ? err.message : String(err);
416
+ result = { outcome: "skipped", reason: `base sync errored: ${detail}` };
417
+ }
418
+ switch (result.outcome) {
419
+ case "synced":
420
+ return { ...base, sync: { result: "synced", headSha: result.headSha } };
421
+ case "repair_needed":
422
+ return { ...base, sync: { result: "repair", files: result.files } };
423
+ case "escalate":
424
+ return { ...base, sync: { result: "escalate", reason: result.reason, evidence: result.evidence } };
425
+ default:
426
+ // skipped (or a merge-only outcome that cannot occur on this entry).
427
+ return {
428
+ ...base,
429
+ sync: {
430
+ result: "escalate",
431
+ reason: `${conflictReason} Dealer could not sync ${opts.baseBranch}: ${result.outcome === "skipped" ? result.reason : result.outcome}`,
432
+ evidence: { checksConflict: true },
433
+ },
434
+ };
435
+ }
436
+ }
293
437
  /**
294
438
  * Infra retry that skips the agent: publish whatever the branch already carries. Uses the
295
439
  * issue repo as `gh` cwd and `origin/<branch>` as the verified head (any worktree was
@@ -528,17 +672,35 @@ async function runPublishOnlyHandoff(ctx, deps, branchName) {
528
672
  prNumber: prView.number,
529
673
  headSha: prView.headRefOid,
530
674
  });
531
- const checks = await pollPrChecks(deps.github, {
675
+ const pollStartedAt = new Date().toISOString();
676
+ const poll = await pollPrChecksDetailed(deps.github, {
532
677
  cwd,
533
678
  timeoutMs: developerEffectConfig.checksPollTimeoutMs,
534
679
  intervalMs: developerEffectConfig.checksPollIntervalMs,
535
680
  signal: ctx.signal,
536
681
  number: prView.number,
537
682
  });
683
+ // NOT-311: pending/queued CI at the timeout (or no checks yet with a
684
+ // `pull_request` workflow on the base) waits with backoff instead of spending
685
+ // an infra attempt — resolved here so the milestone carries the wait reason.
686
+ const wait = await resolveChecksWait({
687
+ github: deps.github,
688
+ cwd,
689
+ prNumber: prView.number,
690
+ baseRef: prView.baseRefName,
691
+ headSha: prView.headRefOid,
692
+ branch: branchName,
693
+ poll,
694
+ signal: ctx.signal,
695
+ payloadJson: workItem.payloadJson,
696
+ pollStartedAt,
697
+ });
698
+ const checks = poll.result;
538
699
  milestone("checks.completed", `Developer · checks ${checks}`, {
539
700
  snapshot: checks,
540
701
  prNumber: prView.number,
541
702
  headSha: prView.headRefOid,
703
+ ...(wait.milestoneReason ? { reason: wait.milestoneReason } : {}),
542
704
  });
543
705
  const postPollView = await deps.github.viewPr({ cwd, branch: branchName });
544
706
  if (!postPollView) {
@@ -575,6 +737,20 @@ async function runPublishOnlyHandoff(ctx, deps, branchName) {
575
737
  author: "system",
576
738
  content: { snapshot: checks, prNumber: prView.number, headSha: prView.headRefOid },
577
739
  });
740
+ // NOT-354: a conflicting PR never gets checks — sync the base instead of waiting.
741
+ if (wait.conflicting) {
742
+ return checksConflictOutcome(ctx, deps, {
743
+ branch: branchName,
744
+ baseBranch: prView.baseRefName,
745
+ prNumber: prView.number,
746
+ headSha: prView.headRefOid,
747
+ waitStartedAt: wait.conflicting.waitStartedAt,
748
+ });
749
+ }
750
+ // NOT-311: still-waiting CI defers with backoff (no attempt spent) instead of
751
+ // timing out the session. An aborted wait keeps today's `timed_out`.
752
+ if (wait.pending)
753
+ return wait.pending;
578
754
  if (checks === "timeout")
579
755
  return { kind: "timed_out" };
580
756
  await fetchRef(cwd, prView.baseRefName);
@@ -773,6 +949,18 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
773
949
  recordIssueBaseSha(issue.id, resolved.baseSha);
774
950
  issue.baseSha = resolved.baseSha;
775
951
  }
952
+ // NOT-315: provision dependencies here, outside the sandbox, with network — the
953
+ // builder sandbox has none, so an in-session install can never work. A throw
954
+ // lands in the worktree-setup adapter_failure below: visible and
955
+ // infra-retryable, never a silent hour-long stall. The session is not spawned.
956
+ const depsResult = await ensureWorktreeDeps(worktreePath, {
957
+ runner: deps.depsRunner,
958
+ timeoutMs: developerEffectConfig.worktreeDepsTimeoutMs,
959
+ });
960
+ milestone("worktree.deps_ready", `Developer · dependencies ready (round ${round})`, {
961
+ ...(depsResult.ran ? { ran: true } : { ran: false, reason: depsResult.reason }),
962
+ durationMs: depsResult.durationMs,
963
+ });
776
964
  }
777
965
  catch (err) {
778
966
  return { kind: "adapter_failure", reason: `worktree setup failed: ${String(err)}` };
@@ -886,6 +1074,14 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
886
1074
  const scopeDecisionNote = typeof payload.scopeDecisionNote === "string" && payload.scopeDecisionNote.trim()
887
1075
  ? payload.scopeDecisionNote.trim()
888
1076
  : undefined;
1077
+ // NOT-314: same one-shot ride for an operator_verification repair note.
1078
+ const operatorRepairNote = typeof payload.operatorRepairNote === "string" && payload.operatorRepairNote.trim()
1079
+ ? payload.operatorRepairNote.trim()
1080
+ : undefined;
1081
+ // NOT-314: the frozen snapshot's operator criteria, parsed here so the
1082
+ // worker never re-derives them — the section renders only when non-empty.
1083
+ const operatorCriteria = extractOperatorCriteria(taskSnapshot.acceptanceCriteria);
1084
+ const hasOperatorCriteria = operatorCriteria.length > 0 ? operatorCriteria : undefined;
889
1085
  // NOT-310: the conflict directive rides only the work item queued by the merge
890
1086
  // path — exactly this round reads it; nothing is re-read from the DB. Shaped
891
1087
  // defensively: a malformed directive renders nothing rather than breaking the
@@ -938,6 +1134,8 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
938
1134
  guidance: guidance.length ? guidance : undefined,
939
1135
  scopeDecisionNote,
940
1136
  conflictRepair,
1137
+ operatorCriteria: hasOperatorCriteria,
1138
+ operatorRepairNote,
941
1139
  });
942
1140
  // NOT-83 review finding: the session is marked `running` (worker-loop.ts) before this
943
1141
  // handler ever runs, so an abort landing during worktree setup/deck-bind above already
@@ -1614,7 +1812,8 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
1614
1812
  prNumber: prView.number,
1615
1813
  headSha: prView.headRefOid,
1616
1814
  });
1617
- const checks = await pollPrChecks(deps.github, {
1815
+ const pollStartedAt = new Date().toISOString();
1816
+ const poll = await pollPrChecksDetailed(deps.github, {
1618
1817
  cwd: worktreePath,
1619
1818
  timeoutMs: developerEffectConfig.checksPollTimeoutMs,
1620
1819
  intervalMs: developerEffectConfig.checksPollIntervalMs,
@@ -1624,10 +1823,27 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
1624
1823
  // back to a bare `gh pr view` either).
1625
1824
  number: prView.number,
1626
1825
  });
1826
+ // NOT-311: pending/queued CI at the timeout (or no checks yet with a
1827
+ // `pull_request` workflow on the base) waits with backoff instead of spending
1828
+ // an infra attempt — resolved here so the milestone carries the wait reason.
1829
+ const wait = await resolveChecksWait({
1830
+ github: deps.github,
1831
+ cwd: worktreePath,
1832
+ prNumber: prView.number,
1833
+ baseRef: prView.baseRefName,
1834
+ headSha: prView.headRefOid,
1835
+ branch: branchName,
1836
+ poll,
1837
+ signal: ctx.signal,
1838
+ payloadJson: workItem.payloadJson,
1839
+ pollStartedAt,
1840
+ });
1841
+ const checks = poll.result;
1627
1842
  milestone("checks.completed", `Developer · checks ${checks}`, {
1628
1843
  snapshot: checks,
1629
1844
  prNumber: prView.number,
1630
1845
  headSha: prView.headRefOid,
1846
+ ...(wait.milestoneReason ? { reason: wait.milestoneReason } : {}),
1631
1847
  });
1632
1848
  // The poll can run for up to checksPollTimeoutMs (default 10 minutes) — re-fetch and
1633
1849
  // re-validate identity against the SAME localHead before trusting either the checks
@@ -1675,6 +1891,24 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
1675
1891
  author: "system",
1676
1892
  content: { snapshot: checks, prNumber: prView.number, headSha: prView.headRefOid },
1677
1893
  });
1894
+ // NOT-354: a conflicting PR never gets checks — sync the base instead of waiting.
1895
+ // The worktree goes first: the sync refuses a branch still checked out elsewhere.
1896
+ if (wait.conflicting) {
1897
+ await bestEffortRemove(repoPath, worktreePath);
1898
+ return checksConflictOutcome(ctx, deps, {
1899
+ branch: branchName,
1900
+ baseBranch: prView.baseRefName,
1901
+ prNumber: prView.number,
1902
+ headSha: prView.headRefOid,
1903
+ waitStartedAt: wait.conflicting.waitStartedAt,
1904
+ });
1905
+ }
1906
+ // NOT-311: still-waiting CI defers with backoff (no attempt spent) instead of
1907
+ // timing out the session. An aborted wait keeps today's `timed_out`.
1908
+ if (wait.pending) {
1909
+ await bestEffortRemove(repoPath, worktreePath);
1910
+ return wait.pending;
1911
+ }
1678
1912
  if (checks === "timeout") {
1679
1913
  await bestEffortRemove(repoPath, worktreePath);
1680
1914
  return { kind: "timed_out" };