Skip to content

feat: add pt hygiene --json portfolio hygiene state command#126

Merged
eriksjaastad merged 3 commits into
mainfrom
feat/pt-hygiene-command
May 11, 2026
Merged

feat: add pt hygiene --json portfolio hygiene state command#126
eriksjaastad merged 3 commits into
mainfrom
feat/pt-hygiene-command

Conversation

@manager-identity
Copy link
Copy Markdown
Contributor

Summary

  • Adds pt hygiene [--json] [--project NAME] [--quiet] — a read-only portfolio scanner that detects hygiene drift across every git repo under PROJECTS_BASE_DIR.
  • Six detections: dirty working tree (excluding the always-dirty PROGRESS.md), local-only branches, branches ahead of remote, stashes, open bot-authored PRs older than 24h (graceful degradation if gh unavailable), stale PROGRESS.md (older than 7 days while non-PROGRESS dirty files exist).
  • Stable JSON envelope pt.hygiene.v1. Exit 0 clean / 6 (EXIT_HYGIENE_FINDING) on any finding — for cron and Phase C's Stop hook to branch on.
  • Sandbox-friendly: never shells through Doppler. Honors PT_NO_BANNER, PT_SUPPRESS_MIGRATION_WARNING, PT_SKIP_DOPPLER.

Phase context

Phase A of the Locked Project Hygiene Workflow (umbrella card #6118). Foundation for B (branch-on-first-edit hook), C (Stop-event four-condition gate), and E (portfolio rollout).

Per-repo result schema contract

Two disjoint shapes:

  • Success: {project, path, clean: bool, findings: {dirty_tree, local_only_branches, branches_ahead_of_remote, stashes, open_pr_drift, stale_progress_md}} — every finding key is always present.
  • Error (broken repo isolated from sibling scans): {project, path, clean: false, error: {class, message}}findings is omitted.

Consumers MUST check error before iterating findings. Documented explicitly in USAGE.md.

Notable robustness wins discovered during review

  • Per-repo isolation: the main scan loop wraps each repo in try/except Exception, surfacing one broken repo as an error entry and continuing — one bad repo does not crash the whole portfolio scan.
  • branches_ahead rewrite: the original implementation used %(ahead-behind:origin/HEAD) which silently broke on fresh clones with no origin/HEAD set. Now iterates for-each-ref refs/heads and resolves each branch's upstream individually via git rev-list --count, robust against missing origin/HEAD, detached HEAD, and per-branch failures.

Test plan

  • 17/17 tests green (pytest tests/test_hygiene_cli.py -v)
  • Per-repo isolation verified (broken repo via PermissionError injection — siblings still scan, exit code still 6)
  • PROGRESS.md exclusion is consistent across dirty_tree and stale_progress_md (shared helper _hygiene_non_progress_dirty_files)
  • Schema contract test asserts error entries omit findings and clean entries carry all 6 finding keys
  • gh graceful degradation tested (missing binary, non-zero exit)
  • After merge: pt hygiene --json on this machine produces a valid pt.hygiene.v1 payload covering all repos under ~/projects/.
  • After merge: pt hygiene (no --json) produces readable per-repo summaries; broken repos clearly tagged as scan error: ….

Code review history

  • Initial commit 9e6eb40: FAIL — 2 HIGH (no per-repo isolation, PROGRESS.md exclusion inconsistency in stale check), 1 MEDIUM (test gaps), 1 LOW.
  • Fixup 4c98935: FAIL — 1 MEDIUM (error-result schema split). Original 4 findings cleanly resolved, but error entries had findings: {} while success entries had full sub-keys, breaking schema-conformant consumers.
  • Final 752980a: PASS — error entries omit findings entirely; consumers branch on error first; contract documented in USAGE.md.

Related

  • Closes #6149
  • Parent: #6118

manager-identity Bot and others added 3 commits May 5, 2026 23:35
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Per-repo scan loop now isolates exceptions; broken repos surface as
  {error: {class, message}} entries while sibling repos still scan.
- Stale PROGRESS.md detection reuses _hygiene_non_progress_dirty_files
  so a repo whose only dirty file is PROGRESS.md no longer trips the
  finding (PROGRESS.md is the documented always-dirty file).
- _hygiene_branches_ahead rewritten to use rev-list per upstream-tracking
  branch; the previous %(ahead-behind:origin/HEAD) atom failed when
  origin/HEAD wasn't set (fresh clones, test fixtures).
- Added behavioral tests for: per-repo isolation, stale PROGRESS.md
  positive/negative cases, branch-ahead-of-remote with a real bare
  upstream, and open_pr_drift via mocked gh CLI (populated, gh missing,
  gh non-zero). Also asserts age_days key in schema completeness check.
  Test count: 9 -> 16.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…schema uniform (#6149)

Error result entries previously emitted findings: {} alongside an error
key. That split shape broke the schema contract: a naive consumer doing
for name, finding in repo["findings"].items() would silently read the
empty dict as "checked and found nothing" rather than "couldn't check."

Option A: drop findings from error entries entirely. Consumers MUST now
branch on "error" before accessing "findings"; absence makes wrong code
fail loud (KeyError) rather than reading misleading semantics.

Documented the disjoint shapes in USAGE.md and added a dedicated test
asserting the contract (error entries: findings absent; clean entries:
findings present with all six keys).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@manager-identity manager-identity Bot added the feature New feature label May 6, 2026
@eriksjaastad eriksjaastad merged commit dde0641 into main May 11, 2026
2 checks passed
@eriksjaastad eriksjaastad deleted the feat/pt-hygiene-command branch May 11, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant