Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions config/portfolio-catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -627,6 +627,7 @@ repos:
lifecycle_state: active
review_cadence: weekly
operating_path: maintain
category: commercial
tool_provenance: claude-code
Calibrate:
owner: d
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/app/portfolio_truth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/portfolio_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
VALID_LIFECYCLE_STATES = {
"active",
"maintenance",
"manual-only",
"dormant",
"experimental",
"archived",
Expand Down
102 changes: 102 additions & 0 deletions src/portfolio_repository_state.py
Original file line number Diff line number Diff line change
@@ -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"
)
Comment on lines +29 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle prunable worktrees without dropping repo state

When a repository has a stale linked-worktree record, git worktree list --porcelain includes that worktree and git -C <missing> status exits 128; this call then trips the outer exception handler and returns state: "unknown" for the entire primary repo. In that scenario the canonical truth snapshot loses otherwise valid local head/dirty/ahead evidence just because one linked worktree is prunable, so record that worktree as unavailable or skip it instead of failing the whole observation.

Useful? React with 👍 / 👎.

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
46 changes: 41 additions & 5 deletions src/portfolio_truth_reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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())),
Expand All @@ -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,
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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"
Comment on lines +1111 to 1112

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve decision-needed for investigate overrides

For active/recent commercial or infrastructure repos whose operating-path evidence sets path_override == "investigate" (for example weak context or missing path confidence), this narrowed guard only treats security risk as decision-needed, so the following maintain/finish branch can label the repo active-product or active-infra. That contradicts the generated path rationale telling operators to treat the repo as investigate and causes downstream consumers of attention_state to lose the unresolved path decision.

Useful? React with 👍 / 👎.

if activity_status in {"active", "recent"} and operating_path in {
"maintain",
Expand Down
Loading