Skip to content

T-T879-task-work-uses-per-project-quality-checks

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

Auto-generated from a /sdlc:task-work post-mortem. Review and promote to ready before picking up.

/sdlc:task-work hard-codes just full-check / just ci as the quality gates it expects to run, which only fits projects that use the just task runner. This repo (and presumably many SDLC consumers) has no justfile — its real gates are validate_frontmatter.py, audit_entities.py, and run_evals.py. Today the orchestrator has to silently substitute; a strict reading of the skill would either fail or skip checks entirely. Closing this gap lets task-work run unmodified across projects with different runners. Cited by T-J2CW-add-epic-entity-task-depends-on-dependencies and previously by T-3A6G-implement-entities-migrate.

From the originating post-mortem:

The skill expects just full-check / just ci for quality checks (/sdlc:task-work step 7). This repo has no justfile — checks here are validate_frontmatter.py, audit_entities.py, and run_evals.py. Worth parameterizing the skill’s check commands per-project or making the skill discover them.

And from T-3A6G-implement-entities-migrate’s post-mortem:

task-work’s Step 4 worktree init (mise trust && just setup-worktree) and Step 7’s quality checks (just full-check, just ci) are templated from a different project that has just; this repo has neither. Orchestrator substituted check_entities.py + the two eval runners. Fix: task-work should declare its quality-check verbs in a per-project config (e.g. .claude/sdlc-config.yaml) or probe for Justfile and substitute.

Skill: plugin/skills/task-work/SKILL.md Step 4 (worktree init) and Step 7 (quality checks).

/sdlc:task-work reads a per-project config at sdlc.yaml in the project root (pinned location — kept top-level so it sits next to README/CLAUDE.md, not buried under .claude/). The config declares the quality-check commands as a list of shell verbs:

quality_checks:
- just full-check
- just ci

Three user-facing pieces ship together, built on top of three shared compositional primitives (see Approach for the primitives):

  1. task-work reads sdlc.yaml in Step 4 (worktree init) and Step 7 (quality checks). If the file is absent, the skill warns visibly and runs no checks (no silent fallback).
  2. /sdlc:setup creates sdlc.yaml with project-appropriate defaults when it doesn’t exist, alongside the per-type docs/planning/ directory creation it already does. Idempotent — re-running setup leaves an existing sdlc.yaml untouched.
  3. A new skill /sdlc:find-quality-checks helps a user populate or update the quality_checks: list. It probes the project for recognized runners (Justfile, package.json scripts, Makefile, presence of plugin/skills/entities-audit/tests/run_evals.py-style harnesses) and proposes commands via AskUserQuestion. Can be invoked any time — at project bootstrap or when adding new gates. (Skill name finalized during implementation; the earlier draft floated configure-quality-checks but find-quality-checks better matches the probe-and-suggest verb.)

This ships as three reusable compositional units plus the consumers that wire them together. The units are deliberately small and unopinionated so other SDLC skills (future CI hooks, pre-PR gates, per-project diagnostics) can pick them up without coupling to task-work.

  1. Deterministic runner-detection scriptplugin/scripts/detect_quality_runners.py. Probes a project root for known runner signals (Justfile, package.json scripts, Makefile, Cargo workspaces, common Python check entrypoints like validate_frontmatter.py / audit_entities.py / run_evals.py, etc.) and emits a structured JSON report on stdout describing what’s present and what shell verbs would invoke each. No LLM, no prompts — pure file/grep introspection. Best-effort: missing runners are simply absent from the report, never an error. Reusable by anything that needs to ask “what runs in this project.”

  2. /sdlc:find-quality-checks LLM skill. Wraps the detection script and uses model judgment to propose the right quality-check commands for the project: picking the right subset of detected verbs, ordering them, splitting fast vs. full gates, and asking the user via AskUserQuestion when the detector’s output is ambiguous. Writes the approved list back to sdlc.yaml. This is the user-facing “configure quality checks” experience.

  3. Deterministic executorplugin/scripts/run_quality_checks.py. Takes a list of shell verbs (typically from sdlc.yaml’s quality_checks:, but accepts an explicit list too) and runs them sequentially, reporting per-command success/failure. Three output modes selectable by flag, so it composes cleanly:

    • --line (default): one-line deterministic stdout marker suitable for use in composition (OK <passed>/<total> on success; FAIL <first-failed-cmd> on failure).
    • --json: full per-command status, stdout, stderr, duration. For machine consumers (orchestrator, dashboards).
    • --log: human-readable streamed output, for interactive runs. Non-zero exit on any failed verb. Reusable by task-work, future pre-commit/pre-PR skills, and ad-hoc invocation.
  1. Define the sdlc.yaml shape. Today: quality_checks: [str] + (reserved) worktree_init: [str]. Documented as a flat schema doc at plugin/conventions/sdlc-yaml.md rather than a new entity under plugin/entities/sdlc-config/sdlc.yaml is operational config without a lifecycle, so the entities/ machinery would be overweight.

  2. Update plugin/skills/task-work/SKILL.md Step 7 (quality checks) to read <project-root>/sdlc.yaml and invoke the executor (unit 3) rather than shelling out to hard-coded just verbs. Absent file => warn + skip. Present file with empty list => warn + skip. Present file with non-empty list => run via the executor. task-work never shells out directly in Step 7; it always goes through the executor so behavior stays consistent across consumers. Step 4’s worktree init verbs (mise trust && just setup-worktree) are left as-is in this PR — the worktree_init: slot in sdlc.yaml is reserved but unused; see Out of scope below.

  3. Extend /sdlc:setup (and its backing plugin/scripts/setup_planning.py) to create sdlc.yaml when absent. Starter file contains commented-out examples and an empty quality_checks: list. Idempotent — existing files left untouched. Accept an optional flag (e.g. --detect) to invoke the detection script (unit 1) and pre-populate proposed verbs as commented-out suggestions, so the user still has to opt in.

  4. Add plugin/skills/find-quality-checks/SKILL.md (the LLM skill, unit 2) plus any backing script. The skill: read existing sdlc.yaml if any, call the detection script, present suggested commands via AskUserQuestion, write the updated list back.

  5. Optional: add fixture-based tests covering the sdlc.yaml reader (missing file, empty list, malformed YAML, good cases), the detection script’s report shape across a few canned project layouts, and the executor’s three output modes against pass/fail mixes.

