Skip to content

T-VE7H-task-work-probe-keys-package-manager-off-project

Status: closed/done · Impact: medium · Complexity: medium

/sdlc:task-work Step 3b’s preflight_permissions.ts probe reports a missing Bash(npm:*) permission on projects that don’t use npm, producing a false-positive gap on every run. The probe’s package-manager signal is a flat SIGNAL_TABLE entry that fires on the literal string npm appearing anywhere in the task body — fuzzy text matching with no awareness of what the project actually builds with.

Replace that with a two-tier signal model, grounded in P-0001-prefer-deterministic-over-llm and S-0004-sdlc-cli-llm-head-deterministic-tail point-1 (the pure-deterministic core):

  1. Hard gaps (blocking missing Bash(<pm>:*) findings) come ONLY from deterministic, project-grounded signals: the package managers / build verbs a factored multi-ecosystem detection module resolves from the project’s lockfiles + ecosystem markers, plus the verbs declared in the project’s top-level sdlc.yaml (quality_checks / worktree_init).
  2. Warnings (advisory, never blocking) come from verbs discovered heuristically in the task body text — the current detectSignals / SIGNAL_TABLE substring approach is a fuzzy signal and must be demoted to an advisory.

The detection module must be multi-ecosystem and polyglot-aware: it recognises node PMs (npm/pnpm/yarn/bun), Rust (cargo), Python (uv/pip/poetry), Go (go modules), and is extensible to more — and it resolves all managers present in a project simultaneously, not “one effective manager.” This very repo is the motivating case: a Bun + Rust moon monorepo where the module must resolve BOTH bun and cargo and emit no spurious npm gap.

task-work Step 3b’s preflight_permissions probe reports ‘npm: missing Bash(npm:*)’ on pnpm projects whose quality verb is ‘pnpm run build’. The probe’s package-manager signal fires on a generic npm family rather than the project’s actual package manager. Key the signal off the project’s declared quality_checks/worktree_init verbs (or the lockfile) so pnpm/yarn/bun projects don’t get a false-positive npm gap on every run.

— from T-WQT3-migrate-exercise-video-field-to-list in the git@github.com:sksizer/family.git repo

The probe scans the task body for tool-family hint strings and emits a Bash(<verb>:*) gap per fired signal that the sandbox doesn’t grant. The package-manager families (npm, pnpm, yarn) are entries in a flat SIGNAL_TABLE keyed purely on body text — detectSignals fires the npm family whenever the literal substring npm (or `npm`, etc.) appears in the task body. Nothing consults the project’s sdlc.yaml verbs, its lockfiles, or its ecosystem markers, so a pnpm-only project whose task body merely mentions npm in passing yields a false npm: missing Bash(npm:*) line on every run — and every body hit is rendered as a hard, exit-1 gap regardless of how fuzzy the signal is.

LocationRole today
plugin/skills/task-work/preflight_permissions.ts#SIGNAL_TABLEFlat signal table; npm/pnpm/yarn/npx/node/uv/cargo/pytest families each fire from body-text hints, with no project-ecosystem awareness
plugin/skills/task-work/preflight_permissions.ts#detectSignalsScans the task body against SIGNAL_TABLE.hints; returns one family name per hit. This is the FUZZY tier — body text only
plugin/skills/task-work/preflight_permissions.ts#mainResolves the repo root (via findRepoRoot), reads settings, renders one <family>: missing Bash(<verb>:*) line per uncovered fired signal as a hard exit-1 gap; never loads sdlc.yaml or inspects lockfiles/ecosystem markers
plugin/lib/services/quality/detect-runners.ts#detectNodeInlines the node lockfile→manager mapping (pnpm-lock.yaml/pnpm-workspace.yaml → pnpm, yarn.lock → yarn, else npm); detectCargo keys off Cargo.toml; detectPyprojectTools parses pyproject.toml — the per-ecosystem markers a shared resolver should reuse/extend
plugin/lib/config/load.ts#loadConfigHydrates <projectRoot>/sdlc.yaml into a typed SdlcConfig (quality_checks, worktree_init); never throws (returns all-defaults on any failure) — the canonical source of a project’s declared verbs
plugin/skills/task-work/tests/preflight_permissions.test.tsBun test suite; AC-1 asserts the current body-text-driven npm: missing Bash(npm:*) hard-gap behavior that this task changes

Split the probe’s package-manager reasoning into two tiers, with a factored deterministic detection module as the load-bearing core.

