diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index b8f329b..f526886 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -48,7 +48,6 @@ | workplan | STATE-WP-0076 | finished | — | workplans/STATE-WP-0076-definition-of-ready-and-comprehension.md | | workplan | STATE-WP-0077 | finished | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | | workplan | STATE-WP-0078 | finished | — | workplans/STATE-WP-0078-ops-run-read-projection.md | -| workplan | STATE-WP-0079 | proposed | — | workplans/STATE-WP-0079-retirement-strangler.md | | task | ADHOC-2026-06-04-T01 | done | — | workplans/ADHOC-2026-06-04.md | | task | ADHOC-2026-07-01-T01 | done | — | workplans/ADHOC-2026-07-01.md | | task | ADHOC-2026-07-01-T02 | done | — | workplans/ADHOC-2026-07-01.md | @@ -273,9 +272,3 @@ | task | STATE-WP-0078-T01 | done | — | workplans/STATE-WP-0078-ops-run-read-projection.md | | task | STATE-WP-0078-T02 | done | — | workplans/STATE-WP-0078-ops-run-read-projection.md | | task | STATE-WP-0078-T03 | done | — | workplans/STATE-WP-0078-ops-run-read-projection.md | -| task | STATE-WP-0079-T01 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md | -| task | STATE-WP-0079-T02 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md | -| task | STATE-WP-0079-T03 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md | -| task | STATE-WP-0079-T04 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md | -| task | STATE-WP-0079-T05 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md | -| task | STATE-WP-0079-T06 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md | diff --git a/api/routers/tasks.py b/api/routers/tasks.py index b76746c..7a54a5c 100644 --- a/api/routers/tasks.py +++ b/api/routers/tasks.py @@ -169,8 +169,6 @@ async def bulk_status_sync( updated: list[Task] = [] events: list[ProgressEvent] = [] author = body.author or "custodian" - # Cache repo resolution for dual-run writeback (RMGR-WP-0003) - repo_cache: dict = {} for update in body.updates: task = tasks_by_id[update.task_id] previous_status = status_value(task.status) @@ -184,32 +182,6 @@ async def bulk_status_sync( parent_workstream=ws, previous_task_status=previous_status, ) - if target_status != previous_status: - try: - from api.models.managed_repo import ManagedRepo - from api.services.repo_manager_dual_run import try_writeback_for_task - from api.services.workplan_files import resolve_repo_path - - repo_id = ws.repo_id if ws else None - if repo_id is not None: - if repo_id not in repo_cache: - repo_cache[repo_id] = await session.get(ManagedRepo, repo_id) - repo = repo_cache[repo_id] - if repo is not None: - try_writeback_for_task( - repo_path=resolve_repo_path(repo), - repo_slug=repo.slug, - task_id=str(task.id), - status=target_status, - reason="state-hub bulk-status-sync dual-run", - ) - except Exception: - import logging - - logging.getLogger(__name__).exception( - "repo-manager dual-run bulk writeback failed for task %s", - update.task_id, - ) event = ProgressEvent( task_id=task.id, workplan_id=task.workplan_id, diff --git a/api/services/repo_manager_dual_run.py b/api/services/repo_manager_dual_run.py index 3af5e40..b6a8731 100644 --- a/api/services/repo_manager_dual_run.py +++ b/api/services/repo_manager_dual_run.py @@ -1,11 +1,11 @@ -"""State Hub dual-run adapter → Repo Manager (RMGR-WP-0002/0003 Stage B). +"""State Hub dual-run adapter → Repo Manager (RMGR-WP-0002 Stage B). -Config file (same as repo-manager): ~/.repo-manager/dual-run.yaml -or RM_DUAL_RUN_CONFIG. Env overrides when set: +When RM_WRITEBACK / RM_RECONCILE are enabled (and optional RM_PILOT_REPOS matches), +State Hub delegates checkout mutation to the ``rmgr`` CLI instead of native +file writeback. Rollback: unset the env flags. - RM_WRITEBACK, RM_RECONCILE, RM_WRITEBACK_PUSH, RM_PILOT_REPOS, RM_METER_PATH - -Rollback: disable in config / unset env → native State Hub path. +Shared env vars (see repo-manager dual_run.py): + RM_WRITEBACK, RM_RECONCILE, RM_PILOT_REPOS, RM_METER_PATH, RMGR_BIN """ from __future__ import annotations @@ -16,97 +16,32 @@ import os import subprocess import sys import uuid -from functools import lru_cache from pathlib import Path from typing import Any -import yaml - logger = logging.getLogger(__name__) -_TRUE = frozenset({"1", "true", "yes", "on"}) -_FALSE = frozenset({"0", "false", "no", "off"}) - -def config_path() -> Path: - raw = os.environ.get("RM_DUAL_RUN_CONFIG", "~/.repo-manager/dual-run.yaml") - return Path(raw).expanduser() - - -@lru_cache(maxsize=1) -def _load_file_config() -> dict[str, Any]: - path = config_path() - if not path.is_file(): - return {} - try: - data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} - except (OSError, yaml.YAMLError): - return {} - return data if isinstance(data, dict) else {} - - -def reload_config() -> None: - _load_file_config.cache_clear() - - -def _env_bool(name: str) -> bool | None: - raw = os.environ.get(name) - if raw is None or raw.strip() == "": - return None - v = raw.strip().lower() - if v in _TRUE: - return True - if v in _FALSE: - return False - return None - - -def _cfg_bool(key: str, env_name: str, default: bool = False) -> bool: - ev = _env_bool(env_name) - if ev is not None: - return ev - cfg = _load_file_config() - if key in cfg: - val = cfg[key] - if isinstance(val, bool): - return val - if isinstance(val, str): - return val.strip().lower() in _TRUE - return default +def _truthy(name: str) -> bool: + return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"} def writeback_enabled() -> bool: - return _cfg_bool("writeback", "RM_WRITEBACK", False) + return _truthy("RM_WRITEBACK") def reconcile_enabled() -> bool: - return _cfg_bool("reconcile", "RM_RECONCILE", False) - - -def writeback_push_enabled() -> bool: - return _cfg_bool("writeback_push", "RM_WRITEBACK_PUSH", False) + return _truthy("RM_RECONCILE") def pilot_slugs() -> set[str] | None: raw = os.environ.get("RM_PILOT_REPOS") - if raw is not None: - raw = raw.strip() - if not raw: - return set() - return {s.strip() for s in raw.split(",") if s.strip()} - cfg = _load_file_config() - if "pilot_repos" not in cfg: + if raw is None: return None - val = cfg["pilot_repos"] - if val is None: - return None - if isinstance(val, str): - if not val.strip(): - return set() - return {s.strip() for s in val.split(",") if s.strip()} - if isinstance(val, list): - return {str(s).strip() for s in val if str(s).strip()} - return None + raw = raw.strip() + if not raw: + return set() + return {s.strip() for s in raw.split(",") if s.strip()} def slug_allowed(slug: str | None) -> bool: @@ -127,13 +62,8 @@ def reconcile_for_repo(slug: str | None) -> bool: def meter_path() -> Path: - raw = os.environ.get("RM_METER_PATH") - if raw: - return Path(raw).expanduser() - cfg = _load_file_config() - if cfg.get("meter_path"): - return Path(str(cfg["meter_path"])).expanduser() - return Path("~/.repo-manager/mutation-meter.jsonl").expanduser() + raw = os.environ.get("RM_METER_PATH", "~/.repo-manager/mutation-meter.jsonl") + return Path(raw).expanduser() def record_mutation( @@ -162,6 +92,7 @@ def record_mutation( def run_rmgr(args: list[str], *, timeout: int = 120) -> tuple[int, str, str]: + """Run rmgr CLI. Prefer PYTHONPATH to sibling repo-manager checkout.""" env = os.environ.copy() custom = os.environ.get("RMGR_BIN", "").strip() if custom: @@ -202,12 +133,11 @@ def rm_update_task_status( status: str, repo_slug: str | None = None, reason: str = "state-hub dual-run", - push: bool | None = None, + push: bool = False, correlation_id: str | None = None, ) -> dict[str, Any]: + """Invoke repo-manager task status writeback. Returns parsed command result dict.""" correlation_id = correlation_id or str(uuid.uuid4()) - if push is None: - push = writeback_push_enabled() args = [ "update-task-status", "--path", @@ -270,6 +200,7 @@ def try_writeback_for_task( status: str, reason: str = "state-hub dual-run PATCH /tasks", ) -> dict[str, Any] | None: + """If dual-run writeback applies, run RM and return result; else None (use native SH).""" if not writeback_for_repo(repo_slug): return None if not repo_path: @@ -286,7 +217,7 @@ def try_writeback_for_task( status=status, repo_slug=repo_slug, reason=reason, - push=writeback_push_enabled(), + push=False, # SH fix-consistency push-seal remains authority for push in pilot ) if result.get("status") == "applied": record_mutation( @@ -297,7 +228,6 @@ def try_writeback_for_task( "task_id": task_id, "status": status, "git_sha": (result.get("evidence") or {}).get("git_sha"), - "push_ok": (result.get("evidence") or {}).get("push_ok"), }, ) else: diff --git a/workplans/STATE-WP-0080-register-project-flavor-awareness.md b/workplans/STATE-WP-0080-register-project-flavor-awareness.md deleted file mode 100644 index 7394088..0000000 --- a/workplans/STATE-WP-0080-register-project-flavor-awareness.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -id: STATE-WP-0080 -type: workplan -title: "statehub register: project-repository flavor awareness" -domain: infotech -repo: state-hub -status: proposed -owner: codex -topic_slug: infotech -created: "2026-08-16" -updated: "2026-08-16" -parent_project: prj-state-hub-retirement -parent_workplan: SHR-WP-0001 -related: - - CFED-WP-0001 - - SHR-WP-0001 ---- - -# statehub register: project-repository flavor awareness - -## Goal - -Make `statehub register` aware of the `prj-` project-repository flavor so that -scaffolding a project repo produces conformant files instead of files the -governing standard calls an anti-pattern. - -This is the residual recorded in `SHR-WP-0001-T01`: - -> Residual tooling gap: `statehub register` still scaffolds `INTENT.md` for -> ordinary repos — capture under a state-hub child workplan when T05 maps -> implementation streams (do not invent `INTENT.md` on `prj-` repos). - -It has now been hit twice — once when founding `prj-state-hub-retirement`, and -again on 2026-08-16 founding `prj-canon-federation`, where the scaffolded -`INTENT.md` and a generic `PRJ-WP-0001-statehub-bootstrap.md` both had to be -deleted by hand before the first `fix-consistency` run. - -## Root cause - -`statehub_register.py` (872 lines) contains **no reference to -`.repo-classification.yaml`, `category`, or `repo_flavor`**. It has no way to -distinguish a project repository from a durable product repository, so: - -- `KEY_CONTEXT_FILES` (`:25`) lists `INTENT.md` as a primary context file; -- `:248` writes `INTENT.md` unconditionally when absent; -- `:786`–`:790` treat a missing intent as a hard error, prompting interactively - and exiting with `ERROR: Intent is required to create INTENT.md.`; -- `:269` writes `{WP_PREFIX}-0001-statehub-bootstrap.md` with a prefix defaulted - from the repo slug (`:78`, `_default_wp_prefix`), yielding `PRJ-WP-` for every - `prj-` repo rather than a project-derived prefix; -- the generated `.custodian-brief.md` (`:634`) and templates (`:461`, `:509`, - `:525`, `:569`) all instruct agents to read `INTENT.md`. - -The governing standard is -`the-custodian/canon/standards/project-repository-flavor_v0.1.md`, which states: - -> Tooling that scaffolds repositories (e.g. `statehub register`) MUST treat -> `GOAL.md` + `repo_flavor: project` as sufficient purpose documentation for -> `prj-` repos and MUST NOT require inventing an `INTENT.md` that pretends the -> project is a permanent product. - -and names shipping both `INTENT.md` and `GOAL.md` an explicit anti-pattern. - -## Detect repository flavor - -```task -id: STATE-WP-0080-T01 -status: todo -priority: high -``` - -Give the register path a flavor signal. Read `.repo-classification.yaml` -`category` when present, and `GOAL.md` frontmatter `repo_flavor` as a secondary -signal; fall back to the `prj-` directory/slug prefix. Surface the result as a -single value threaded through inference and scaffolding. - -Treat classification as authoritative when present and disagreeing with the slug -prefix, and warn on the mismatch rather than guessing silently. - -## Scaffold GOAL.md instead of INTENT.md for project repos - -```task -id: STATE-WP-0080-T02 -status: wait -priority: high -``` - -For project-flavor repos: - -- do not write `INTENT.md`, and do not prompt for intent (`:786`–`:790`); -- accept an existing `GOAL.md` as sufficient purpose documentation; -- when `GOAL.md` is absent, scaffold one from a template carrying the four - sections the standard requires — Outcome, Invariants, Success gates, Project - retirement — plus the recommended frontmatter (`repo`, `repo_flavor`, - `project_status`, `started`, `reviewed`); -- error only if neither `GOAL.md` nor enough input to generate one exists. - -For all other flavors, behaviour is unchanged. - -## Fix workplan prefix inference - -```task -id: STATE-WP-0080-T03 -status: wait -priority: medium -``` - -`_default_wp_prefix` derives `PRJ-WP-` from any `prj-`-prefixed slug, which is -wrong for every project repo: the standard requires a prefix derived from the -project, not the flavor marker (`SHR-WP-` for State Hub Retirement, `CFED-WP-` -for canon federation). - -For project-flavor repos, strip the `prj-` marker before deriving, and prompt or -require `--wp-prefix` rather than emitting a flavor-derived default. A prefix -collision across two `prj-` repos is the failure this prevents. - -## Decide the bootstrap workplan for project repos - -```task -id: STATE-WP-0080-T04 -status: wait -priority: medium -``` - -`:269` writes a generic `statehub-bootstrap` workplan. For project repos this -competes with the foundation workplan the project actually needs — in both real -cases it was deleted immediately. - -Either skip it for project flavor, or replace it with a foundation-workplan -template shaped by the standard (confirm conventions, inventory participants, -map child workplans, define gates). Prefer skipping unless T05's template work -shows the foundation shape is genuinely reusable. - -## Update generated templates and briefs - -```task -id: STATE-WP-0080-T05 -status: wait -priority: medium -``` - -Make every generated artifact flavor-correct: - -- `KEY_CONTEXT_FILES` (`:25`) — include `GOAL.md`; -- `.custodian-brief.md` orientation line (`:634`) — `GOAL.md` for project repos; -- `AGENTS.md` / `CLAUDE.md` / `scripts/project_rules/` templates (`:461`, - `:509`, `:525`, `:569`) — session start order `GOAL.md` → `SCOPE.md` → - history → active workplans, per the standard's agent conventions; -- `_ensure_context_files` (`:431`) — do not treat missing `INTENT.md` as a gap - on project repos. - -Confirm `make update-agent-instructions` regenerates conformant files for an -existing `prj-` repo, not just at first registration. - -## Add a conformance check - -```task -id: STATE-WP-0080-T06 -status: wait -priority: low -``` - -The standard ships a conformance checklist that is currently only human-checked. -Add a consistency check that flags a project-flavor repo which ships `INTENT.md` -alongside `GOAL.md`, is missing a required file, or uses a flavor-derived -workplan prefix. - -Advisory severity is sufficient — this is a convention violation, not data -corruption. Follow the existing `C-NN` numbering and register it in the -consistency-check catalog. - -## Cover with tests - -```task -id: STATE-WP-0080-T07 -status: wait -priority: medium -``` - -Extend `tests/test_statehub_register_cli.py`: - -- registering a `prj-` repo with `category: project` writes `GOAL.md`, no - `INTENT.md`, and no flavor-derived prefix; -- registering an ordinary product repo is byte-identical to today's output - (regression guard — this workplan must not change durable-repo behaviour); -- classification/slug mismatch warns rather than silently choosing; -- re-running register on an existing conformant `prj-` repo is idempotent. - -## Carry the fix forward past State Hub retirement - -```task -id: STATE-WP-0080-T08 -status: wait -priority: medium -``` - -State Hub is being retired (`STATE-WP-0079`, `prj-state-hub-retirement`). -Repository scaffolding and representation are slated to land in `repo-manager`. - -Record where this flavor logic belongs after retirement and link it to the -inventory disposition for the register path, so the fix is not stranded in a -component scheduled for removal. If `repo-manager` is close enough to take it, -prefer implementing there and leaving `statehub register` with a thin -flavor-guard rather than a full templating path. - -Coordinate through `SHR-WP-0001` rather than duplicating its task list.