Compositional units:

  • plugin/scripts/detect_quality_runners.py (new) — deterministic runner-detection script (unit 1). Emits JSON.
  • plugin/scripts/run_quality_checks.py (new) — deterministic executor (unit 3). Supports --line / --json / --log output modes.
  • plugin/skills/find-quality-checks/SKILL.md (new) — LLM skill (unit 2) that composes the detection script + AskUserQuestion to populate sdlc.yaml.
  • plugin/conventions/sdlc-yaml.md (new) — flat schema doc describing the file shape, execution semantics, and reserved-key list.
  • sdlc.yaml (new, at project root) — this repo’s actual gates, consumed by the executor in Step 7.

Consumers:

  • plugin/skills/task-work/SKILL.md — Step 7 stops hard-coding just verbs; reads <project-root>/sdlc.yaml and invokes run_quality_checks.py. Step 4’s worktree-init verbs are deferred to a follow-up PR (see Out of scope).
  • plugin/skills/setup/SKILL.md and plugin/scripts/setup_planning.py — create sdlc.yaml with a documented starter when absent; accept --detect to pre-populate from detect_quality_runners.py output as commented suggestions.
  • AC-1: Running /sdlc:task-work in this repo (no justfile) executes validate_frontmatter.py, audit_entities.py, and run_evals.py as its quality gates without any silent substitution by the orchestrator — sourced from sdlc.yaml at the project root.
  • AC-2: Running /sdlc:task-work in a project whose sdlc.yaml declares just full-check / just ci continues to work (no regression for the original shape).
  • AC-3: A project without sdlc.yaml (or with an empty quality_checks:) gets a clear visible warning, not a silent pass.
  • AC-4: /sdlc:setup creates sdlc.yaml at the project root when absent. Re-running setup with the file present leaves it untouched.
  • AC-5: /sdlc:find-quality-checks probes a real project, suggests appropriate commands, and writes them to quality_checks: in sdlc.yaml. Idempotent.
  • AC-6 (absorbed from T-FC8W-task-work-graceful-no-justfile-fallback): the skill emits a single clear log line per skipped phase (e.g. Step 7: no quality_checks configured in sdlc.yaml — skipping) rather than running a phantom just … and failing noisily.
  • AC-7: detect_quality_runners.py (unit 1) is independently invokable and emits valid JSON describing detected runners — usable without going through any skill.
  • AC-8: run_quality_checks.py (unit 3) is independently invokable with an explicit list of verbs and supports all three output modes (--line, --json, --log). Non-zero exit on any failed verb. task-work uses it for all quality-check execution; no direct shell-outs remain in Step 7.
  • Designing a full project-config schema covering things other than quality checks (everything beyond quality_checks: is later).
  • Wiring worktree_init: in sdlc.yaml. The schema reserves the key but this PR does not consume it; /sdlc:task-work Step 4 still uses the hard-coded mise trust && just setup-worktree line. Switching Step 4 to read sdlc.yaml is a follow-up — it has the same shape as the Step 7 change but enough additional surface area (idempotency of the init verbs across re-runs, what to do when the verbs don’t exist on the user’s PATH) that it’s worth landing on its own.
  • Migrating other SDLC skills off hard-coded just verbs (their post-mortems can spawn their own follow-ups).
  • none

Spawned by /sdlc:task-work post-mortem of T-J2CW-add-epic-entity-task-depends-on-dependencies on 2026-05-19.

