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):
- 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-levelsdlc.yaml(quality_checks/worktree_init). - Warnings (advisory, never blocking) come from verbs discovered
heuristically in the task body text — the current
detectSignals/SIGNAL_TABLEsubstring 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-listin thegit@github.com:sksizer/family.gitrepo
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.
| Location | Role today |
|---|---|
plugin/skills/task-work/preflight_permissions.ts#SIGNAL_TABLE | Flat 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#detectSignals | Scans 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#main | Resolves 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#detectNode | Inlines 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#loadConfig | Hydrates <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.ts | Bun test suite; AC-1 asserts the current body-text-driven npm: missing Bash(npm:*) hard-gap behavior that this task changes |
Proposed
Section titled “Proposed”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, elserequirements.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 inquality_checks+worktree_init, read vialoadConfig. A declaredpnpm run buildcontributespnpm;cargo testcontributescargo;bun testcontributesbun. 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.
Approach
Section titled “Approach”- Factor the multi-ecosystem resolver. Add a shared
libhelper —plugin/lib/services/quality/detect-managers.ts(besidedetect-runners.ts, the canonical detection home) — exportingresolveProjectManagers(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-drivenMARKER_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 ofdetect-runners.ts#detectNodeinto a shareddetectNodeManager(projectRoot)(or have both call the sameMARKER_TABLE), so there is ONE source of truth for the node lockfile→manager mapping. Add thebun.lock/bun.lockb→ bun row (currently absent fromdetectNode). - Add the verb-token tier. In the resolver, parse the leading token of each
declared verb (split on whitespace;
pnpm run build→pnpm,cargo test→cargo,bunx tsc→ treatbunx→bun / keepbunwhen present) and union the recognised manager tokens into the result set. - Wire
mainto the deterministic core. AfterfindRepoRoot(taskPath)resolves the repo root, callloadConfig(repoRoot)(@lib/config) to readquality_checks+worktree_init, pass their union toresolveProjectManagers, and treat the returned set as the source of HARD package-manager gaps. For each resolved manager lackingBash(<pm>:*), push a hard gap line. - Demote body-text PM discoveries to warnings. Keep
detectSignalsbut 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/uvbecome Tier-1 managers via the resolver. - Update the header docstring to describe the two-tier model: deterministic project-grounded resolution → hard gaps; body-text heuristics → warnings.
- Update
preflight_permissions.test.tsper the acceptance criteria below: rewrite theAC-1npm case to a project whosesdlc.yaml/lockfile resolves to npm; add the polyglot, pnpm-vs-npm, body-warning, and no-ecosystem cases. Use the existing tmpdir +writeFileSyncharness; writesdlc.yamland/or marker files into the tmp repo root. - Update
SKILL.mdStep 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.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/lib/services/quality/detect-managers.ts | new | New 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#detectNode | modify | Factor 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.ts | modify | Load 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.ts | modify | Rewrite 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.md | modify | Update Step 3b prose: deterministic project-derived hard gaps vs body-text warnings |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: A Bun + Rust moon repo (
bun.lock+ aCargo.toml,sdlc.yamldeclaringbun testandcargo test) with noBash(bun:*)orBash(cargo:*)grant resolves BOTHbunandcargo, emits a hardbun: missing Bash(bun:*)ANDcargo: missing Bash(cargo:*)line, and emits NOnpmgap (no falseBash(npm:*)). - AC-2: A verb that appears ONLY in the task body (the body mentions
npm installon 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 buildinsdlc.yamlorpnpm-lock.yamlat root) with noBash(pnpm:*)grant exits 1 with a hardpnpm: 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.yamlorpackage-lock.jsonat root) with noBash(npm:*)grant exits 1 with a hardnpm: 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 mentionsnpm/cargoemits 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’sMARKER_TABLEis 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’squality_checksset —bunx tsc --noEmit,bun test,bun test ./.claude,sdlc entities audit) passes on the dev branch, including the updatedpreflight_permissions.test.tsand the newdetect-managers.test.ts.
Out of scope
Section titled “Out of scope”- 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,pytestkeep 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.yamlverbs. Resolving a different manager per workspace package (e.g. a node subpackage with its own lockfile nested under a Rust workspace) is deferred.
Dependencies
Section titled “Dependencies”- Builds on the shipped pre-flight probe (T-Z8VC-task-work-preflight-permissions-probe, #70). Complementary to — not blocked by — T-NUML-task-work-preflight-permissions-probe-extension-for-skill-internal-scripts, which extends the same probe with plugin-internal-script coverage (a distinct check). The two can land in either order.
Discovery context
Section titled “Discovery context”- Spawned by /sdlc:spawn-task-pr on 2026-06-10 UTC from
T-WQT3-migrate-exercise-video-field-to-listin thegit@github.com:sksizer/family.gitrepo. - 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.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-06-19. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
bun test plugin/skills/task-work/tests/preflight_permissions.test.ts(the polyglotbun.lock+Cargo.tomlfixture asserts bothbun: missing Bash(bun:*)andcargo: missing Bash(cargo:*)and nonpmgap). Also confirmed agent-manual: a faithful dogfood of the new probe against this repo’s own task file resolved{bun}and emitted no falsenpmgap. - AC-2: auto —
preflight_permissions.test.tsbody-only-npm case (stderrwarning:, no hard gap, exit 0). Dogfood corroborated:pnpm/yarn/uv/cargobody mentions demoted to stderr warnings. - AC-3: auto —
preflight_permissions.test.tspnpm case (hardpnpm: missing Bash(pnpm:*), no standalone npm hard gap despite a body mention). - AC-4: auto —
preflight_permissions.test.tsnpm-resolved case (npm verb /package-lock.json→ hardnpm: missing Bash(npm:*); real-npm behavior preserved). - AC-5: auto —
preflight_permissions.test.tsno-ecosystem case (body mentionsnpm/cargo, no hard package-manager gap). - AC-6: auto —
detect-managers.test.tsplus the full suite:detect-runners.ts#detectNodeconsumes the shareddetectNodeManager/NODE_LOCKFILE_MANAGERS; no duplicated node-lockfile literals;MARKER_TABLEextends by one row. - AC-7: auto —
detect-managers.test.tspurity test (same inputs → same set; no cwd read, no stream writes). - AC-8: auto —
bunx tsc --noEmitclean;bun test1481/1481;bun test ./.claude52/52;docs generate --checkreports zero drift. The one baseline-gatednew-drift=1is a rumdl summary-line timing artifact (see friction below), not a real finding.
What worked
Section titled “What worked”- 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:
detectNodenow delegates lockfile→manager to the shared helper with no behavior change for pnpm/yarn/npm and a one-row gain forbun.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
npmhard gap gone andpnpm/yarn/uv/cargodemoted to advisory warnings.
Friction and automation gaps
Section titled “Friction and automation gaps”- 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)asnew-driftbecause the(XXms)timing is non-deterministic — a phantomnew-drift=1with 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 runfrom the worktree defaults--baseline-dirto the worktree’s own.sdlc/, but Step 3a captured the baseline into the MAIN repo’s.sdlc/quality-baselines/, so the gate erroredbaseline not founduntil--baseline-dir <main>/.sdlc/quality-baselineswas 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--helpdocuments 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
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-XB7F-task-work-step6-briefs-baseline-gated-quality (https://github.com/sksizer/dev/pull/461) — Step 6’s sub-agent brief should hand the implementer the baseline-gated quality invocation Step 7 uses, so pre-existing drift is invisible to it; spawned.
- T-BQRU-quality-normalize-ports-pids-timings —
normalizeFindingmasking of(Nms)timing tokens (the live owner of the rumdl summary-line false-new-drift); linked. - T-44OO-plugin-scripts-self-discover-project-root — plugin scripts self-discover project
root via
git rev-parse --git-common-dir, so the Step 7 gate resolves the main-checkout baseline dir from a worktree without an explicit flag; linked. - T-XLSV-heartbeat-loop-help-documents-basename-positional
(https://github.com/sksizer/dev/pull/462) —
lease heartbeat-loop --helpdocuments the<basename>positional it already accepts; spawned.