⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
attemptLoopReentry in packages/loopover-miner/lib/loop-reentry.ts documents itself (lines 138-142) as: "only when it allows — dequeues the next candidate and transitions run-state to "discovering"". The implementation:
let dequeued: LoopReentryResult["dequeued"] = null;
if (decision.reenter) {
dequeued = portfolioQueue.dequeueNext();
if (runState && typeof runState.setRunState === "function") {
runState.setRunState(repoFullName, "discovering");
}
}
Two defects against that contract:
- Wrong repo.
portfolioQueue.dequeueNext() is portfolio-wide (portfolio-queue.ts:297-303 — the claim is "deliberately global (no api_base_url filter)"), so the item it returns is frequently for a different repo. loop-cli.ts:626-628 then does claimed = reentry.dequeued as QueueEntry and works that repo. But the run-state row flipped to "discovering" is the one for repoFullName — the repo that just finished. The repo actually about to be worked keeps its stale state.
- Unconditional on
dequeued. When the queue is empty, dequeued === null and the finished repo is still parked at "discovering" forever. Nothing in loop-cli.ts:630-634 (the branch that sleeps, re-discovers, and re-dequeues) resets it.
run-state.ts describes this store as "the per-repo discover/plan/prepare signal a 'run portfolio' view folds alongside managed PR rows", so manage status's portfolio view reports phantom in-flight discovery on a repo the loop has already left, and never reports it on the repo it moved to.
Requirements
- The
setRunState(..., "discovering") call must only run when dequeued is non-null.
- When
dequeued is non-null, the repo written must be dequeued.repoFullName (the repo about to be worked), not the just-finished repoFullName.
- The
dequeueNext() call itself, the decision.reenter gate, the audit event append, and the returned LoopReentryResult shape must all be unchanged.
- The docstring at lines 138-143 must be updated to state which repo's run-state is transitioned and that no transition happens when the queue is empty.
⚠️ Required pattern: keep the existing if (runState && typeof runState.setRunState === "function") duck-typed guard exactly as-is and only tighten its condition + argument. It does NOT satisfy this issue to also write a second run-state row for the finished repo, to add a new "idle" transition, to move the transition into loop-cli.ts, or to change dequeueNext() into a repo-scoped dequeue.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding the dequeued null-guard while still writing the finished repo's name — does not resolve this issue.
Test Coverage Requirements
packages/loopover-miner/lib/**/*.ts IS inside Codecov's coverage.include in vitest.config.ts, so the 99%+ branch-counted codecov/patch gate applies exactly as for src/**. Both arms of the new dequeued guard need a test (dequeued / not dequeued), as does the existing decision.reenter arm. The fix needs a named regression test that fails against the current code.
Expected Outcome
loopover-miner state get <repo> and manage status's portfolio view report "discovering" on the repo the loop is actually about to work, and stop parking a finished repo in "discovering" when the queue runs dry.
Links & Resources
packages/loopover-miner/lib/loop-reentry.ts:138-190, packages/loopover-miner/lib/loop-cli.ts:602-634, packages/loopover-miner/lib/portfolio-queue.ts:289-303, packages/loopover-miner/lib/run-state.ts.
Context
attemptLoopReentryinpackages/loopover-miner/lib/loop-reentry.tsdocuments itself (lines 138-142) as: "only when it allows — dequeues the next candidate and transitions run-state to"discovering"". The implementation:Two defects against that contract:
portfolioQueue.dequeueNext()is portfolio-wide (portfolio-queue.ts:297-303— the claim is "deliberately global (no api_base_url filter)"), so the item it returns is frequently for a different repo.loop-cli.ts:626-628then doesclaimed = reentry.dequeued as QueueEntryand works that repo. But the run-state row flipped to"discovering"is the one forrepoFullName— the repo that just finished. The repo actually about to be worked keeps its stale state.dequeued. When the queue is empty,dequeued === nulland the finished repo is still parked at"discovering"forever. Nothing inloop-cli.ts:630-634(the branch that sleeps, re-discovers, and re-dequeues) resets it.run-state.tsdescribes this store as "the per-repo discover/plan/prepare signal a 'run portfolio' view folds alongside managed PR rows", somanage status's portfolio view reports phantom in-flight discovery on a repo the loop has already left, and never reports it on the repo it moved to.Requirements
setRunState(..., "discovering")call must only run whendequeuedis non-null.dequeuedis non-null, the repo written must bedequeued.repoFullName(the repo about to be worked), not the just-finishedrepoFullName.dequeueNext()call itself, thedecision.reentergate, the audit event append, and the returnedLoopReentryResultshape must all be unchanged.Deliverables
attemptLoopReentrycallsrunState.setRunState(dequeued.repoFullName, "discovering")and only whendequeuedis non-null.dequeued.repoFullNameas the transitioned repo and states the empty-queue no-op.test/unit/miner-loop-reentry.test.tsasserts that whendequeueNext()returns an item for repob/bwhile the candidate's repo isa/a, the injected run-state store receivessetRunState("b/b", "discovering")and neversetRunState("a/a", ...).dequeueNext()returnsnullanddecision.reenteris true,setRunStateis never called.decision.reenteris false,setRunStateis still never called (unchanged behaviour).All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding the
dequeuednull-guard while still writing the finished repo's name — does not resolve this issue.Test Coverage Requirements
packages/loopover-miner/lib/**/*.tsIS inside Codecov'scoverage.includeinvitest.config.ts, so the 99%+ branch-countedcodecov/patchgate applies exactly as forsrc/**. Both arms of the newdequeuedguard need a test (dequeued / not dequeued), as does the existingdecision.reenterarm. The fix needs a named regression test that fails against the current code.Expected Outcome
loopover-miner state get <repo>andmanage status's portfolio view report"discovering"on the repo the loop is actually about to work, and stop parking a finished repo in"discovering"when the queue runs dry.Links & Resources
packages/loopover-miner/lib/loop-reentry.ts:138-190,packages/loopover-miner/lib/loop-cli.ts:602-634,packages/loopover-miner/lib/portfolio-queue.ts:289-303,packages/loopover-miner/lib/run-state.ts.