Captured by /sdlc:task-work on 2026-05-20. PR: pending.

  • AC-1: auto — plugin/scripts/run_quality_checks.py --config sdlc.yaml --line returns OK 5/5 against the project’s gates (audit_entities.py + four run_evals.py harnesses), sourced from sdlc.yaml with no orchestrator substitution.
  • AC-2: agent-manual — implementation sub-agent verified a tmpdir fixture containing a just shim and sdlc.yaml: [just full-check, just ci]; executor runs both via shell=True and reports OK 2/2. Not re-verified here; subprocess.run(verb, shell=True, cwd=project_root) handles multi-word verbs uniformly.
  • AC-3: auto — missing config emits config file not found: <path> on stderr, exit 2; empty quality_checks: [] emits warning: no quality_checks configured on stderr, exit 2. Step 7 of task-work documents both cases as warn-and-skip with no silent fallback.
  • AC-4: agent-manual — sub-agent verified setup_planning.py’s write_sdlc_yaml creates the file when absent with a documented header, leaves existing files untouched (prints exists:), and --detect pre-populates commented-out runner suggestions.
  • AC-5: deferred-user — /sdlc:find-quality-checks is a SKILL.md procedure (probe via detector → AskUserQuestion multiSelect with prior choices pre-selected → write back). Cannot be exercised end-to-end from sub-agent context; please run it interactively to confirm the idempotent re-write loop.
  • AC-6 (absorbed clear-log-line): auto — the executor emits warning: no quality_checks configured in <path> on stderr (empty list) and config file not found: <path> on stderr (missing file), both with exit 2. task-work Step 7 surfaces these warn-and-skip cases explicitly with no phantom just … invocation. The exact “Step 7:… — skipping” wording from the AC example isn’t used verbatim; the substance (single clear line per skipped phase, no phantom runner) is satisfied. Spot-check the user-facing message wording if you want byte-level fidelity to the absorbed example.
  • AC-7: auto — plugin/scripts/detect_quality_runners.py --project-root . returns valid JSON describing 19 python-script runners in this repo. Independently invokable.
  • AC-8: auto — three modes exercised directly: --line (OK 2/2 / FAIL <cmd>), --json (per-command {command, exit_code, duration_ms, stdout, stderr} plus passed/total), --log (streamed [N/total] <cmd> prefix). Mixed pass/fail returns non-zero. task-work Step 7 invokes the executor exclusively at line 213; no direct shell-outs remain.
  • Phasing the implementation into 7 atomic commits (one per logical unit) kept the diff easy to review and made each step independently reversible.
  • Pre-deciding the script-placement question (plugin/scripts/ vs co-located under a skill) and the schema-doc location (flat convention doc, not a new entity under plugin/entities/) in the sub-agent brief eliminated bikeshedding during implementation.
  • Eat-your-own-dogfood — running the new executor against the project’s own sdlc.yaml as the final quality gate — produced OK 5/5 and validated the AC-1 contract directly.
  • Sub-agent honored plugin/skills/CLAUDE.md conventions (no piped gating commands, no duplicated prose across SKILL.md files) without being reminded mid-implementation.
  • Two spec-drift fragments survived sub-agent implementation: (final name TBD) in Approach unit 2 and (or final name) in AC-5 — both referenced the abandoned configure-quality-checks name. Only surfaced when I grepped the spec post-implementation. Fix: the implementation-sub-agent brief should include “search the task spec for any TBD / ‘or final name’ / ‘(pick one)’ phrases and resolve them” as an explicit Phase 7 sub-step. Better still: /sdlc:task-ensure-ready could flag those phrases as readiness warnings before the implementation gate so the spec is name-clean from the start. → T-KG6Y-task-ensure-ready-flags-spec-placeholders
  • This task modifies the very skill (plugin/skills/task-work/SKILL.md) that drove its execution. Worked fine because the runtime version is loaded from the global plugin path, not the worktree — but there’s no mechanism that surfaces this to the orchestrator at task pickup. Fix: /sdlc:task-work could detect when a task’s ## Files to touch list includes its own SKILL.md and emit a one-line note at Step 1 explaining the runtime/worktree split, so the operator doesn’t worry about the recursion mid-flight. → T-FWZH-task-work-detects-self-modifying-skill
  • The detector finds runners by file probe but cannot judge which constitute “the project’s quality gates” — quality_checks: curation is inherently a judgment call. This is the failure mode that motivated /sdlc:find-quality-checks (the interactive curator) and is documented design, not a gap. Noting for the aggregate signal.
  • Step 4 worktree-init (mise trust && just setup-worktree) is still hard-coded; the spec’s ## Out of scope documents this as the obvious next follow-up. Reserving the worktree_init: key in sdlc.yaml without consuming it splits the surface area cleanly between this PR and the next. → T-K7FR-task-work-worktree-init-language-agnostic

← Back to Tasks