Skip to content

T-J1M3-sdlc-yaml-json-schema-and-validator

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

sdlc.yaml now has more than one top-level key (quality_checks: shipped first; the orchestrator: block landed in PR #46), so its shape is no longer self-evident from a single example. Adopt the same pattern the plugin already uses for entity frontmatter — a JSON Schema next to its prose documentation, plus a small Python validator — so the file’s shape is machine-checkable and the documentation is generated from (or co-located with) the schema rather than drifting against the consumers.

  • The shape of sdlc.yaml is documented prose-only at plugin/conventions/sdlc-yaml.md. There is no machine-readable schema and no validator.
  • Consumers each load and validate their own slice ad hoc:
    • plugin/scripts/count_inflight_tasks.py load_config() (lines ~142–168) parses YAML, walks to orchestrator:, type- checks max_implementations / max_awaiting_review (int,

      = 0), and silently falls back to defaults on any failure.

    • plugin/scripts/run_quality_checks.py parses quality_checks: similarly.
  • The plugin already has the pattern we want, applied to entity frontmatter: plugin/entities/task/schema.json is the JSON Schema, and plugin/validators/validate_frontmatter.py is the shared validator that every consumer calls. That’s the shape this task lifts over to sdlc.yaml.

A new plugin/schemas/sdlc-yaml.schema.json (or plugin/entities/sdlc-yaml/schema.json, TBD in Approach Step 1) describing the document’s full shape — including the existing quality_checks: array and the new orchestrator: block with its two integer keys, their defaults, and their minima — with title: / description: populated on every field so the schema itself is self-documenting. A new plugin/validators/validate_sdlc_yaml.py (mirroring validate_frontmatter.py’s signature and exit-code conventions) that validates a given sdlc.yaml against the schema. The prose doc at plugin/conventions/sdlc-yaml.md is rewritten to link to (and where useful, embed snippets from) the schema rather than restating the field list.

  1. Place the schema at plugin/schemas/sdlc-yaml.schema.json (NOT under plugin/entities/). sdlc.yaml is operational config and intentionally not an entity per the prose claim in plugin/conventions/sdlc-yaml.md; siting it under plugin/schemas/ keeps the entity-audit/migrate machinery from treating it as one. Rejected alternative: plugin/entities/sdlc-yaml/schema.json (would invite the audit machinery into it).
  2. Author the schema. Initial scope = the two blocks shipping today:
    • quality_checks:array of string shell verbs.
    • orchestrator:object with max_implementations: integer, minimum: 0, default: 5 and max_awaiting_review: integer, minimum: 0, default: 20.
    • Note the reserved worktree_init: key in the schema as description:-only or via x-reserved: true; do not enforce a shape until that consumer ships.
  3. Implement plugin/validators/validate_sdlc_yaml.py mirroring validate_frontmatter.py:
    • CLI: validate_sdlc_yaml.py <path-to-sdlc.yaml>.
    • Exit 0 on pass; non-zero with a human-readable list of errors on fail.
    • Use jsonschema (already a transitive dep — verify before adding) for the actual validation.
  4. Rewrite plugin/conventions/sdlc-yaml.md so the field-by-field reference is sourced from (or trivially derived from) the schema’s title: / description: blocks, rather than restating shapes prose-only. Keep the high-level “why” and the consumer-list at the bottom in prose.
  5. Refactor the two ad-hoc loaders to call the validator (or a shared library function) instead of hand-rolling type checks:
    • count_inflight_tasks.py load_config() — validate first, then read values knowing the shape is sound.
    • run_quality_checks.py quality_checks: loader — same.
  6. Add a smoke test (or fixture under plugin/validators/tests/) covering: valid full file, missing orchestrator: block, wrong type on max_implementations:, unknown top-level key.
  • plugin/schemas/sdlc-yaml.schema.json (new) — JSON Schema for sdlc.yaml.
  • plugin/validators/validate_sdlc_yaml.py (new) — validator CLI.
  • plugin/conventions/sdlc-yaml.md — rewritten to point at the schema; prose becomes the “why” not the field list.
  • plugin/scripts/count_inflight_tasks.pyload_config() uses the shared validator / loader.
  • plugin/scripts/run_quality_checks.pyquality_checks: loader uses the shared validator / loader.
  • plugin/validators/tests/ (new fixtures) — coverage cases.
  • AC-1: validate_sdlc_yaml.py against a known-good sdlc.yaml exits 0 with no stdout. (auto)
  • AC-2: validate_sdlc_yaml.py against a file with orchestrator.max_implementations: "five" exits non-zero and the error message names both the key and the expected type. (auto)
  • AC-3: count_inflight_tasks.py and run_quality_checks.py no longer contain hand-rolled type checks for keys covered by the schema; their loaders route through the shared validator. (auto via grep)
  • AC-4: plugin/conventions/sdlc-yaml.md contains a literal path reference to plugin/schemas/sdlc-yaml.schema.json. command grep -F 'plugin/schemas/sdlc-yaml.schema.json' plugin/conventions/sdlc-yaml.md exits 0.
  • Enforcing the worktree_init: shape. Still reserved; this task only documents it in the schema as x-reserved.
  • Adding a /sdlc: skill that interactively edits sdlc.yaml. /sdlc:find-quality-checks already covers quality_checks:; an orchestrator: editor is a separate task if it’s wanted.
  • Bumping the entities-audit / entities-migrate machinery to treat sdlc.yaml as an entity. The prose doc explicitly argues against entity-fying it; this task respects that.
  • Migrating projects with existing sdlc.yaml files. The schema is permissive enough that existing files pass; no migration needed.
  • none

Spawned from PR #46 review comment by sksizer on plugin/conventions/sdlc-yaml.md:78 — “Would it make sense to define a JSON Schema for the Yaml with most of the documentation inline so that it can dictate shape (with a python tool similar to our frontmatter schema validator) next to its documentation?”

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

  • AC-1: auto — validate_sdlc_yaml.py --quiet <project sdlc.yaml> exits 0 with no stdout.
  • AC-2: auto — validate_sdlc_yaml.py against a fixture with orchestrator.max_implementations: "five" exits 1; stdout contains at /orchestrator/max_implementations: 'five' is not of type 'integer' (names both key and expected type).
  • AC-3: auto via grep — command grep -n "isinstance.*int" plugin/scripts/count_inflight_tasks.py no longer shows the old max_impl/max_rev int-and-non-negative guards inside load_config; the loader now subprocesses plugin/validators/validate_sdlc_yaml.py before reading the block. In run_quality_checks.py, hand-rolled type checks survive only in the explicit non-schema-owned-key fallback (so the executor stays usable for future ad-hoc keys); schema-owned keys (quality_checks, worktree_init) route through the shared validator.
  • AC-4: auto — command grep -F 'plugin/schemas/sdlc-yaml.schema.json' plugin/conventions/sdlc-yaml.md exits 0 (3 occurrences).
  • The validate_frontmatter.py shape ported over cleanly — same CLI conventions, same exit-code policy, same format_error shape. Zero design discussion needed for the validator’s surface.
  • Existing PEP-723 uv-script bootstrap meant the validator script could declare its own jsonschema/pyyaml deps without touching any project-level Python packaging.
  • The schema’s additionalProperties: false immediately caught the quality_chex: typo fixture in tests — the schema is doing real work, not just describing shapes.
  • start_task.py hit a frontmatter rebase conflict (exit 3) because the task file had a pre-existing readiness_verified_at: stamp from a prior ensure-ready run, and the main-side start-commit bumped last_reviewed: while the feat-side verify-commit re-stamped readiness_verified_at: — a structural conflict that recurs whenever ensure-ready ran before task-work — start_task.py should either detect this exact two-key conflict and auto-resolve (take the newer stamp + today’s last_reviewed) or have task-work Step 5b document the canonical resolution so sub-agents don’t have to reason about it. → T-H0W9-task-work-rebase-frontmatter-conflict
  • Task spec said to document worktree_init: in the schema as x-reserved: true since “that consumer hasn’t shipped yet” — but the convention doc and the executor’s --key worktree_init invocation show that consumer has shipped. The task body drifted from reality between authoring and pickup — task-ensure-ready’s relevance check should grep for cited “not yet shipped” claims against the codebase so the implementer doesn’t have to catch the contradiction themselves. → T-780G-task-ensure-ready-checks-shipped-claims
  • Cross-uv-script imports are awkward — the cleanest reuse path for validate_loaded() from sibling scripts would be a regular Python module under plugin/lib/ that both the CLI and consumers import, but each script’s PEP-723 dep-block makes that non-trivial. Settled for subprocess’ing the CLI, which is robust but adds startup cost per consumer invocation. A plugin/lib/sdlc_yaml.py (or similar) shared helper would reduce duplication and shave the subprocess overhead — worth a follow-up. → T-1PII-shared-sdlc-yaml-loader-lib

← Back to Tasks