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.yamlis documented prose-only atplugin/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.pyload_config()(lines ~142–168) parses YAML, walks toorchestrator:, type- checksmax_implementations/max_awaiting_review(int,= 0), and silently falls back to defaults on any failure.
plugin/scripts/run_quality_checks.pyparsesquality_checks:similarly.
- The plugin already has the pattern we want, applied to entity
frontmatter:
plugin/entities/task/schema.jsonis the JSON Schema, andplugin/validators/validate_frontmatter.pyis the shared validator that every consumer calls. That’s the shape this task lifts over tosdlc.yaml.
Proposed
Section titled “Proposed”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.
Approach
Section titled “Approach”- Place the schema at
plugin/schemas/sdlc-yaml.schema.json(NOT underplugin/entities/).sdlc.yamlis operational config and intentionally not an entity per the prose claim inplugin/conventions/sdlc-yaml.md; siting it underplugin/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). - Author the schema. Initial scope = the two blocks shipping
today:
quality_checks:—arrayofstringshell verbs.orchestrator:—objectwithmax_implementations: integer, minimum: 0, default: 5andmax_awaiting_review: integer, minimum: 0, default: 20.- Note the reserved
worktree_init:key in the schema asdescription:-only or viax-reserved: true; do not enforce a shape until that consumer ships.
- Implement
plugin/validators/validate_sdlc_yaml.pymirroringvalidate_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.
- CLI:
- Rewrite
plugin/conventions/sdlc-yaml.mdso the field-by-field reference is sourced from (or trivially derived from) the schema’stitle:/description:blocks, rather than restating shapes prose-only. Keep the high-level “why” and the consumer-list at the bottom in prose. - Refactor the two ad-hoc loaders to call the validator (or a
shared library function) instead of hand-rolling type checks:
count_inflight_tasks.pyload_config()— validate first, then read values knowing the shape is sound.run_quality_checks.pyquality_checks:loader — same.
- Add a smoke test (or fixture under
plugin/validators/tests/) covering: valid full file, missingorchestrator:block, wrong type onmax_implementations:, unknown top-level key.
Files to touch
Section titled “Files to touch”plugin/schemas/sdlc-yaml.schema.json(new) — JSON Schema forsdlc.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.py—load_config()uses the shared validator / loader.plugin/scripts/run_quality_checks.py—quality_checks:loader uses the shared validator / loader.plugin/validators/tests/(new fixtures) — coverage cases.
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
validate_sdlc_yaml.pyagainst a known-goodsdlc.yamlexits 0 with no stdout. (auto) - AC-2:
validate_sdlc_yaml.pyagainst a file withorchestrator.max_implementations: "five"exits non-zero and the error message names both the key and the expected type. (auto) - AC-3:
count_inflight_tasks.pyandrun_quality_checks.pyno 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.mdcontains a literal path reference toplugin/schemas/sdlc-yaml.schema.json.command grep -F 'plugin/schemas/sdlc-yaml.schema.json' plugin/conventions/sdlc-yaml.mdexits 0.
Out of scope
Section titled “Out of scope”- Enforcing the
worktree_init:shape. Still reserved; this task only documents it in the schema asx-reserved. - Adding a
/sdlc:skill that interactively editssdlc.yaml./sdlc:find-quality-checksalready coversquality_checks:; anorchestrator:editor is a separate task if it’s wanted. - Bumping the
entities-audit/entities-migratemachinery to treatsdlc.yamlas an entity. The prose doc explicitly argues against entity-fying it; this task respects that. - Migrating projects with existing
sdlc.yamlfiles. The schema is permissive enough that existing files pass; no migration needed.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”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?”
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-21. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
validate_sdlc_yaml.py --quiet <project sdlc.yaml>exits 0 with no stdout. - AC-2: auto —
validate_sdlc_yaml.pyagainst a fixture withorchestrator.max_implementations: "five"exits 1; stdout containsat /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.pyno longer shows the oldmax_impl/max_revint-and-non-negative guards insideload_config; the loader now subprocessesplugin/validators/validate_sdlc_yaml.pybefore reading the block. Inrun_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.mdexits 0 (3 occurrences).
What worked
Section titled “What worked”- The validate_frontmatter.py shape ported over cleanly — same CLI
conventions, same exit-code policy, same
format_errorshape. 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: falseimmediately caught thequality_chex:typo fixture in tests — the schema is doing real work, not just describing shapes.
Friction and automation gaps
Section titled “Friction and automation gaps”start_task.pyhit a frontmatter rebase conflict (exit 3) because the task file had a pre-existingreadiness_verified_at:stamp from a prior ensure-ready run, and the main-side start-commit bumpedlast_reviewed:while the feat-side verify-commit re-stampedreadiness_verified_at:— a structural conflict that recurs whenever ensure-ready ran before task-work —start_task.pyshould 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 asx-reserved: truesince “that consumer hasn’t shipped yet” — but the convention doc and the executor’s--key worktree_initinvocation 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 underplugin/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. Aplugin/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
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-780G-task-ensure-ready-checks-shipped-claims — flag stale “not yet shipped” claims in spec prose (created).
- T-1PII-shared-sdlc-yaml-loader-lib — extract a
plugin/lib/sdlc_yaml.pyso consumers stop subprocess’ing the validator CLI (created). - T-H0W9-task-work-rebase-frontmatter-conflict — already covers the start_task.py / ensure-ready rebase conflict (linked existing).