Tier 1 — deterministic, project-grounded → HARD gaps. A factored, multi-ecosystem resolver inspects the project root for ecosystem markers and resolves the FULL SET of package managers / build verbs in play. For each resolved manager whose Bash(<pm>:*) permission is not granted, the probe emits a blocking <pm>: missing Bash(<pm>:*) line (exit 1). The resolved set is the union of:

  • Lockfiles / ecosystem markers at the project root: node (bun.lock/bun.lockb → bun, pnpm-lock.yaml/pnpm-workspace.yaml → pnpm, yarn.lock → yarn, package-lock.json → npm), Rust (Cargo.toml → cargo), Python (uv.lock → uv, poetry.lock → poetry, else requirements.txt → pip), Go (go.mod → go). The mapping is data-driven and extensible — one table, one entry per { marker → manager-verb }.
  • sdlc.yaml-declared verbs — the leading token of every entry in quality_checks + worktree_init, read via loadConfig. A declared pnpm run build contributes pnpm; cargo test contributes cargo; bun test contributes bun. Declared verbs are authoritative even when no lockfile is present.

The resolver returns a SET, so a polyglot repo (e.g. this Bun + Rust moon monorepo: bun.lock + a Cargo.toml, sdlc.yaml declaring bunx tsc and bun test) resolves { bun, cargo } and never npm.

Tier 2 — body-text heuristics → WARNINGS only. The existing detectSignals / SIGNAL_TABLE substring matching stays, but every package-manager family it discovers from body text that is NOT in the Tier-1 resolved set is downgraded to an advisory warning line — never a hard gap, never an exit-1. A verb that appears only because the task prose mentions it (e.g. a family/ task body that says “run npm install” on a pnpm project) produces, at most, a warning the operator can read and ignore.

Net effect on the motivating bug: a pnpm project (Tier-1 resolves pnpm from pnpm run build / pnpm-lock.yaml) with no Bash(pnpm:*) grant emits a hard pnpm: missing Bash(pnpm:*) gap; a passing npm mention in the body emits at most a warning, never the false Bash(npm:*) hard gap. A repo with no node ecosystem at all emits no node-PM gap regardless of body text.

  1. Factor the multi-ecosystem resolver. Add a shared lib helper — plugin/lib/services/quality/detect-managers.ts (beside detect-runners.ts, the canonical detection home) — exporting resolveProjectManagers(projectRoot, declaredVerbs): Set<string>. It is pure (project root + verb list in, set out; no ambient cwd, no process writes), per S-0004-sdlc-cli-llm-head-deterministic-tail point-1. It owns a single data-driven MARKER_TABLE ({ marker filename → manager verb }) spanning node/Rust/Python/Go and is trivially extensible by adding a row. Reuse the existing node lockfile literals: factor the lockfile branch out of detect-runners.ts#detectNode into a shared detectNodeManager(projectRoot) (or have both call the same MARKER_TABLE), so there is ONE source of truth for the node lockfile→manager mapping. Add the bun.lock/bun.lockb → bun row (currently absent from detectNode).
  2. Add the verb-token tier. In the resolver, parse the leading token of each declared verb (split on whitespace; pnpm run buildpnpm, cargo testcargo, bunx tsc → treat bunx→bun / keep bun when present) and union the recognised manager tokens into the result set.
  3. Wire main to the deterministic core. After findRepoRoot(taskPath) resolves the repo root, call loadConfig(repoRoot) (@lib/config) to read quality_checks + worktree_init, pass their union to resolveProjectManagers, and treat the returned set as the source of HARD package-manager gaps. For each resolved manager lacking Bash(<pm>:*), push a hard gap line.
  4. Demote body-text PM discoveries to warnings. Keep detectSignals but route any package-manager family it returns that is NOT in the resolved set to a new advisory-warning channel (a distinct line shape, e.g. warning: <pm> mentioned in task body but not a resolved project manager; Bash(<pm>:*) not granted). Warnings do NOT affect the exit code. Non-PM families (node, npx, pytest) keep their current behavior pending T-NUML-task-work-preflight-permissions-probe-extension-for-skill-internal-scripts; cargo/uv become Tier-1 managers via the resolver.
  5. Update the header docstring to describe the two-tier model: deterministic project-grounded resolution → hard gaps; body-text heuristics → warnings.
  6. Update preflight_permissions.test.ts per the acceptance criteria below: rewrite the AC-1 npm case to a project whose sdlc.yaml/lockfile resolves to npm; add the polyglot, pnpm-vs-npm, body-warning, and no-ecosystem cases. Use the existing tmpdir + writeFileSync harness; write sdlc.yaml and/or marker files into the tmp repo root.
  7. Update SKILL.md Step 3b prose so the package-manager-gap description reflects the two-tier model (deterministic project-derived hard gaps; body-text warnings) rather than body-text-only detection.
