Compare commits
3 commits
e5a083f444
...
ddd6a66730
| Author | SHA1 | Date | |
|---|---|---|---|
| ddd6a66730 | |||
| 71c94c940a | |||
| cb6eddf92b |
4 changed files with 333 additions and 22 deletions
|
|
@ -48,6 +48,7 @@
|
|||
| 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 |
|
||||
|
|
@ -272,3 +273,9 @@
|
|||
| 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 |
|
||||
|
|
|
|||
|
|
@ -169,6 +169,8 @@ 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)
|
||||
|
|
@ -182,6 +184,32 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
"""State Hub dual-run adapter → Repo Manager (RMGR-WP-0002 Stage B).
|
||||
"""State Hub dual-run adapter → Repo Manager (RMGR-WP-0002/0003 Stage B).
|
||||
|
||||
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.
|
||||
Config file (same as repo-manager): ~/.repo-manager/dual-run.yaml
|
||||
or RM_DUAL_RUN_CONFIG. Env overrides when set:
|
||||
|
||||
Shared env vars (see repo-manager dual_run.py):
|
||||
RM_WRITEBACK, RM_RECONCILE, RM_PILOT_REPOS, RM_METER_PATH, RMGR_BIN
|
||||
RM_WRITEBACK, RM_RECONCILE, RM_WRITEBACK_PUSH, RM_PILOT_REPOS, RM_METER_PATH
|
||||
|
||||
Rollback: disable in config / unset env → native State Hub path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -16,32 +16,97 @@ 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 _truthy(name: str) -> bool:
|
||||
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
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 writeback_enabled() -> bool:
|
||||
return _truthy("RM_WRITEBACK")
|
||||
return _cfg_bool("writeback", "RM_WRITEBACK", False)
|
||||
|
||||
|
||||
def reconcile_enabled() -> bool:
|
||||
return _truthy("RM_RECONCILE")
|
||||
return _cfg_bool("reconcile", "RM_RECONCILE", False)
|
||||
|
||||
|
||||
def writeback_push_enabled() -> bool:
|
||||
return _cfg_bool("writeback_push", "RM_WRITEBACK_PUSH", False)
|
||||
|
||||
|
||||
def pilot_slugs() -> set[str] | None:
|
||||
raw = os.environ.get("RM_PILOT_REPOS")
|
||||
if raw is None:
|
||||
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:
|
||||
return None
|
||||
raw = raw.strip()
|
||||
if not raw:
|
||||
return set()
|
||||
return {s.strip() for s in raw.split(",") if s.strip()}
|
||||
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
|
||||
|
||||
|
||||
def slug_allowed(slug: str | None) -> bool:
|
||||
|
|
@ -62,8 +127,13 @@ def reconcile_for_repo(slug: str | None) -> bool:
|
|||
|
||||
|
||||
def meter_path() -> Path:
|
||||
raw = os.environ.get("RM_METER_PATH", "~/.repo-manager/mutation-meter.jsonl")
|
||||
return Path(raw).expanduser()
|
||||
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()
|
||||
|
||||
|
||||
def record_mutation(
|
||||
|
|
@ -92,7 +162,6 @@ 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:
|
||||
|
|
@ -133,11 +202,12 @@ def rm_update_task_status(
|
|||
status: str,
|
||||
repo_slug: str | None = None,
|
||||
reason: str = "state-hub dual-run",
|
||||
push: bool = False,
|
||||
push: bool | None = None,
|
||||
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",
|
||||
|
|
@ -200,7 +270,6 @@ 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:
|
||||
|
|
@ -217,7 +286,7 @@ def try_writeback_for_task(
|
|||
status=status,
|
||||
repo_slug=repo_slug,
|
||||
reason=reason,
|
||||
push=False, # SH fix-consistency push-seal remains authority for push in pilot
|
||||
push=writeback_push_enabled(),
|
||||
)
|
||||
if result.get("status") == "applied":
|
||||
record_mutation(
|
||||
|
|
@ -228,6 +297,7 @@ 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:
|
||||
|
|
|
|||
206
workplans/STATE-WP-0080-register-project-flavor-awareness.md
Normal file
206
workplans/STATE-WP-0080-register-project-flavor-awareness.md
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
---
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue