Skip to content

miner(stack-detection): pickScript selects watch/fix script variants that the target-repo verification gate then executes #10006

Description

@JSONbored

⚠️ 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:

  1. 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: nullok: 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.
  2. 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

  • detectRepoStack with package.json scripts { "test:watch": "vitest" } (no plain test) returns
    testCommand: null — asserted in test/unit/miner-stack-detection.test.ts.
  • Same for { "build:watch": "tsc -w" }buildCommand: null, { "lint:fix": "eslint --fix ." }
    lintCommand: null, and { "format:write": "prettier -w ." }formatCommand: null — asserted in
    test/unit/miner-stack-detection.test.ts.
  • { "test": "vitest run", "test:watch": "vitest" } still returns testCommand: "npm test", and
    { "lint": "eslint .", "lint:fix": "eslint --fix ." } still returns lintCommand: "npm run lint"
    asserted in test/unit/miner-stack-detection.test.ts.
  • { "test:fixtures": "node gen.js" } still returns testCommand: "npm run test:fixtures" (not excluded) —
    asserted in test/unit/miner-stack-detection.test.ts.
  • Determinism: { "test:unit": "a", "test:e2e": "b" } and { "test:e2e": "b", "test:unit": "a" } both return
    the same testCommand — asserted in test/unit/miner-stack-detection.test.ts.
  • runTargetRepoVerification({ worktreeDir, stack }) for a stack whose only candidates were excluded (so all
    three commands are null) returns { status: "skipped", reason: "no_commands_detected" } and calls the
    injected spawn zero times — asserted in test/unit/miner-target-repo-verification.test.ts.
  • A regression test at test/unit/miner-stack-detection.test.ts named for this bug (e.g. REGRESSION: a watch-only or fix-only script is never selected as a validation command) that fails against the current
    code.

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-108pickScript
  • 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions