From 379ae6bb2c45fa3dc915320b34e051f7b5b23724 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sun, 12 Jul 2026 11:40:25 -0700 Subject: [PATCH 1/3] feat: fail closed on portfolio truth authority --- src/app/portfolio_truth.py | 2 +- src/portfolio_repository_state.py | 102 +++++++++++++++++++++++ src/portfolio_truth_reconcile.py | 46 ++++++++-- src/portfolio_truth_types.py | 20 ++++- src/portfolio_truth_validate.py | 7 ++ src/producer_preflight.py | 35 +++++++- tests/test_portfolio_repository_state.py | 55 ++++++++++++ tests/test_portfolio_truth.py | 35 +++++++- tests/test_producer_preflight.py | 6 ++ 9 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 src/portfolio_repository_state.py create mode 100644 tests/test_portfolio_repository_state.py diff --git a/src/app/portfolio_truth.py b/src/app/portfolio_truth.py index 987b789..dd64770 100644 --- a/src/app/portfolio_truth.py +++ b/src/app/portfolio_truth.py @@ -85,7 +85,7 @@ def run_portfolio_truth_mode(args: Any) -> None: producer_evidence=producer_evidence, producer_repo_root=producer_repo_root, require_producer_evidence=bool( - os.environ.get("GHRA_REQUIRE_PRODUCER_EVIDENCE") == "1" + os.environ.get("GHRA_REQUIRE_PRODUCER_EVIDENCE", "1") == "1" ), ) except (PortfolioTruthPublishError, ValueError) as exc: diff --git a/src/portfolio_repository_state.py b/src/portfolio_repository_state.py new file mode 100644 index 0000000..d8a13ab --- /dev/null +++ b/src/portfolio_repository_state.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import subprocess +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + + +def observe_repository_state(path: Path, *, observed_at: datetime) -> dict[str, Any]: + """Read local Git/worktree state without changing refs or exposing file names.""" + if not (path / ".git").exists(): + return { + "state": "not_a_repository", + "observed_at": observed_at.astimezone(UTC).isoformat(), + } + try: + head = _git(path, "rev-parse", "HEAD") + branch = _git(path, "branch", "--show-current") or None + dirty = _git(path, "status", "--porcelain", "--untracked-files=all") + upstream = _git_optional(path, "rev-parse", "--abbrev-ref", "@{upstream}") + ahead = behind = None + if upstream: + counts = _git(path, "rev-list", "--left-right", "--count", f"{upstream}...HEAD") + behind_text, ahead_text = counts.split() + behind, ahead = int(behind_text), int(ahead_text) + worktrees = [] + for item in _worktrees(path): + worktree_path = Path(item["path"]) + worktree_dirty = _git( + worktree_path, "status", "--porcelain", "--untracked-files=all" + ) + worktrees.append( + { + "path": str(worktree_path), + "head": item.get("head"), + "branch": item.get("branch"), + "detached": item.get("detached", False), + "dirty": bool(worktree_dirty), + "dirty_path_count": len(worktree_dirty.splitlines()) if worktree_dirty else 0, + } + ) + return { + "state": "observed", + "observed_at": observed_at.astimezone(UTC).isoformat(), + "local": { + "path": str(path), + "head": head, + "branch": branch, + "dirty": bool(dirty), + "dirty_path_count": len(dirty.splitlines()) if dirty else 0, + "upstream": upstream, + "upstream_observation_source": "local_tracking_ref" if upstream else "unavailable", + "ahead": ahead, + "behind": behind, + }, + "remote_default_branch": { + "state": "unknown", + "reason": "no independent live remote read was performed by portfolio generation", + }, + "worktrees": worktrees, + } + except (OSError, subprocess.CalledProcessError, ValueError) as exc: + return { + "state": "unknown", + "observed_at": observed_at.astimezone(UTC).isoformat(), + "reason": str(exc), + } + + +def _git(path: Path, *args: str) -> str: + return subprocess.run( + ["git", "-C", str(path), *args], check=True, capture_output=True, text=True + ).stdout.strip() + + +def _git_optional(path: Path, *args: str) -> str | None: + try: + return _git(path, *args) or None + except subprocess.CalledProcessError: + return None + + +def _worktrees(path: Path) -> list[dict[str, Any]]: + output = _git(path, "worktree", "list", "--porcelain") + items: list[dict[str, Any]] = [] + current: dict[str, Any] = {} + for line in output.splitlines() + [""]: + if not line: + if current: + items.append(current) + current = {} + continue + key, _, value = line.partition(" ") + if key == "worktree": + current["path"] = value + elif key == "HEAD": + current["head"] = value + elif key == "branch": + current["branch"] = value.removeprefix("refs/heads/") + elif key == "detached": + current["detached"] = True + return items diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index fff1e23..0ebe024 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -18,6 +18,7 @@ from src.portfolio_context_contract import has_substantive_readme_support from src.portfolio_pathing import build_operating_path_entry from src.portfolio_risk import build_risk_entry +from src.portfolio_repository_state import observe_repository_state from src.portfolio_truth_sources import ( WORKSPACE_DISCOVERY_POLICY_VERSION, discover_workspace_projects, @@ -337,6 +338,11 @@ def build_portfolio_truth_snapshot( notion_context_carried_forward=notion_context_carried_forward, prior_notion_generated_at=prior_notion_generated_at, ), + coverage=_build_coverage_envelope( + projects=projects, + notion_context_carried_forward=notion_context_carried_forward, + notion_context_rows=len(notion_context), + ), exclusions={ "policy_version": WORKSPACE_DISCOVERY_POLICY_VERSION, "counts": dict(sorted(exclusion_counts.items())), @@ -347,6 +353,24 @@ def build_portfolio_truth_snapshot( ) +def _build_coverage_envelope( + *, + projects: list[PortfolioTruthProject], + notion_context_carried_forward: bool, + notion_context_rows: int, +) -> list[dict[str, Any]]: + scanned = sum(project.security.alerts_available for project in projects) + git_observed = sum( + project.repository_state.get("state") == "observed" for project in projects + ) + return [ + {"source": "workspace", "state": "observed", "project_count": len(projects)}, + {"source": "git", "state": "observed" if git_observed else "unknown", "observed_count": git_observed, "project_count": len(projects)}, + {"source": "github_security", "state": "known" if scanned == len(projects) else "partial" if scanned else "unknown", "scanned_count": scanned, "project_count": len(projects)}, + {"source": "notion", "state": "carried_forward" if notion_context_carried_forward else "observed" if notion_context_rows else "unknown", "observed_count": notion_context_rows}, + ] + + def _build_input_envelope( *, workspace_root: Path, @@ -483,6 +507,7 @@ def _count(source: dict[str, Any], key: str) -> int: return SecurityFields( alerts_available=bool(dependabot.get("available", False)), + coverage_state="known" if dependabot.get("available", False) else "unknown", dependabot_critical=_count(dependabot, "critical"), dependabot_high=_count(dependabot, "high"), dependabot_medium=_count(dependabot, "medium"), @@ -701,6 +726,14 @@ def _build_truth_project( security_high_alerts=security.dependabot_high, security_critical_alerts=security.dependabot_critical, ) + if ( + not security.alerts_available + and risk_entry.get("risk_summary") == "No elevated risk factors." + ): + risk_entry["risk_summary"] = ( + "No non-security risk factors detected; security posture is unknown " + "because alert coverage is unavailable." + ) attention_state = _attention_state_for( activity_status=activity_status, archived=archived, @@ -860,6 +893,11 @@ def _build_truth_project( risk=risk, security=security, advisory=advisory, + repository_state=( + observe_repository_state(project_path, observed_at=now) + if project_path is not None and has_git + else {"state": "not_a_repository", "observed_at": now.isoformat()} + ), provenance=provenance, warnings=warnings, ) @@ -1062,17 +1100,15 @@ def _attention_state_for( return "archived" if operating_path == "experiment" or lifecycle_state == "experimental": return "experiment" + if lifecycle_state == "manual-only": + return "manual-only" if activity_status == "stale": # A declared finish path is itself an unresolved operator decision. It can # remain valid while the default branch is stale (for example, when work is # on a release branch or waiting at a human/publication gate), so do not # silently collapse it back into the parked pool. return "decision-needed" if operating_path == "finish" else "parked" - if ( - path_override == "investigate" - or not operating_path - or risk_entry.get("security_risk") - ): + if risk_entry.get("security_risk"): return "decision-needed" if activity_status in {"active", "recent"} and operating_path in { "maintain", diff --git a/src/portfolio_truth_types.py b/src/portfolio_truth_types.py index cc13f6b..45b9685 100644 --- a/src/portfolio_truth_types.py +++ b/src/portfolio_truth_types.py @@ -6,11 +6,13 @@ from pathlib import Path from typing import Any -SCHEMA_VERSION = "0.9.0" +SCHEMA_VERSION = "0.10.0" +# 0.10.0: canonical producer receipts bind the exact checkout; coverage and +# repository/worktree observation envelopes fail closed on unavailable evidence. # 0.8.0: derived.registry_status removed (was a stale->parked synonym table over # activity_status); derived.archived added as a first-class lifecycle boolean; # source_summary.registry_status_counts replaced by activity_status_counts + archived_count. -LEGACY_SCHEMA_VERSIONS = {"0.7.0", "0.8.0"} +LEGACY_SCHEMA_VERSIONS = {"0.7.0", "0.8.0", "0.9.0"} DERIVATION_POLICY_VERSION = "portfolio_attention.v2" # The published "latest" portfolio-truth artifact. The producer @@ -201,6 +203,7 @@ class SecurityFields: with zero open alerts, so consumers don't mislabel an unscanned repo as secure.""" alerts_available: bool = False + coverage_state: str = "unknown" dependabot_critical: int = 0 dependabot_high: int = 0 dependabot_medium: int = 0 @@ -228,6 +231,7 @@ class PortfolioTruthProject: risk: RiskFields = field(default_factory=RiskFields) security: SecurityFields = field(default_factory=SecurityFields) advisory: AdvisoryFields = field(default_factory=AdvisoryFields) + repository_state: dict[str, Any] = field(default_factory=dict) provenance: dict[str, dict[str, str]] = field(default_factory=dict) warnings: list[str] = field(default_factory=list) @@ -239,6 +243,7 @@ def to_dict(self) -> dict[str, Any]: "risk": self.risk.to_dict(), "security": self.security.to_dict(), "advisory": self.advisory.to_dict(), + "repository_state": dict(self.repository_state), "provenance": self.provenance, "warnings": list(self.warnings), } @@ -268,6 +273,7 @@ def from_projects( repos_with_open_high_critical = 0 total_open_high = 0 total_open_critical = 0 + unavailable_count = 0 decision_needed_count = 0 default_attention_count = 0 for project in projects: @@ -281,6 +287,8 @@ def from_projects( repos_with_open_high_critical += 1 total_open_high += security.dependabot_high total_open_critical += security.dependabot_critical + else: + unavailable_count += 1 attention = project.derived.attention_state if attention == "decision-needed": decision_needed_count += 1 @@ -291,6 +299,14 @@ def from_projects( risk_tier_counts=risk_tier_counts, security={ "scanned_count": scanned_count, + "unavailable_count": unavailable_count, + "coverage_state": ( + "known" + if unavailable_count == 0 + else "partial" + if scanned_count + else "unknown" + ), "repos_with_open_high_critical": repos_with_open_high_critical, "total_open_high": total_open_high, "total_open_critical": total_open_critical, diff --git a/src/portfolio_truth_validate.py b/src/portfolio_truth_validate.py index 189eeca..98754f1 100644 --- a/src/portfolio_truth_validate.py +++ b/src/portfolio_truth_validate.py @@ -105,8 +105,11 @@ def _validate_contract_envelope(snapshot: PortfolioTruthSnapshot) -> None: "commit", "ref", "checkout_role", + "checkout_path", "worktree_clean", + "dirty_path_count", "verified_at", + "receipt_id", } missing = sorted(required - producer.keys()) if missing: @@ -122,6 +125,10 @@ def _validate_contract_envelope(snapshot: PortfolioTruthSnapshot) -> None: raise ValueError( "Canonical producer evidence must declare a clean worktree." ) + if producer.get("dirty_path_count") != 0: + raise ValueError("Canonical producer evidence must declare zero dirty paths.") + if not snapshot.coverage: + raise ValueError("Portfolio truth coverage envelope is required.") notion = ( snapshot.inputs.get("notion") if isinstance(snapshot.inputs, dict) else None ) diff --git a/src/producer_preflight.py b/src/producer_preflight.py index a8c6685..475b98a 100644 --- a/src/producer_preflight.py +++ b/src/producer_preflight.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import hashlib import json import subprocess from dataclasses import dataclass @@ -10,7 +11,7 @@ from urllib.parse import urlparse -PREFLIGHT_SCHEMA_VERSION = "ghra_producer_preflight.v1" +PREFLIGHT_SCHEMA_VERSION = "ghra_producer_preflight.v2" @dataclass(frozen=True) @@ -19,8 +20,11 @@ class ProducerEvidence: commit: str ref: str checkout_role: str + checkout_path: str worktree_clean: bool + dirty_path_count: int verified_at: datetime + receipt_id: str def to_dict(self) -> dict[str, Any]: return { @@ -28,8 +32,11 @@ def to_dict(self) -> dict[str, Any]: "commit": self.commit, "ref": self.ref, "checkout_role": self.checkout_role, + "checkout_path": self.checkout_path, "worktree_clean": self.worktree_clean, + "dirty_path_count": self.dirty_path_count, "verified_at": self.verified_at.isoformat(), + "receipt_id": self.receipt_id, } @classmethod @@ -39,8 +46,11 @@ def from_dict(cls, payload: dict[str, Any]) -> ProducerEvidence: "commit", "ref", "checkout_role", + "checkout_path", "worktree_clean", + "dirty_path_count", "verified_at", + "receipt_id", } missing = sorted(required - payload.keys()) if missing: @@ -59,13 +69,18 @@ def from_dict(cls, payload: dict[str, Any]) -> ProducerEvidence: raise ValueError("Producer evidence commit must be a lowercase 40-character SHA.") if payload["worktree_clean"] is not True: raise ValueError("Producer evidence must declare a clean worktree.") + if payload["dirty_path_count"] != 0: + raise ValueError("Clean producer evidence must declare dirty_path_count=0.") return cls( repository=str(payload["repository"]), commit=commit, ref=str(payload["ref"]), checkout_role=str(payload["checkout_role"]), + checkout_path=str(payload["checkout_path"]), worktree_clean=True, + dirty_path_count=0, verified_at=parsed_verified_at.astimezone(UTC), + receipt_id=str(payload["receipt_id"]), ) @@ -119,13 +134,21 @@ def inspect_canonical_producer( "pass" if commit == expected_commit else "fail" ) state = "pass" if all(value == "pass" for value in checks.values()) else "fail" + verified_at = now or datetime.now(UTC) + dirty_path_count = len(status.splitlines()) if status else 0 + receipt_material = "\n".join( + (repository, commit, expected_ref, checkout_role, str(repo_root.resolve()), verified_at.isoformat()) + ) evidence = ProducerEvidence( repository=repository, commit=commit, ref=expected_ref, checkout_role=checkout_role, + checkout_path=str(repo_root.resolve()), worktree_clean=not status, - verified_at=now or datetime.now(UTC), + dirty_path_count=dirty_path_count, + verified_at=verified_at, + receipt_id=f"sha256:{hashlib.sha256(receipt_material.encode()).hexdigest()}", ) return ProducerPreflightResult(state=state, checks=checks, evidence=evidence) @@ -137,6 +160,14 @@ def verify_evidence_still_current(repo_root: Path, evidence: ProducerEvidence) - "Producer HEAD changed after preflight: " f"verified={evidence.commit}; current={current}" ) + if str(repo_root.resolve()) != evidence.checkout_path: + raise ValueError( + "Producer checkout changed after preflight: " + f"verified={evidence.checkout_path}; current={repo_root.resolve()}" + ) + status = _git(repo_root, "status", "--porcelain", "--untracked-files=all") + if status: + raise ValueError("Producer worktree became dirty after preflight.") def load_producer_evidence(path: Path) -> ProducerEvidence: diff --git a/tests/test_portfolio_repository_state.py b/tests/test_portfolio_repository_state.py new file mode 100644 index 0000000..e92ee25 --- /dev/null +++ b/tests/test_portfolio_repository_state.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import subprocess +from datetime import UTC, datetime +from pathlib import Path + +from src.portfolio_repository_state import observe_repository_state + + +def _git(path: Path, *args: str) -> str: + return subprocess.run( + ["git", "-C", str(path), *args], check=True, capture_output=True, text=True + ).stdout.strip() + + +def _repo(tmp_path: Path) -> Path: + repo = tmp_path / "repo" + repo.mkdir() + _git(repo, "init", "-b", "main") + _git(repo, "config", "user.email", "tests@example.invalid") + _git(repo, "config", "user.name", "Tests") + (repo / "README.md").write_text("fixture\n") + _git(repo, "add", "README.md") + _git(repo, "commit", "-m", "fixture") + return repo + + +def test_observation_reports_dirty_no_upstream_and_unknown_remote(tmp_path: Path) -> None: + repo = _repo(tmp_path) + (repo / "dirty.txt").write_text("dirty\n") + + state = observe_repository_state( + repo, observed_at=datetime(2026, 7, 12, tzinfo=UTC) + ) + + assert state["state"] == "observed" + assert state["local"]["dirty"] is True + assert state["local"]["dirty_path_count"] == 1 + assert state["local"]["upstream"] is None + assert state["remote_default_branch"]["state"] == "unknown" + + +def test_observation_reports_linked_worktree_without_file_names(tmp_path: Path) -> None: + repo = _repo(tmp_path) + linked = tmp_path / "linked" + _git(repo, "worktree", "add", "-b", "feature", str(linked), "HEAD") + (linked / "untracked.txt").write_text("preserve\n") + + state = observe_repository_state(repo, observed_at=datetime.now(UTC)) + + assert len(state["worktrees"]) == 2 + linked_state = next(item for item in state["worktrees"] if item["path"] == str(linked)) + assert linked_state["dirty"] is True + assert linked_state["dirty_path_count"] == 1 + assert "untracked.txt" not in str(state) diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 7cfd939..4e515f7 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -391,7 +391,7 @@ def test_truth_snapshot_respects_declared_and_derived_fields( assert gamma.identity.section_marker == "iOS Projects" assert gamma.derived.stack == ["Swift"] - assert result.snapshot.schema_version == "0.9.0" + assert result.snapshot.schema_version == "0.10.0" assert result.snapshot.derivation_policy_version == "portfolio_attention.v2" assert result.snapshot.inputs["catalog"]["sha256"] assert result.snapshot.inputs["notion"]["mode"] == "unavailable" @@ -417,6 +417,8 @@ def test_truth_snapshot_respects_declared_and_derived_fields( assert sum(rollups["risk_tier_counts"].values()) == len(result.snapshot.projects) assert set(rollups["security"]) == { "scanned_count", + "unavailable_count", + "coverage_state", "repos_with_open_high_critical", "total_open_high", "total_open_critical", @@ -535,7 +537,31 @@ def test_attention_state_classifier_separates_activity_from_operator_attention() path_override="investigate", risk_entry={"security_risk": False}, ) - == "decision-needed" + == "manual-only" + ) + assert ( + _attention_state_for( + activity_status="active", + archived=False, + lifecycle_state="active", + operating_path="", + category="infrastructure", + path_override="investigate", + risk_entry={"security_risk": False}, + ) + == "manual-only" + ) + assert ( + _attention_state_for( + activity_status="active", + archived=False, + lifecycle_state="manual-only", + operating_path="maintain", + category="infrastructure", + path_override="", + risk_entry={"security_risk": False}, + ) + == "manual-only" ) assert ( _attention_state_for( @@ -1961,8 +1987,11 @@ def test_portfolio_truth_app_passes_validated_producer_receipt_to_publisher( "commit": "a" * 40, "ref": "refs/remotes/origin/main", "checkout_role": "canonical-automation", + "checkout_path": str(tmp_path / "producer-repo"), "worktree_clean": True, + "dirty_path_count": 0, "verified_at": "2026-07-10T12:00:00Z", + "receipt_id": "sha256:" + "a" * 64, "checks": {}, } ) @@ -2022,6 +2051,7 @@ def test_cli_portfolio_truth_allow_empty_notion_carries_forward( monkeypatch: pytest.MonkeyPatch, ) -> None: output_dir = tmp_path / "output" + monkeypatch.setenv("GHRA_REQUIRE_PRODUCER_EVIDENCE", "0") output_dir.mkdir() discovered = build_portfolio_truth_snapshot( @@ -2404,6 +2434,7 @@ def test_cli_portfolio_truth_respects_path_overrides( monkeypatch: pytest.MonkeyPatch, ) -> None: output_dir = tmp_path / "output" + monkeypatch.setenv("GHRA_REQUIRE_PRODUCER_EVIDENCE", "0") registry_output = portfolio_workspace / "compat-registry.md" report_output = portfolio_workspace / "compat-report.md" argv = [ diff --git a/tests/test_producer_preflight.py b/tests/test_producer_preflight.py index 6ebb0c4..454ab92 100644 --- a/tests/test_producer_preflight.py +++ b/tests/test_producer_preflight.py @@ -85,8 +85,11 @@ def test_evidence_rejects_head_change(tmp_path: Path) -> None: commit=commit, ref="refs/remotes/origin/main", checkout_role="canonical-automation", + checkout_path=str(repo.resolve()), worktree_clean=True, + dirty_path_count=0, verified_at=datetime.now(UTC), + receipt_id="sha256:" + "a" * 64, ) (repo / "README.md").write_text("changed\n") _git(repo, "add", "README.md") @@ -106,8 +109,11 @@ def test_load_producer_evidence_accepts_passing_receipt(tmp_path: Path) -> None: "commit": "a" * 40, "ref": "refs/remotes/origin/main", "checkout_role": "canonical-automation", + "checkout_path": str(tmp_path / "producer-repo"), "worktree_clean": True, + "dirty_path_count": 0, "verified_at": "2026-07-10T12:00:00Z", + "receipt_id": "sha256:" + "a" * 64, "checks": {}, } ) From e07af4d419f2e36b96c288ad4ac5bf4f672b79f8 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Mon, 13 Jul 2026 20:42:01 -0700 Subject: [PATCH 2/3] fix: tighten portfolio attention discovery --- config/portfolio-catalog.yaml | 31 ++++++++++++----------- src/portfolio_catalog.py | 1 + src/portfolio_truth_sources.py | 31 +++++++++++++++++++---- src/portfolio_truth_types.py | 3 ++- tests/test_operator_os_seam_linter.py | 2 +- tests/test_portfolio_catalog.py | 36 +++++++++++++++++++++++++++ tests/test_portfolio_truth.py | 5 +++- tests/test_portfolio_truth_sources.py | 33 +++++++++++++++++++++++- 8 files changed, 118 insertions(+), 24 deletions(-) diff --git a/config/portfolio-catalog.yaml b/config/portfolio-catalog.yaml index 047654a..afd431a 100644 --- a/config/portfolio-catalog.yaml +++ b/config/portfolio-catalog.yaml @@ -77,7 +77,7 @@ repos: _machine/machine-control-tower: owner: d purpose: local machine-ops and Codex operating-layer control surface for durable operator cockpit assets - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: maintain @@ -90,7 +90,7 @@ repos: agent-bridge: owner: d purpose: local SQLite-backed MCP shared-state bus for coordinating coding agents - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: maintain @@ -225,7 +225,7 @@ repos: MCPAudit: owner: d purpose: local MCP security and drift audit tool - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: maintain category: infrastructure @@ -236,7 +236,7 @@ repos: doctor_standard: full knowledgecore: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: maintain category: infrastructure @@ -276,7 +276,7 @@ repos: operant-public: owner: d purpose: operating-agent calibration benchmark with shipped public MCP access - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: maintain @@ -507,7 +507,7 @@ repos: mcpforge: owner: d purpose: FastMCP server generator and validation toolkit - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: maintain @@ -559,7 +559,7 @@ repos: tool_provenance: claude-code notification-hub: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: maintain tool_provenance: claude-code @@ -627,6 +627,7 @@ repos: lifecycle_state: active review_cadence: weekly operating_path: maintain + category: commercial tool_provenance: claude-code Calibrate: owner: d @@ -707,7 +708,7 @@ repos: category: vanity GPT_RAG: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: monthly operating_path: maintain category: infrastructure @@ -957,7 +958,7 @@ repos: cross-provider-egress-guard: owner: d purpose: destination-aware egress-control hardening lane for Claude Code and Codex hook surfaces - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: maintain @@ -1148,14 +1149,14 @@ repos: tool_provenance: claude-code cost-tracker: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: monthly operating_path: maintain category: infrastructure tool_provenance: claude-code portfolio-health: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: monthly operating_path: maintain category: infrastructure @@ -1305,7 +1306,7 @@ repos: operating_path: maintain portfolio-mcp: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: monthly operating_path: maintain category: infrastructure @@ -1363,7 +1364,7 @@ repos: Lazarus: owner: d purpose: guarded repository recovery tool that selects safe historical revisions and blocks sensitive-file restoration - lifecycle_state: maintenance + lifecycle_state: manual-only criticality: medium review_cadence: monthly operating_path: maintain @@ -1386,14 +1387,14 @@ repos: notes: Session 2026-07-11 one-week and 32-scenario repeated dogfood matrices passed; a private connector remains unjustified. Keep outside default attention unless explicitly reactivated. continuity: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: maintain category: infrastructure peer-agent-tools: owner: d purpose: local peer-agent dispatch and preflight tooling for branch-lease safety and verified agent handoffs - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: maintain diff --git a/src/portfolio_catalog.py b/src/portfolio_catalog.py index 42f645f..68897ac 100644 --- a/src/portfolio_catalog.py +++ b/src/portfolio_catalog.py @@ -9,6 +9,7 @@ VALID_LIFECYCLE_STATES = { "active", "maintenance", + "manual-only", "dormant", "experimental", "archived", diff --git a/src/portfolio_truth_sources.py b/src/portfolio_truth_sources.py index 81871c2..b34e7d9 100644 --- a/src/portfolio_truth_sources.py +++ b/src/portfolio_truth_sources.py @@ -86,21 +86,42 @@ # nogoprjs -> operator-flagged "no-go" projects, never pursued # smoke-export -> generated AuraForge signed-smoke-export bundles (no real repo) IGNORE_PROJECT_DIR_TOKENS = frozenset({"nogoprjs", "smoke-export"}) -IGNORE_PROJECT_DIR_NAMES = frozenset({"codex backups"}) +IGNORE_PROJECT_DIR_NAMES = frozenset( + { + "codex backups", + "scratch", + "_backups", + "_preserved-local-artifacts", + "sweep-reports", + "_fable-worktrees", + "_codex-worktrees", + } +) +IGNORE_NESTED_PROJECT_DIR_NAMES = frozenset({"packets", "prompts"}) # Transient / generated working directories matched by regex on the dir name — # e.g. a `-tmp-` clone left behind by a tooling run. IGNORE_PROJECT_DIR_PATTERNS: tuple[re.Pattern[str], ...] = (re.compile(r"-tmp-\d+$"),) ARCHIVE_REMOTE_BASENAME_TOKENS = frozenset({"private-archive", "scrubbed-import"}) -WORKSPACE_DISCOVERY_POLICY_VERSION = "workspace_discovery.v1" +WORKSPACE_DISCOVERY_POLICY_VERSION = "workspace_discovery.v2" -def workspace_exclusion_reason(name: str) -> str | None: +def workspace_exclusion_reason(name: str, *, nested: bool = False) -> str | None: """Return the stable policy reason for a non-project directory name.""" lowered = name.lower() - if lowered in IGNORE_PROJECT_DIR_NAMES: + if lowered in {"codex backups", "_backups"}: return "backup-container" + if lowered == "_preserved-local-artifacts": + return "preserved-artifacts" + if lowered == "scratch": + return "scratch-container" + if lowered == "sweep-reports": + return "generated-reports" + if lowered in {"_fable-worktrees", "_codex-worktrees"}: + return "linked-worktree-container" + if nested and lowered in IGNORE_NESTED_PROJECT_DIR_NAMES: + return "nested-content" if any(token in lowered for token in IGNORE_PROJECT_DIR_TOKENS): return "operator-excluded" if "nogoprjs" in lowered else "generated-evidence" if any(pattern.search(name) for pattern in IGNORE_PROJECT_DIR_PATTERNS): @@ -210,7 +231,7 @@ def _discover_nested_projects( for child in sorted(root.iterdir(), key=lambda item: item.name.lower()): if child.name.startswith(".") or not child.is_dir() or child.is_symlink(): continue - exclusion_reason = workspace_exclusion_reason(child.name) + exclusion_reason = workspace_exclusion_reason(child.name, nested=True) if exclusion_reason is not None: _record_exclusion(exclusion_counts, exclusion_reason) continue diff --git a/src/portfolio_truth_types.py b/src/portfolio_truth_types.py index 45b9685..b31feba 100644 --- a/src/portfolio_truth_types.py +++ b/src/portfolio_truth_types.py @@ -43,6 +43,7 @@ def truth_latest_path(output_dir: Path) -> Path: VALID_LIFECYCLE_STATES = { "active", "maintenance", + "manual-only", "dormant", "experimental", "archived", @@ -340,7 +341,7 @@ class PortfolioTruthSnapshot: coverage: list[dict[str, Any]] = field(default_factory=list) exclusions: dict[str, Any] = field( default_factory=lambda: { - "policy_version": "workspace_discovery.v1", + "policy_version": "workspace_discovery.v2", "counts": {}, } ) diff --git a/tests/test_operator_os_seam_linter.py b/tests/test_operator_os_seam_linter.py index 881712b..6d0cf15 100644 --- a/tests/test_operator_os_seam_linter.py +++ b/tests/test_operator_os_seam_linter.py @@ -326,7 +326,7 @@ def test_contract_shadow_fails_when_excluded_backup_leaks_into_projects( "source_summary": {"attention_state_counts": {"decision-needed": 1}}, "rollups": {"decision": {"decision_needed_count": 1}}, "exclusions": { - "policy_version": "workspace_discovery.v1", + "policy_version": "workspace_discovery.v2", "counts": {}, }, "projects": [ diff --git a/tests/test_portfolio_catalog.py b/tests/test_portfolio_catalog.py index c5831b6..25b95a4 100644 --- a/tests/test_portfolio_catalog.py +++ b/tests/test_portfolio_catalog.py @@ -171,6 +171,42 @@ def test_live_catalog_keeps_settled_recovery_exclusions_archived() -> None: assert entry["automation_eligible"] is False +def test_live_catalog_keeps_settled_infrastructure_manual_only() -> None: + catalog_path = Path(__file__).parents[1] / "config" / "portfolio-catalog.yaml" + catalog = load_portfolio_catalog(catalog_path) + + manual_only = { + "_machine/machine-control-tower", + "agent-bridge", + "MCPAudit", + "knowledgecore", + "operant-public", + "mcpforge", + "notification-hub", + "GPT_RAG", + "cross-provider-egress-guard", + "cost-tracker", + "portfolio-health", + "portfolio-mcp", + "Lazarus", + "continuity", + "peer-agent-tools", + } + for repo_name in manual_only: + assert catalog["repos"][repo_name.lower()]["lifecycle_state"] == "manual-only" + + assert catalog["repos"]["afterimage"]["category"] == "commercial" + for repo_name in ( + "AIGCCore", + "bridge-db", + "GithubRepoAuditor", + "mcp-trust", + "cross-system-smoke", + "PortfolioCommandCenter", + ): + assert catalog["repos"][repo_name.lower()]["lifecycle_state"] == "active" + + def test_catalog_entry_matches_full_name_then_bare_name(): catalog = { "repos": { diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 4e515f7..3b2a654 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -396,7 +396,7 @@ def test_truth_snapshot_respects_declared_and_derived_fields( assert result.snapshot.inputs["catalog"]["sha256"] assert result.snapshot.inputs["notion"]["mode"] == "unavailable" assert result.snapshot.exclusions == { - "policy_version": "workspace_discovery.v1", + "policy_version": "workspace_discovery.v2", "counts": {}, } assert ( @@ -502,6 +502,9 @@ def test_attention_state_classifier_separates_activity_from_operator_attention() None ): from src.portfolio_truth_reconcile import _attention_state_for + from src.portfolio_truth_types import VALID_LIFECYCLE_STATES + + assert "manual-only" in VALID_LIFECYCLE_STATES assert ( _attention_state_for( diff --git a/tests/test_portfolio_truth_sources.py b/tests/test_portfolio_truth_sources.py index b224d76..f18ca9e 100644 --- a/tests/test_portfolio_truth_sources.py +++ b/tests/test_portfolio_truth_sources.py @@ -99,6 +99,24 @@ def test_ignore_predicate_matches_transient_dirs() -> None: assert _is_ignored_project_dir("resume-evolver-tmp-1776063720") assert _is_ignored_project_dir("Codex Backups") assert workspace_exclusion_reason("Codex Backups") == "backup-container" + assert workspace_exclusion_reason("scratch") == "scratch-container" + assert workspace_exclusion_reason("_backups") == "backup-container" + assert ( + workspace_exclusion_reason("_preserved-local-artifacts") + == "preserved-artifacts" + ) + assert workspace_exclusion_reason("sweep-reports") == "generated-reports" + assert ( + workspace_exclusion_reason("_fable-worktrees") + == "linked-worktree-container" + ) + assert ( + workspace_exclusion_reason("_codex-worktrees") + == "linked-worktree-container" + ) + assert workspace_exclusion_reason("packets") is None + assert workspace_exclusion_reason("packets", nested=True) == "nested-content" + assert workspace_exclusion_reason("prompts", nested=True) == "nested-content" def test_ignore_predicate_keeps_real_projects() -> None: @@ -128,6 +146,14 @@ def _project(*parts: str) -> None: _project("resume-evolver-tmp-1776063720") # top-level tmp clone -> skipped _project("Documents", "Codex Backups", "Wave 2R Post-Update", "README-fixture") _project("Documents", "RealNestedProject") + _project("scratch", "README-fixture") + _project("_backups", "old-repo") + _project("_preserved-local-artifacts", "saved-repo") + _project("sweep-reports", "branch-hygiene-2026-07-03") + _project("_fable-worktrees", "personal-ops-worklist-phase1") + _project("_codex-worktrees", "personal-ops-truth-authority") + _project("Campaign", "packets") + _project("Campaign", "prompts") exclusion_counts: dict[str, int] = {} result = discover_workspace_projects( @@ -138,8 +164,13 @@ def _project(*parts: str) -> None: ) assert {p["name"] for p in result} == {"LegitProject", "RealNestedProject"} assert exclusion_counts == { - "backup-container": 1, + "backup-container": 2, "generated-evidence": 1, + "generated-reports": 1, + "linked-worktree-container": 2, + "nested-content": 2, "operator-excluded": 1, + "preserved-artifacts": 1, + "scratch-container": 1, "temporary-checkout": 1, } From b825440c3c433f1fa63c28eaf3a9cc27ea2b6a87 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Mon, 13 Jul 2026 21:34:39 -0700 Subject: [PATCH 3/3] fix(truth): type security coverage state --- src/portfolio_truth_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/portfolio_truth_types.py b/src/portfolio_truth_types.py index b31feba..99293f1 100644 --- a/src/portfolio_truth_types.py +++ b/src/portfolio_truth_types.py @@ -257,7 +257,7 @@ class PortfolioTruthRollups: re-deriving the auditor's risk/security logic, which is the #1 drift risk.""" risk_tier_counts: dict[str, int] - security: dict[str, int] + security: dict[str, int | str] decision: dict[str, int] @classmethod