Phase sdd rules
Skill zig999/siegard-code/dist/.claude/skills/phase-sdd-rules
Exit criteria checkers and worker routing table for the sdd (Specification-Driven Development) phase. Consumed by orchestrator-sdd.md to dispatch spec workers via select_worker.py and evaluate phase transition gates (check_handoff_manifest_approved, check_all_domains_validated, check_error_codes_synced). Includes check_structural_diff.py to determine if spec changes require domain worker dispatch during improve flows. Not user-invocable — orchestrators call scripts directly.From its SKILL.md
npx -y skills add zig999/siegard-code --skill phase-sdd-rulesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 9 stars9 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
- runs commandsInstructs the agent to run 8 commands, including `python3 .claude/skills/phase-sdd-rules/scripts/select_worker.py --task-type <type>` and 7 more.
SKILL.md
14.8 KB, ~3.8k tokens by cl100k_base, as published. Nobody here has run it
phase-sdd-rules
Phase rules skill for the sdd (Specification-Driven Development) phase.
Provides exit criteria checkers and worker routing table consumed by orchestrator-sdd.md.
Contract
The orchestrator calls this skill's scripts directly. No inter-skill communication envelope needed. Every script returns a JSON object to stdout and exits 0 on success or 1 on error.
Phase identity
| Field | Value |
|---|---|
phase_name | sdd |
order | 1 |
required | true |
worker_default | u-spec-writer |
Concurrency ceiling (RESOURCE_LIMITS)
The orchestrator MUST enforce these per-batch ceilings before each dispatch in Step 5.1:
effective_mode | Max concurrent workers per batch |
|---|---|
standard | 2 |
targeted | 1 |
The ceiling is enforced as a hard cap — the orchestrator may dispatch FEWER than the cap (e.g., if only one task is ready) but MUST NOT exceed it. Violation is a protocol violation per RESOURCE_LIMITS.
Worker routing table
Maps task.type to worker sub-agent. Consumed by the orchestrator dispatcher.
| task.type | worker subagent_type |
|---|---|
spec-triage | u-spec-triage |
spec-writer | u-spec-writer |
spec-reviewer | u-spec-reviewer |
spec-back | u-spec-back |
spec-front | u-spec-front |
spec-validator | u-spec-validator |
spec-compliance | u-spec-compliance |
* (default) | u-spec-writer |
scripts/select_worker.py
Returns the worker sub-agent name for a given task type.
Usage
python3 .claude/skills/phase-sdd-rules/scripts/select_worker.py \
--task-type <type>
Output (exit 0)
{"worker": "u-spec-writer", "task_type": "spec-writer", "phase": "sdd"}
Error (exit 1, stderr)
{"status": "error", "reason": "internal_error", "detail": "<message>"}
Exit criteria
All three criteria must be met before the sdd phase can transition.
Evaluated by orchestrator-sdd.md at the end of each cycle.
| Criterion | Script | Description |
|---|---|---|
handoff_manifest_approved | scripts/check_handoff_manifest_approved.py | handoff-manifest.yaml exists and Status: approved |
all_domains_validated | scripts/check_all_domains_validated.py | No INVALID status in _validation/ (scoped to the change's domains on /u-improve via --workflow-id) |
error_codes_synced | scripts/check_error_codes_synced.py | All error.code values in in-scope specs are in error-codes.md (scoped to touched domains on /u-improve via --workflow-id) |
spec_drift_reviewed | scripts/check_spec_drift_reviewed.py | Opt-in (R04d). With sdd_policy.drift_check = warn/required in .orch/config.json, a current /u-drift report must exist for the approved specs. OFF by default — vacuously met |
See exit-criteria.json for the machine-readable declaration.
Environment variables
| Variable | Default | Description |
|---|---|---|
ORCH_PROJECT_DIR | . | Project root |
SPECS_DIR | specs | Specs directory, relative to ORCH_PROJECT_DIR |
scripts/check_handoff_manifest_approved.py
Criterion: handoff-manifest.yaml exists in SPECS_DIR and contains Status: approved.
python3 .claude/skills/phase-sdd-rules/scripts/check_handoff_manifest_approved.py
Output schema:
{
"criterion": "handoff_manifest_approved",
"met": true,
"evidence": {
"file": "specs/handoff-manifest.yaml",
"exists": true,
"status_found": "approved"
}
}
scripts/check_all_domains_validated.py
Criterion: no INVALID status in any in-scope .yaml or .md file under SPECS_DIR/_validation/.
python3 .claude/skills/phase-sdd-rules/scripts/check_all_domains_validated.py [--workflow-id <wid>]
Scope (fix F1): with --workflow-id, an /u-improve gates only the domains the
change touched (scope.py). Untouched domains inherit their last verdict, so a
stale INVALID in an unrelated domain does not block the change — it is reported
under out_of_scope_invalid for audit. For u-spec / greenfield / un-derivable
scope the check stays global (every domain must be VALID). Without --workflow-id
it is global (prior behavior).
Output schema:
{
"criterion": "all_domains_validated",
"met": true,
"evidence": {
"validation_dir": "specs/_validation",
"exists": true,
"total": 5,
"passing": 5,
"failing": [],
"out_of_scope_invalid": [],
"scoped": false,
"scope_domains": null
}
}
met is false if the _validation/ directory does not exist or contains no in-scope files.
scripts/scope.py
Derives the set of domains a change actually touches, so the gate, the handoff
scan, and the orchestrator dispatch can restrict work to them (fix F1). Reads
triage.json.
python3 .claude/skills/phase-sdd-rules/scripts/scope.py --workflow-id <wid>
# → {"scoped": true, "domains": ["<affected>", ...]} /u-improve
# → {"scoped": false, "domains": null} u-spec / greenfield / un-derivable
scoped: false (domains null) always means "no scoping — evaluate every
domain" (never "empty scope"), keeping greenfield and legacy triage on prior
behavior. Also exposes domain_of_validation_file(filename) used by the gate and
the handoff scan to map a _validation/ artifact back to its domain.
scripts/check_spec_entry.py
Entry guard for /u-spec (R10). Answers one question — does {SPECS_DIR}/domains/
already hold a spec? — and projects what a full fan-out would cost. Consumed by the
/u-spec command in Initial Validation, before any event is appended.
python3 .claude/skills/phase-sdd-rules/scripts/check_spec_entry.py \
--specs-dir <dir> [--project-dir <dir>]
# exit 0 → {"entry":"greenfield", "domain_count":0, "domains":[], "projected":{...}}
# exit 3 → {"entry":"non_greenfield", "domain_count":N, "domains":[...], "projected":{...}}
Pairs with scope.py: that script returns scoped: false for the u-spec trigger by
design, so orchestrator-sdd dispatches the pipeline for every scanned domain. Correct
on an empty repository; on a populated one it turns an addition into a full re-spec that
outruns the per-session subagent spawn budget. This guard is what distinguishes the two
cases, and it is the only thing that did.
projected.wall_clock_minutes uses WALL_CLOCK_MINUTES_PER_WORKER = 6 — measured across
three real workflows (5.6 / 5.9 / 6.9 min per dispatched worker, over differing modes,
domain counts and change sizes). Its purpose is to make the price visible before it is
paid rather than after.
scripts/check_error_codes_synced.py
Criterion: every error.code / code: Exxx value found in an in-scope spec YAML/MD file is
registered in SPECS_DIR/error-codes.md. Trivially met if no error codes are defined in specs.
With --workflow-id, an /u-improve gates only the codes referenced by touched domains
(scope.py, fix F1) — an unregistered code living exclusively in untouched domains is reported
under out_of_scope_missing and does not block. Files outside domains/<slug>/ are always
in scope. Without --workflow-id (or for u-spec / greenfield) the check is global.
python3 .claude/skills/phase-sdd-rules/scripts/check_error_codes_synced.py [--workflow-id <wid>]
Output schema:
{
"criterion": "error_codes_synced",
"met": true,
"evidence": {
"error_codes_file": "specs/error-codes.md",
"error_codes_file_exists": true,
"spec_codes_found": ["E001", "E002"],
"registered_codes_count": 10,
"missing_codes": [],
"out_of_scope_missing": [],
"files_scanned": ["domain-auth.yaml", "domain-billing.yaml"],
"scoped": false,
"scope_domains": null
}
}
scripts/identify_invalid_domains.py
Utility (repair loop Step R2): lists domains whose validation report in
{SPECS_DIR}/_validation/ is INVALID and derives each one's defect origin from
the machine-readable {domain}-validation-result.yaml (blocking_issues[].responsible).
Feeds the SM's stage-granular repair (S16): origin "back" (all blocking issues
belong to u-spec-back) routes a reduced ["spec-back", "spec-validator"] repair
pipeline; any other origin (mixed, front, writer, missing or unparseable companion)
returns null and keeps the full pipeline — mis-attribution degrades to redundant
work, never to under-repair.
Usage
ORCH_PROJECT_DIR=<path> SPECS_DIR=<specs> python3 .claude/skills/phase-sdd-rules/scripts/identify_invalid_domains.py [--workflow-id <wid>]
With --workflow-id, an /u-improve restricts the repair-target set to the touched domains
(scope.py, fix F1): a stale INVALID report in an untouched domain goes to out_of_scope_invalid
and never enters invalid_domains — the repair loop must not dispatch workers for domains this
workflow did not touch. Without --workflow-id (or for u-spec / greenfield) the scan is global.
Output (exit 0)
{
"invalid_domains": ["chat", "ingestion"],
"defect_origins": {"chat": "back", "ingestion": null},
"out_of_scope_invalid": [],
"scoped": false
}
scripts/check_structural_diff.py
Utility (not an exit criterion): determines whether a spec change requires dispatching a domain
worker. Used by orchestrator-sdd.md during improve flows to decide if structural sections were
modified (endpoints, schemas, auth_rules, data_models, etc.) and a domain worker must run.
Safe fallback: if improve-scope.json is missing or the spec is not listed in scope, returns
domain_worker_required: true to avoid skipping required work.
Usage
ORCH_PROJECT_DIR=<path> python3 .claude/skills/phase-sdd-rules/scripts/check_structural_diff.py \
--workflow-id <wid> \
--spec-path <relative-path-to-spec>
Output (exit 0)
{
"domain_worker_required": true,
"changed_sections": ["endpoints", "schemas"],
"structural_sections_found": ["endpoints", "schemas"]
}
Structural sections that trigger domain_worker_required: true:
endpoints, schemas, error_codes, component_props, state_contracts,
data_models, auth_rules, event_types, api_contracts
scripts/generate_handoff_manifest.py
Utility (not an exit criterion): deterministically produces SPECS_DIR/handoff-manifest.yaml
from the validated specs on disk plus triage.json. Closes the gap where no pipeline worker
produced the manifest the SDD exit gate requires (the phase previously dead-ended at E08).
Invoked by orchestrator-sdd.md in Step 6 after check_all_domains_validated.py (or
check_all_improve_reviewers_completed.py in targeted mode) and check_error_codes_synced.py
pass, and before check_handoff_manifest_approved.py. Deterministic (no LLM): sha256 must be
exact and the output must round-trip through lib/minimal_yaml.py, which validate.py uses.
Usage
ORCH_PROJECT_DIR=<path> SPECS_DIR=<rel> \
python3 .claude/skills/phase-sdd-rules/scripts/generate_handoff_manifest.py \
--workflow-id <wid>
Behavior
- Enumerates domains via
glob domains/*/openapi.yaml; buildsdomains[],backend_package[](openapi + back-spec per domain are required by FLOW-037;error-codes/conventionsadded when present), and — only iffront/front.mdexists —frontend_artifacts+frontend_package[]. Omitting the frontend blocks lets the Dev orchestrator inferstack=be(back-only handoff). handoff.delivered_byis the constu-spec-orchestrator(required by FLOW-030);handoff.typeis derived from triage (new_domain/major_evolution/fast_track).change_summaryis emitted only for evolution handoffs.- sha256 of every package file is computed at generation time; paths are stored relative to
ORCH_PROJECT_DIRsovalidate.py(--specs-dir = ORCH_PROJECT_DIR) resolves them.
Output (exit 0 when status=ok, exit 1 when status=blocked)
{"status": "ok", "check": "handoff_manifest_generated",
"manifest_path": "specs/handoff-manifest.yaml", "manifest_id": "HANDOFF-20260601-120000",
"domains": ["auth"], "stack_implied": "be", "reason": "triage_loaded"}
Fail-closed: no domains, a missing required backend artifact, a handoff_allowed: false in
_validation/*-validation-result.yaml, a block_handoff / non_compliant in
_validation/*-compliance.yaml, or a triage stack/front mismatch (triage.stack ∈ {fullstack,
fe} but no front artifacts on disk — stack_mismatch_front_expected_but_missing, fix P0-1) yields
status: blocked without writing the manifest. The orchestrator treats a blocked generation as
criterion-not-met (Validation Repair Loop / E08).
scripts/check_spec_drift_reviewed.py
Exit criterion spec_drift_reviewed (R04d) — connects /u-drift to the phase boundary.
/u-drift does what the spec pipeline structurally cannot: it matches approved specs against
the code by exact keys and reports what diverged, with evidence per finding. It runs standalone
and has to be remembered, which is why a spec once declared three method signatures that do not
exist and another cited a grep nobody ran.
python3 .claude/skills/phase-sdd-rules/scripts/check_spec_drift_reviewed.py [--workflow-id <wid>]
Policy in .orch/config.json:
{"sdd_policy": {"drift_check": "off" | "warn" | "required"}}
| Policy | Missing / stale report | Critical findings |
|---|---|---|
off (default) | criterion vacuously met | not evaluated |
warn | reported, does not block | reported, does not block |
required | blocks the phase exit | blocks the phase exit |
Opt-in by design, not as a shortcut. /u-drift costs an LLM code-inventory pass. Making it
mandatory on every sdd phase would add an agent and several minutes to every workflow — including
the ones whose measured problem already is that the pipeline costs more than the change it
specifies. A gate that worsens that gets switched off wholesale; one the operator enables for the
handoffs that matter gets used.
"Reviewed" means the report exists and its spec_content_hash still matches the specs on
disk. A report generated before the specs changed describes a state that no longer exists — the
same staleness class R08 handles for validation verdicts.
What ships with it: 14 files
78.2 KB alongside SKILL.md, 13 of them executable
scripts/
- check_all_domains_validated.pyruns5.6 KB
- check_all_improve_reviewers_completed.pyruns3.6 KB
- check_error_codes_synced.pyruns6.0 KB
- check_handoff_manifest_approved.pyruns5.4 KB
- check_sdd_artifacts_committed.pyruns5.1 KB
- check_spec_drift_reviewed.pyruns6.3 KB
- check_spec_entry.pyruns4.1 KB
- check_structural_diff.pyruns2.9 KB
- generate_handoff_manifest.pyruns17.5 KB
- identify_invalid_domains.pyruns7.9 KB
- record_spec_baseline.pyruns6.2 KB
- scope.pyruns3.1 KB
- select_worker.pyruns1.8 KB
- exit-criteria.json2.7 KB