LocationKindChange
plugin/lib/services/quality/detect-managers.tsnewNew shared lib helper: resolveProjectManagers(projectRoot, declaredVerbs): Set<string> over a data-driven multi-ecosystem MARKER_TABLE (node/Rust/Python/Go) + verb-token parsing; the deterministic core
plugin/lib/services/quality/detect-runners.ts#detectNodemodifyFactor the node lockfile→manager mapping into a shared helper (add bun.lock/bun.lockb → bun); single source of truth shared with the resolver; detectNode external behavior unchanged
plugin/skills/task-work/preflight_permissions.tsmodifyLoad sdlc.yaml via loadConfig in main; call resolveProjectManagers; emit HARD Bash(<pm>:*) gaps only for resolved managers; demote body-text PM discoveries to warnings; update header docstring
plugin/skills/task-work/tests/preflight_permissions.test.tsmodifyRewrite AC-1 to an npm-resolved project; add polyglot (bun+cargo), pnpm-vs-npm, body-warning-not-gap, and no-ecosystem fixtures
plugin/skills/task-work/SKILL.mdmodifyUpdate Step 3b prose: deterministic project-derived hard gaps vs body-text warnings
  • AC-1: A Bun + Rust moon repo (bun.lock + a Cargo.toml, sdlc.yaml declaring bun test and cargo test) with no Bash(bun:*) or Bash(cargo:*) grant resolves BOTH bun and cargo, emits a hard bun: missing Bash(bun:*) AND cargo: missing Bash(cargo:*) line, and emits NO npm gap (no false Bash(npm:*)).
  • AC-2: A verb that appears ONLY in the task body (the body mentions npm install on a project whose resolved manager set does not include npm) emits a WARNING line, not a hard gap, and does not contribute to a non-zero exit on its own.
  • AC-3: A pnpm project (pnpm run build in sdlc.yaml or pnpm-lock.yaml at root) with no Bash(pnpm:*) grant exits 1 with a hard pnpm: missing Bash(pnpm:*) line, and npm is NOT a hard gap even when the task body mentions npm.
  • AC-4: A genuine npm project (npm verb in sdlc.yaml or package-lock.json at root) with no Bash(npm:*) grant exits 1 with a hard npm: missing Bash(npm:*) line — the pre-existing behavior is preserved for real npm projects.
  • AC-5: A project with no resolvable node/Rust/Python/Go ecosystem (no manager verb in sdlc.yaml, no lockfile/marker) whose task body mentions npm/cargo emits NO hard package-manager gap (at most warnings).
  • AC-6: The lockfile/ecosystem→manager mapping is shared between the new resolver and detect-runners.ts (no duplicated node-lockfile literals); the resolver’s MARKER_TABLE is extensible by adding a single row.
  • AC-7: The resolver is pure — given the same project root and declared-verb list it returns the same set, with no ambient cwd reads and no process-stream writes (covered by a unit test in detect-managers.test.ts).
  • AC-8: just full-check (or the project’s quality_checks set — bunx tsc --noEmit, bun test, bun test ./.claude, sdlc entities audit) passes on the dev branch, including the updated preflight_permissions.test.ts and the new detect-managers.test.ts.
  • Plugin-internal script-path coverage. Extending the probe to enumerate the plugin’s own shell-out scripts (classify_pr.ts, post_self_comment.sh, etc.) is T-NUML-task-work-preflight-permissions-probe-extension-for-skill-internal-scripts’s concern — a different probe check at a different layer. This task does not touch it.
  • Auto-granting or writing the missing permission. The probe REPORTS hard gaps and warnings; it does not mutate sandbox settings.
  • Non-package-manager body families. node, npx, pytest keep their current body-text behavior; only the package-manager / build-verb families (npm/pnpm/yarn/bun/cargo/uv/pip/go) move to the two-tier model.
  • Per-subproject manager resolution inside a monorepo. The resolver keys off the project ROOT’s markers + the root sdlc.yaml verbs. Resolving a different manager per workspace package (e.g. a node subpackage with its own lockfile nested under a Rust workspace) is deferred.
  • Spawned by /sdlc:spawn-task-pr on 2026-06-10 UTC from T-WQT3-migrate-exercise-video-field-to-list in the git@github.com:sksizer/family.git repo.
  • Reconciled against T-NUML-task-work-preflight-permissions-probe-extension-for-skill-internal-scripts on 2026-06-19: T-NUML extends the probe to cover the plugin’s own internal script paths; this task refines the existing package-manager signal into a two-tier (deterministic-hard / body-warning) multi-ecosystem model. Distinct, complementary scopes — kept separate.

