T-Y1JN-add-sdlc-reconcile-reporter
Status: closed/done · Impact: high · Complexity: large
Ship the sdlc reconcile command that builds a structured report comparing every authoritative
source of lease-protocol state (task frontmatter, active lease refs, archive refs, control-plane
ref, branches, PRs) and surfaces the 14 anomaly categories specified in the ADR’s Reconcile section.
Read-only by design — no --fix modes in this slice. This is the verification surface that
operators use to confirm the cutover migration worked, and the ongoing health check for distributed
lease state. Without reconcile, the only way to spot drift is by manually git ls-remote-ing and
cross-referencing PRs.
| Location | Role today |
|---|---|
plugin/scripts/sdlc_lease.py | Top-level CLI dispatcher. No reconcile subcommand exists. |
plugin/scripts/lease_cli/ | Per-subcommand handlers. No reconcile.py. |
plugin/lib/lease/ | Library exposes primitives, schemas, runtime helpers, cache, control-plane validator, migration helpers (after the migrate task lands). No reconcile detector library. |
docs/planning/decisions/github-ref-leases/protocol.md (section “Reconcile”, lines 1033–1084) | Canonical 14-category checklist that this task implements. |
docs/planning/decisions/github-ref-leases/protocol.md (section “Local Runtime Index for UI”, lines 572–621) | Defines the local-mirror semantics that reconcile’s FETCH-NAMESPACE refresh relies on. |
Operators currently have no automated way to spot the anomalies the ADR enumerates: expired leases, abandoned PRs, mismatched footers, post-mortem-after-closing violations, namespace conflicts, etc. Each requires manual inspection.
Proposed
Section titled “Proposed”Ship sdlc reconcile as a top-level subcommand that:
- Refreshes the local mirror via batch
FETCH-NAMESPACE refs/sdlc/*and reports the count of pruned-stale-locals. - Loads five sources of truth in parallel: task frontmatter (all of
docs/planning/tasks/*.md), active task leases (refs/sdlc/tasks/*), active operation leases (refs/sdlc/ops/*), archive refs (refs/sdlc/archive/*), the control-plane ref, open + merged PRs (viagh pr list), and local task branches (refs/heads/task/*,refs/heads/feat/*legacy). - Runs 14 anomaly detectors independently — each is a small pure function over the loaded state
returning a list of
AnomalyRecord. - Emits the report in two modes: human-readable plaintext (default) grouped by category with
counts and per-row detail; structured JSON (
--json) for machine consumers (CI, dashboards, future--fixplumbing). - Exit code reflects severity: 0 (clean), 1 (warnings present), 2 (critical anomalies present). Critical set: namespace-conflict, duplicate-or-malformed-payloads, lifecycle-lease-missing-task-file.
- Scopable via
--task <id>to scope the report to one task’s frontmatter+lease+PR+branch consistency.
Approach
Section titled “Approach”-
Add
plugin/lib/lease/reconcile.pyas the detector library. Defines:-
AnomalyRecorddataclass:{category: str, severity: Literal["info","warning","critical"], task_id: str|None, ref: str|None, description: str, evidence: dict} -
ReconcileStatedataclass holding all loaded sources of truth (task_frontmatter,task_leases,op_leases,archive_refs,control_plane,prs_open,prs_merged,branches). -
load_reconcile_state(authority, project_root, cwd=None)— runs the batch FETCH-NAMESPACE, loads everything else, returnsReconcileState. Records the prune count. -
14 detector functions, one per anomaly category, each
(state: ReconcileState) -> list[AnomalyRecord].
-
-
Implement the 14 detectors (one per ADR bullet):
detect_expired_task_leases—expires_at + grace < nowfor active task leases.detect_abandoned_awaiting_review—awaiting-reviewleases withlast_phase_transition>30 days ago and no PR activity in that window.detect_stale_or_incompatible_client_versions— leases whosesdlc_versiondiffers from the control-plane’ssdlc_versionby major segment.detect_lifecycle_lease_missing_task_file—refs/sdlc/tasks/<id>exists butdocs/planning/tasks/<id>.mddoesn’t.detect_op_leases_with_results_not_applied— operation leases at phasedonewithresult_refset but the result ref never consumed (heuristic: archive ref absent).detect_tasks_in_progress_without_lease— task frontmatterstatus: in-progresswith norefs/sdlc/tasks/<id>ref.detect_leases_with_merged_pr_but_no_close_out— open task lease whose linked PR is merged but archive ref absent.detect_task_branches_without_lease—refs/heads/task/<id>orrefs/heads/feat/<id>exists with no corresponding lease ref.detect_duplicate_or_malformed_lease_payloads— lease ref whose blob fails Pydantic validation, or two refs collide ontask_id.detect_pr_footer_missing_or_mismatched— PR body lacks the footer, OR footer’slease_iddoesn’t match any active or archived lease for that task.detect_post_mortem_edited_after_closing— task file’s “Post-mortem” section modified after the lease transitioned toclosing(compare commit timestamp of task file change vs lease’s phase-transition commit).detect_clock_skew_from_heartbeat_pattern— sample the last 30 active leases’ commit timestamps vs embeddedexpires_at - TTL; flag persistent one-directional skew >TTL/4.detect_heartbeat_rate_above_floor— leases whose commit cadence exceeds the configured floor (heartbeat-rate denial-of-service signal).detect_namespace_conflict_ref_present— a literalrefs/sdlcref exists (blocks creation of children) — uses the slice 1 namespace-conflict guard library.
-
Build
plugin/scripts/lease_cli/reconcile.pyas the CLI handler. Callsload_reconcile_state, runs each detector, aggregatesAnomalyRecords into the report shape, emits per the format flag. -
Wire
reconcileintosdlc_lease.pytop-level dispatch assdlc reconcile [--task <id>] [--json] [--include <category>...] [--exclude <category>...]. -
Output format — plaintext default: top-level summary block (overall counts by severity + prune count); per-category sections with header
### <category> — <count> records (<severity>), each record formatted as<task_id-or-ref>: <description>. Footer with exit code rationale. -
Output format — JSON (
--json):{summary: {total: N, by_severity: {critical: N, warning: N, info: N}, by_category: {...}, prune_count: N}, anomalies: [AnomalyRecord, ...], metadata: {authority, sdlc_version, timestamp_utc}} -
Add
--task <id>scope filter to narrow detectors to records matching that task_id; if a detector is global (e.g., namespace-conflict), it still runs and reports. -
Tests in
plugin/lib/lease/tests/test_reconcile.py— each of the 14 detectors gets a known-positive (anomaly present) and known-negative (anomaly absent) test against the local-bare-repo authority fixture. CLI integration tests inplugin/scripts/lease_cli/tests/test_reconcile.pycover plaintext output, JSON output, exit codes,--taskscoping. -
Document the subcommand in
plugin/scripts/lease_cli/README.md: signature, exit codes, category list with definitions and severity, worked examples for both output modes.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/lib/lease/reconcile.py | new | AnomalyRecord, ReconcileState, load_reconcile_state, 14 detector functions. |
plugin/lib/lease/__init__.py | modify | Re-export reconcile types and load_reconcile_state. |
plugin/scripts/lease_cli/reconcile.py | new | CLI handler — runs detectors, emits report in plaintext or JSON. |
plugin/scripts/sdlc_lease.py | modify | Add reconcile to top-level argparse dispatch + module docstring. |
plugin/lib/lease/tests/test_reconcile.py | new | Per-detector unit tests with positive + negative fixtures. |
plugin/scripts/lease_cli/tests/test_reconcile.py | new | CLI integration tests covering output formats, exit codes, --task filter. |
plugin/scripts/lease_cli/README.md | modify | Document the reconcile subcommand, category list, exit codes. |
Acceptance criteria
Section titled “Acceptance criteria”-
AC-1:
sdlc reconcilebegins each run with a batchFETCH-NAMESPACE refs/sdlc/*and reports the count of pruned-stale-locals in the summary block. Verified by injecting a stale local ref before run and asserting it’s pruned + counted. -
AC-2: Each of the 14 anomaly categories has a dedicated detector function with at least one known-positive test (anomaly correctly detected) and one known-negative test (clean state produces no anomaly).
-
AC-3: Plaintext output (default) groups anomalies by category with counts and per-row detail lines. Verified against fixture-generated state with multiple anomaly types.
-
AC-4:
sdlc reconcile --jsonemits valid JSON matching the documented schema ({summary, anomalies, metadata}). Verified by parsing the output and asserting structure. -
AC-5: Exit code is 0 (no anomalies), 1 (warnings), or 2 (critical anomalies present). Critical set: namespace-conflict, duplicate-or-malformed-payloads, lifecycle-lease-missing-task-file.
-
AC-6: Reconcile is read-only — no refs, no PR bodies, no files are mutated. Verified by running against a known-good fixture, recording
git ls-remote+gh pr liststate before/after, and asserting identity. -
AC-7: The clock-skew detector samples lease-commit timestamps across the last 30 active leases (or all if fewer) and flags only persistent one-directional skew >TTL/4. Verified by fixture with simulated clock-skew commit timestamps.
-
AC-8:
sdlc reconcile --task <id>scopes per-task detectors to that one task; global detectors (namespace-conflict, control-plane absent) still run and report. Verified by comparing scoped vs unscoped runs against the same state. -
AC-9:
--includeand--excludeflags filter detectors by category name; mutually-exclusive flags surface a clear argparse error. -
AC-10: All tests pass:
uv run --with pytest --with pyyaml --with pydantic python -m pytest plugin/lib/lease/tests/test_reconcile.py plugin/scripts/lease_cli/tests/test_reconcile.pyQuality checks pass:
plugin/scripts/run_quality_checks.py --config sdlc.yaml. -
AC-11: Reconcile against a freshly-migrated repo (post
sdlc lease migrate) reports zero critical anomalies — verifies that the migrate task’s output is reconcile-clean by construction. -
AC-12: Reconcile output is deterministic — given identical state, two consecutive runs produce byte-identical reports (modulo the timestamp_utc metadata field). Tested by snapshot comparison.
Out of scope
Section titled “Out of scope”--fixmodes — slice 3 reconcile ships as read-only only. Safe cleanups (e.g., archive merged-but-not-closed leases) defer to a later slice.- New anomaly categories beyond the ADR’s 14. Future categories belong in dedicated follow-up tasks
(and likely a sub-document under
docs/planning/decisions/github-ref-leases/). - A reconcile web UI / Grafana dashboard — JSON output exists so consumers can build that separately.
- Cross-repo reconcile — single-repo scope only. Multi-repo coordination belongs in the operation-leases slice (slice 4).
Dependencies
Section titled “Dependencies”- E0002 slice 2 cutover PR (#127) must merge first — reconcile uses slice 2’s library helpers
(
fetch_namespace,validate_dict, etc.). - T-DNN5-add-sdlc-lease-migrate-and-control-plane-bootstrap should land first so reconcile can be exercised against a migrated repo for AC-11. Reconcile does not strictly depend on migrate (it runs against any repo state, including a pre-migration one), but the most useful integration test is post-migrate.
- T-S0PK-add-lease-protocol-library-and-schemas — provides
fetch_namespace, the four payload schemas,validate_dict. Already merged. - T-K3RR-add-lease-namespace-conflict-guard — provides the namespace-conflict detector primitive. Already merged.
Discovery context
Section titled “Discovery context”- This task closes E0002 Rollout Plan item 8 (“Add
sdlc reconcilereporting against the checklist in Reconcile section”). Defined as a stub in the epic; this task is the concrete spec. - The ADR’s Reconcile section (protocol.md lines 1033–1084) enumerates the 14 categories verbatim; this task does not invent new categories. Future categories belong in dedicated follow-up tasks per the ADR’s “Likely expansion areas” note (README.md#expanding-this-design).
- Decision to ship read-only (no
--fix) follows the ADR’s framing: “Reconcile does not mutate by default. […] Later, reconcile may gain explicit--fixmodes for safe cleanups.” Adding--fixhere would require operator-confirmation UX work that’s out of slice 3’s scope.
Dedup search (spawn-from-post-mortem)
Section titled “Dedup search (spawn-from-post-mortem)”Bullet: The baseline showed 198 pre-existing findings vs Task B’s 0. Most are from the slice 4a schema-bump fixture churn; some may be from the dashboard merge. Worth surfacing in reconcile or in /sdlc:entities-migrate —include-closed to clear closed-task drift. Keywords searched: entities-migrate, include-closed, pre-existing, schema-bump, closed-task, dashboard, surfacing, reconcile Excluded: 2026-05-27-task-close-out-verifies-prs-against-merged-pr Top candidates (score / status / headline):
- 39 / planning/draft / 2026-05-25-add-sdlc-reconcile-reporter — Add
sdlc reconcile— read-only multi-source anomaly reporter - 35 / closed/done / 2026-05-19-implement-entities-migrate — Implement /sdlc:entities-migrate to apply mechanical schema-drift fixes
- 32 / closed/done / 2026-05-21-run-quality-checks-isolates-pre-existing-drift — run_quality_checks.py only fails on drift the current branch introduced
- 28 / closed/done / 2026-05-22-restructure-task-touchpoints-as-a-table-with-symbol-dir-glob — Restructure task touchpoints as a table with symbol/dir/glob citation grammar
- 24 / closed/done / 2026-05-19-schema-bump-tasks-handle-missing-version — Schema-bump task template enumerates the missing-schema_version case explicitly Decision: LINKED-EXISTING 2026-05-25-add-sdlc-reconcile-reporter Originating task: 2026-05-27-task-close-out-verifies-prs-against-merged-pr
Depends on
Section titled “Depends on”T-FFHN-github-ref-leases-coordination