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.
- package/bundle/server/dist/adapters/github.js +93 -4
- package/bundle/server/dist/adapters/github.test.js +96 -1
- package/bundle/server/dist/adapters/worktree-deps.js +96 -0
- package/bundle/server/dist/adapters/worktree-deps.test.js +144 -0
- package/bundle/server/dist/coordinator/auto-merge.js +205 -1
- package/bundle/server/dist/coordinator/checks-wait-config.js +19 -0
- package/bundle/server/dist/coordinator/commands.js +341 -18
- package/bundle/server/dist/coordinator/commands.test.js +47 -0
- package/bundle/server/dist/coordinator/developer-effect.js +237 -3
- package/bundle/server/dist/coordinator/developer-effect.test.js +460 -3
- package/bundle/server/dist/coordinator/human-resolution.js +19 -0
- package/bundle/server/dist/coordinator/human-resolution.test.js +33 -0
- package/bundle/server/dist/coordinator/merge-conflict-sync.js +100 -12
- package/bundle/server/dist/coordinator/merge-conflict-sync.test.js +190 -0
- package/bundle/server/dist/coordinator/muse-spawn.js +22 -4
- package/bundle/server/dist/coordinator/muse-spawn.test.js +166 -10
- package/bundle/server/dist/coordinator/operator-criteria.js +168 -0
- package/bundle/server/dist/coordinator/operator-criteria.test.js +78 -0
- package/bundle/server/dist/coordinator/operator-verification.integration.test.js +347 -0
- package/bundle/server/dist/coordinator/playbook-feedback.js +3 -0
- package/bundle/server/dist/coordinator/projection.js +10 -4
- package/bundle/server/dist/coordinator/projection.test.js +13 -0
- package/bundle/server/dist/coordinator/prompts.js +134 -1
- package/bundle/server/dist/coordinator/prompts.test.js +148 -0
- package/bundle/server/dist/coordinator/recovery.js +57 -3
- package/bundle/server/dist/coordinator/recovery.test.js +67 -8
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +1 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +6 -0
- package/bundle/server/dist/coordinator/routing.js +55 -6
- package/bundle/server/dist/coordinator/routing.test.js +62 -15
- package/bundle/server/dist/coordinator/session-timeouts.js +22 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +61 -0
- package/bundle/server/dist/db/ci-attempts-migration.test.js +70 -0
- package/bundle/server/dist/db/index.js +13 -0
- package/bundle/server/dist/db/schema.sql +2 -0
- package/bundle/server/dist/repository/issues.js +39 -3
- package/bundle/server/dist/repository/work-items.js +7 -0
- package/bundle/server/dist/runners/muse-code-args.js +25 -0
- package/bundle/server/dist/runners/muse-code-args.test.js +30 -0
- package/bundle/server/dist/runners/muse-code-jsonl.js +61 -0
- package/bundle/server/dist/runners/muse-code.test.js +13 -1
- package/bundle/server/dist/runners/spawn-cli.js +93 -20
- package/bundle/server/dist/runners/spawn-cli.test.js +38 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-CU6fNntg.js +64 -0
- package/bundle/server/static-ui/index.html +1 -1
- package/bundle/shared/dist/human-actions.d.ts +4 -4
- package/bundle/shared/dist/human-actions.js +8 -0
- package/bundle/shared/dist/issues.d.ts +30 -0
- package/bundle/shared/dist/issues.js +14 -0
- package/bundle/shared/dist/issues.test.js +2 -0
- package/bundle/shared/dist/workflow.d.ts +4 -4
- package/bundle/shared/dist/workflow.js +6 -0
- package/bundle/shared/dist/workflow.test.js +1 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.js +4 -1
- package/dist/action.test.js +42 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- 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,
|
|
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
|
|
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
|
|
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" };
|