Captured by /sdlc:task-work on 2026-06-19. PR: pending.

  • AC-1: auto — bun test plugin/skills/task-work/tests/preflight_permissions.test.ts (the polyglot bun.lock + Cargo.toml fixture asserts both bun: missing Bash(bun:*) and cargo: missing Bash(cargo:*) and no npm gap). Also confirmed agent-manual: a faithful dogfood of the new probe against this repo’s own task file resolved {bun} and emitted no false npm gap.
  • AC-2: auto — preflight_permissions.test.ts body-only-npm case (stderr warning:, no hard gap, exit 0). Dogfood corroborated: pnpm/yarn/uv/cargo body mentions demoted to stderr warnings.
  • AC-3: auto — preflight_permissions.test.ts pnpm case (hard pnpm: missing Bash(pnpm:*), no standalone npm hard gap despite a body mention).
  • AC-4: auto — preflight_permissions.test.ts npm-resolved case (npm verb / package-lock.json → hard npm: missing Bash(npm:*); real-npm behavior preserved).
  • AC-5: auto — preflight_permissions.test.ts no-ecosystem case (body mentions npm/cargo, no hard package-manager gap).
  • AC-6: auto — detect-managers.test.ts plus the full suite: detect-runners.ts#detectNode consumes the shared detectNodeManager/NODE_LOCKFILE_MANAGERS; no duplicated node-lockfile literals; MARKER_TABLE extends by one row.
  • AC-7: auto — detect-managers.test.ts purity test (same inputs → same set; no cwd read, no stream writes).
  • AC-8: auto — bunx tsc --noEmit clean; bun test 1481/1481; bun test ./.claude 52/52; docs generate --check reports zero drift. The one baseline-gated new-drift=1 is a rumdl summary-line timing artifact (see friction below), not a real finding.
  • The deterministic gap-report + corpus-assumption scanner passed the readiness gate on the first pass — the task spec was already implementation-ready, no auto-define needed.
  • The resolver factored cleanly: detectNode now delegates lockfile→manager to the shared helper with no behavior change for pnpm/yarn/npm and a one-row gain for bun.lock; the full 1481-test suite stayed green.
  • The two-tier split landed the motivating fix exactly: a faithful dogfood from the main repo showed the false npm hard gap gone and pnpm/yarn/uv/cargo demoted to advisory warnings.
  • The Step 6 implementer was briefed with the non-baseline-gated quality command (quality run --config … --line), so it saw pre-existing rumdl drift in an UNRELATED file (T-XBJY-…md) as a gate failure and reflowed it — scope creep that had to be reverted (one dropped commit). Fix: Step 6’s sub-agent brief should hand the implementer the same baseline-gated invocation Step 7 uses (--diff-against-baseline "$ORIGIN_MAIN_SHA" --baseline-dir <main>/.sdlc/quality-baselines) so pre-existing drift is invisible to it. → T-XB7F-task-work-step6-briefs-baseline-gated-quality
  • The baseline line-diff flags rumdl’s summary line Issues: Found N issues in M/K files (XXms) as new-drift because the (XXms) timing is non-deterministic — a phantom new-drift=1 with zero real findings changed. Fix: the differ should strip trailing (\d+ms) timings (or exclude per-runner summary lines) before diffing. Same baseline-isolation class T-XBJY’s own post-mortem already flagged. → T-BQRU-quality-normalize-ports-pids-timings
  • Step 7’s quality run from the worktree defaults --baseline-dir to the worktree’s own .sdlc/, but Step 3a captured the baseline into the MAIN repo’s .sdlc/quality-baselines/, so the gate errored baseline not found until --baseline-dir <main>/.sdlc/quality-baselines was passed explicitly. Fix: Step 7 should resolve the baseline dir from the git-common-dir (the superproject) rather than the worktree cwd, or the SKILL.md should document the explicit flag. → T-44OO-plugin-scripts-self-discover-project-root
  • The heartbeat: sdlc lease heartbeat-loop start <basename>’s --help documents only --project-root, not the <basename> positional the SKILL.md invokes (the positional is accepted and works — doc drift only). Low priority. → T-XLSV-heartbeat-loop-help-documents-basename-positional

← Back to Tasks