⚠️ 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
detectRepoStack infers a Node repo's validation commands by exact script name first, then by pattern —
packages/loopover-miner/lib/stack-detection.ts:103-108:
/** Pick a package.json script by exact name first, then by pattern, considering only string-valued scripts. */
function pickScript(scripts: any, exactName: any, pattern: any) {
const names = Object.keys(scripts).filter((name) => typeof scripts[name] === "string");
if (names.includes(exactName)) return exactName;
return names.find((name) => pattern.test(name)) ?? null;
}
with the patterns at packages/loopover-miner/lib/stack-detection.ts:138-141:
const buildName = pickScript(scripts, "build", /^(build|compile|bundle)(:|$)/i);
const testName = pickScript(scripts, "test", /(^|:)test(:|$)/i);
const lintName = pickScript(scripts, "lint", /(^|:)lint(:|$)/i);
const formatName = pickScript(scripts, "format", /(^|:)(format|fmt)(:|$)/i);
Those patterns match <name>:<anything>. There is no exclusion of write-mode or non-terminating variants, and the
fallback is names.find(...) — the first Object.keys (insertion-order) match, with no preference among
several candidates. So for a package.json whose scripts are:
{ "test:watch": "vitest" } → testCommand: "npm run test:watch"
{ "build:watch": "tsc -w" } → buildCommand: "npm run build:watch"
{ "lint:fix": "eslint --fix ." } → lintCommand: "npm run lint:fix"
{ "test:update": "vitest -u" } → testCommand: "npm run test:update"
The file's own header (packages/loopover-miner/lib/stack-detection.ts:1-7) promises exactly the opposite posture:
"a repo whose stack can't be confidently identified returns an explicit { detected: false, reason } instead of
guessing."
These are not display-only strings. runTargetRepoVerification executes them in the attempt's real worktree —
packages/loopover-miner/lib/target-repo-verification.ts:117-140:
const commands: Array<{ kind: TargetRepoVerificationCheck["kind"]; command: string | null }> = [
{ kind: "test", command: stack.testCommand },
{ kind: "lint", command: stack.lintCommand },
{ kind: "build", command: stack.buildCommand },
];
...
for (const { kind, command } of runnable) {
const { code, output } = await spawn(command, { cwd: options.worktreeDir, timeoutMs });
and attempt-cli.ts binds it to the live worktree at packages/loopover-miner/lib/attempt-cli.ts:900-906:
verifyTargetRepo: () =>
(options.runTargetRepoVerification ?? runTargetRepoVerification)({
worktreeDir: attemptWorktreePath,
stack: detectRepoStack(attemptWorktreePath),
}),
Two concrete harms:
- A
:watch command never exits. defaultVerificationSpawn bounds each command at
DEFAULT_VERIFICATION_TIMEOUT_MS = 10 * 60 * 1000
(packages/loopover-miner/lib/target-repo-verification.ts:41), after which the process tree is killed and the
command resolves code: null → ok: code === 0 is false
(packages/loopover-miner/lib/target-repo-verification.ts:138) → the gate returns
{ status: "failed", … } → runMinerAttempt returns verification_failed
(packages/loopover-miner/lib/attempt-runner.ts:228-233) and the PR is blocked. A repo whose only test script
is test:watch therefore costs 10 minutes of wall clock and then fails every attempt, permanently.
- A
:fix command mutates the worktree. runTargetRepoVerification runs after the iterate loop, after
self-review, and immediately before the submission gate
(packages/loopover-miner/lib/attempt-runner.ts:223-233). npm run lint:fix rewrites files in the worktree at
that point, so the working tree no longer matches the changed-file set the handoff packet and self-review
scored. The module's own header calls itself the "independent quality gate" that "run[s] the TARGET
repository's own detected test/lint/build commands"
(packages/loopover-miner/lib/target-repo-verification.ts:1-12) — a check, not a mutation.
The gate already has a documented, correct fallback for "detection came up empty": it SKIPS, never fails —
packages/loopover-miner/lib/target-repo-verification.ts:11-13 and :121-123
(if (runnable.length === 0) return { status: "skipped", reason: "no_commands_detected" };). That is the right
disposition when the only candidate script is non-terminating or write-mode.
Requirements
pickScript's pattern fallback must not select a script name that denotes a watch/non-terminating variant
(watch, dev, serve) or a write-mode variant (fix, write, update, u) in any :-delimited segment
after the base name. Matching must be case-insensitive and must only consider whole :-delimited segments, so a
script literally named lint or test is unaffected and a name like test:fixtures is NOT excluded
(fixtures is not fix).
- When every pattern candidate is excluded, the corresponding command must be
null — never a fallback guess —
so runTargetRepoVerification reaches its existing no_commands_detected skip instead of running or failing.
- Exact-name selection must be unchanged: a
"test", "lint", "build" or "format" script always wins
outright, exactly as today at packages/loopover-miner/lib/stack-detection.ts:106.
- When several non-excluded pattern candidates exist, selection must be deterministic and independent of
package.json key order — pick the lexicographically smallest candidate name. Today's names.find(...) returns
whichever key happens to come first, so { "test:e2e": …, "test:unit": … } and the same object with the keys
swapped currently produce different testCommand values for the same repo.
- Do NOT change the four patterns' base vocabulary (
build|compile|bundle, test, lint, format|fmt), the
detector precedence order in DETECTORS (packages/loopover-miner/lib/stack-detection.ts:245), the
packageManager resolution, or renderStackSummary's output format.
- Do NOT change
runTargetRepoVerification, its command ordering, its timeout, or its skip reasons.
⚠️ Required pattern: keep the fix inside pickScript
(packages/loopover-miner/lib/stack-detection.ts:103-108), which all four command selections already funnel
through — one exclusion list plus a deterministic pick, applied uniformly. What does NOT satisfy this issue:
(a) filtering the commands inside runTargetRepoVerification instead, which leaves detectRepoStack still
reporting npm run lint:fix as the repo's lint command to every other consumer (including the validation
guidance rendered into the coding-task spec); (b) a substring blocklist that also drops legitimate names like
test:fixtures or build:updates; (c) raising or removing DEFAULT_VERIFICATION_TIMEOUT_MS so a :watch
command "only" wastes less time.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example one that
excludes :watch but still selects lint:fix, or one that fixes exclusion but leaves names.find(...)'s
key-order dependence — does not resolve this issue.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted. vitest.config.ts's coverage.include lists
packages/loopover-miner/lib/**/*.ts, so stack-detection.ts and target-repo-verification.ts are measured and
gated. Every branch the change introduces needs both arms tested: the exact-name hit vs miss in pickScript; the
exclusion predicate matching vs not matching (cover watch, dev, serve, fix, write, update, u, and at
least one near-miss such as fixtures); the "some candidate survives" vs "all candidates excluded → null" arms;
and the deterministic-pick comparison with both one candidate and several. Each of the four pickScript call
sites at packages/loopover-miner/lib/stack-detection.ts:138-141 must exercise the null arm, since each feeds a
separate ternary at :146-150.
Expected Outcome
The target-repo verification gate stops running non-terminating watch scripts (which today burn a 10-minute
timeout and then block every PR for that repo) and stops running auto-fix scripts that silently rewrite the
worktree after self-review; a repo with only such scripts falls through to the gate's existing, documented
no_commands_detected skip, and stack detection returns the same commands regardless of package.json key order.
Links & Resources
packages/loopover-miner/lib/stack-detection.ts:103-108 — pickScript
packages/loopover-miner/lib/stack-detection.ts:138-150 — the four selections and the commands they build
packages/loopover-miner/lib/stack-detection.ts:1-7 — the "never guess" contract
packages/loopover-miner/lib/target-repo-verification.ts:117-140 — the gate that executes them
packages/loopover-miner/lib/target-repo-verification.ts:11-13, :121-123 — the existing skip fallback
packages/loopover-miner/lib/attempt-cli.ts:900-906 — the production binding to the attempt worktree
packages/loopover-miner/lib/attempt-runner.ts:223-233 — where the gate runs relative to self-review and submission
Context
detectRepoStackinfers a Node repo's validation commands by exact script name first, then by pattern —packages/loopover-miner/lib/stack-detection.ts:103-108:with the patterns at
packages/loopover-miner/lib/stack-detection.ts:138-141:Those patterns match
<name>:<anything>. There is no exclusion of write-mode or non-terminating variants, and thefallback is
names.find(...)— the firstObject.keys(insertion-order) match, with no preference amongseveral candidates. So for a
package.jsonwhose scripts are:{ "test:watch": "vitest" }→testCommand: "npm run test:watch"{ "build:watch": "tsc -w" }→buildCommand: "npm run build:watch"{ "lint:fix": "eslint --fix ." }→lintCommand: "npm run lint:fix"{ "test:update": "vitest -u" }→testCommand: "npm run test:update"The file's own header (
packages/loopover-miner/lib/stack-detection.ts:1-7) promises exactly the opposite posture:"a repo whose stack can't be confidently identified returns an explicit
{ detected: false, reason }instead ofguessing."
These are not display-only strings.
runTargetRepoVerificationexecutes them in the attempt's real worktree —packages/loopover-miner/lib/target-repo-verification.ts:117-140:and
attempt-cli.tsbinds it to the live worktree atpackages/loopover-miner/lib/attempt-cli.ts:900-906:Two concrete harms:
:watchcommand never exits.defaultVerificationSpawnbounds each command atDEFAULT_VERIFICATION_TIMEOUT_MS = 10 * 60 * 1000(
packages/loopover-miner/lib/target-repo-verification.ts:41), after which the process tree is killed and thecommand resolves
code: null→ok: code === 0is false(
packages/loopover-miner/lib/target-repo-verification.ts:138) → the gate returns{ status: "failed", … }→runMinerAttemptreturnsverification_failed(
packages/loopover-miner/lib/attempt-runner.ts:228-233) and the PR is blocked. A repo whose only test scriptis
test:watchtherefore costs 10 minutes of wall clock and then fails every attempt, permanently.:fixcommand mutates the worktree.runTargetRepoVerificationruns after the iterate loop, afterself-review, and immediately before the submission gate
(
packages/loopover-miner/lib/attempt-runner.ts:223-233).npm run lint:fixrewrites files in the worktree atthat point, so the working tree no longer matches the changed-file set the handoff packet and self-review
scored. The module's own header calls itself the "independent quality gate" that "run[s] the TARGET
repository's own detected test/lint/build commands"
(
packages/loopover-miner/lib/target-repo-verification.ts:1-12) — a check, not a mutation.The gate already has a documented, correct fallback for "detection came up empty": it SKIPS, never fails —
packages/loopover-miner/lib/target-repo-verification.ts:11-13and:121-123(
if (runnable.length === 0) return { status: "skipped", reason: "no_commands_detected" };). That is the rightdisposition when the only candidate script is non-terminating or write-mode.
Requirements
pickScript's pattern fallback must not select a script name that denotes a watch/non-terminating variant(
watch,dev,serve) or a write-mode variant (fix,write,update,u) in any:-delimited segmentafter the base name. Matching must be case-insensitive and must only consider whole
:-delimited segments, so ascript literally named
lintortestis unaffected and a name liketest:fixturesis NOT excluded(
fixturesis notfix).null— never a fallback guess —so
runTargetRepoVerificationreaches its existingno_commands_detectedskip instead of running or failing."test","lint","build"or"format"script always winsoutright, exactly as today at
packages/loopover-miner/lib/stack-detection.ts:106.package.jsonkey order — pick the lexicographically smallest candidate name. Today'snames.find(...)returnswhichever key happens to come first, so
{ "test:e2e": …, "test:unit": … }and the same object with the keysswapped currently produce different
testCommandvalues for the same repo.build|compile|bundle,test,lint,format|fmt), thedetector precedence order in
DETECTORS(packages/loopover-miner/lib/stack-detection.ts:245), thepackageManagerresolution, orrenderStackSummary's output format.runTargetRepoVerification, its command ordering, its timeout, or its skip reasons.Deliverables
detectRepoStackwithpackage.jsonscripts{ "test:watch": "vitest" }(no plaintest) returnstestCommand: null— asserted intest/unit/miner-stack-detection.test.ts.{ "build:watch": "tsc -w" }→buildCommand: null,{ "lint:fix": "eslint --fix ." }→lintCommand: null, and{ "format:write": "prettier -w ." }→formatCommand: null— asserted intest/unit/miner-stack-detection.test.ts.{ "test": "vitest run", "test:watch": "vitest" }still returnstestCommand: "npm test", and{ "lint": "eslint .", "lint:fix": "eslint --fix ." }still returnslintCommand: "npm run lint"—asserted in
test/unit/miner-stack-detection.test.ts.{ "test:fixtures": "node gen.js" }still returnstestCommand: "npm run test:fixtures"(not excluded) —asserted in
test/unit/miner-stack-detection.test.ts.{ "test:unit": "a", "test:e2e": "b" }and{ "test:e2e": "b", "test:unit": "a" }both returnthe same
testCommand— asserted intest/unit/miner-stack-detection.test.ts.runTargetRepoVerification({ worktreeDir, stack })for a stack whose only candidates were excluded (so allthree commands are
null) returns{ status: "skipped", reason: "no_commands_detected" }and calls theinjected
spawnzero times — asserted intest/unit/miner-target-repo-verification.test.ts.test/unit/miner-stack-detection.test.tsnamed for this bug (e.g.REGRESSION: a watch-only or fix-only script is never selected as a validation command) that fails against the currentcode.
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example one that
excludes
:watchbut still selectslint:fix, or one that fixes exclusion but leavesnames.find(...)'skey-order dependence — does not resolve this issue.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted.
vitest.config.ts'scoverage.includelistspackages/loopover-miner/lib/**/*.ts, sostack-detection.tsandtarget-repo-verification.tsare measured andgated. Every branch the change introduces needs both arms tested: the exact-name hit vs miss in
pickScript; theexclusion predicate matching vs not matching (cover
watch,dev,serve,fix,write,update,u, and atleast one near-miss such as
fixtures); the "some candidate survives" vs "all candidates excluded → null" arms;and the deterministic-pick comparison with both one candidate and several. Each of the four
pickScriptcallsites at
packages/loopover-miner/lib/stack-detection.ts:138-141must exercise the null arm, since each feeds aseparate ternary at
:146-150.Expected Outcome
The target-repo verification gate stops running non-terminating watch scripts (which today burn a 10-minute
timeout and then block every PR for that repo) and stops running auto-fix scripts that silently rewrite the
worktree after self-review; a repo with only such scripts falls through to the gate's existing, documented
no_commands_detectedskip, and stack detection returns the same commands regardless ofpackage.jsonkey order.Links & Resources
packages/loopover-miner/lib/stack-detection.ts:103-108—pickScriptpackages/loopover-miner/lib/stack-detection.ts:138-150— the four selections and the commands they buildpackages/loopover-miner/lib/stack-detection.ts:1-7— the "never guess" contractpackages/loopover-miner/lib/target-repo-verification.ts:117-140— the gate that executes thempackages/loopover-miner/lib/target-repo-verification.ts:11-13,:121-123— the existing skip fallbackpackages/loopover-miner/lib/attempt-cli.ts:900-906— the production binding to the attempt worktreepackages/loopover-miner/lib/attempt-runner.ts:223-233— where the gate runs relative to self-review and submission