feat: add fast forge work-record reconciliation
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
parent
a65cef02cf
commit
34f5cb3fc3
22 changed files with 799 additions and 162 deletions
|
|
@ -630,6 +630,24 @@ def _inject_task_id_frontmatter_list(
|
|||
# API helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_API_CLIENTS: dict[tuple[str, object], Any] = {}
|
||||
|
||||
|
||||
def _api_client(api_base: str) -> Any:
|
||||
"""Reuse one keep-alive pool per API/client implementation for this run."""
|
||||
key = (api_base.rstrip("/"), _httpx.Client)
|
||||
client = _API_CLIENTS.get(key)
|
||||
if client is None:
|
||||
client = _httpx.Client(
|
||||
base_url=api_base,
|
||||
timeout=10.0,
|
||||
follow_redirects=True,
|
||||
headers=_legacy_meter_headers(),
|
||||
)
|
||||
_API_CLIENTS[key] = client
|
||||
return client
|
||||
|
||||
|
||||
def _api_get(
|
||||
api_base: str,
|
||||
path: str,
|
||||
|
|
@ -646,15 +664,9 @@ def _api_get(
|
|||
last_error: Exception | None = None
|
||||
for attempt in range(_API_GET_RETRIES):
|
||||
try:
|
||||
with _httpx.Client(
|
||||
base_url=api_base,
|
||||
timeout=10.0,
|
||||
follow_redirects=True,
|
||||
headers=_legacy_meter_headers(),
|
||||
) as c:
|
||||
r = c.get(path, params=filtered if filtered else None)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
r = _api_client(api_base).get(path, params=filtered if filtered else None)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except _httpx.HTTPStatusError as exc:
|
||||
if exc.response.status_code == 404:
|
||||
return None
|
||||
|
|
@ -689,15 +701,9 @@ def _api_patch(api_base: str, path: str, body: dict) -> Any:
|
|||
if not path.endswith("/"):
|
||||
path += "/"
|
||||
try:
|
||||
with _httpx.Client(
|
||||
base_url=api_base,
|
||||
timeout=10.0,
|
||||
follow_redirects=True,
|
||||
headers=_legacy_meter_headers(),
|
||||
) as c:
|
||||
r = c.patch(path, json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
r = _api_client(api_base).patch(path, json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as exc:
|
||||
# Return a sentinel dict so callers can distinguish "API error" from "success"
|
||||
# and report it rather than silently dropping the fix.
|
||||
|
|
@ -710,15 +716,9 @@ def _api_put(api_base: str, path: str, body: dict) -> Any:
|
|||
if not path.endswith("/"):
|
||||
path += "/"
|
||||
try:
|
||||
with _httpx.Client(
|
||||
base_url=api_base,
|
||||
timeout=30.0,
|
||||
follow_redirects=True,
|
||||
headers=_legacy_meter_headers(),
|
||||
) as c:
|
||||
r = c.put(path, json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
r = _api_client(api_base).put(path, json=body, timeout=30.0)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as exc:
|
||||
return {"_error": str(exc)}
|
||||
|
||||
|
|
@ -729,15 +729,9 @@ def _api_post(api_base: str, path: str, body: dict) -> Any:
|
|||
if not path.endswith("/"):
|
||||
path += "/"
|
||||
try:
|
||||
with _httpx.Client(
|
||||
base_url=api_base,
|
||||
timeout=10.0,
|
||||
follow_redirects=True,
|
||||
headers=_legacy_meter_headers(),
|
||||
) as c:
|
||||
r = c.post(path, json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
r = _api_client(api_base).post(path, json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except _httpx.HTTPStatusError as exc:
|
||||
detail = exc.response.text
|
||||
if len(detail) > 500:
|
||||
|
|
@ -1357,6 +1351,37 @@ def check_repo(
|
|||
if task_file_id and task_sh_id and task_sh_id not in ("~", "null", "None", "none"):
|
||||
task_file_id_to_sh_id[task_file_id] = task_sh_id
|
||||
|
||||
# Modern hubs expose the entire repo-scoped read model in one response.
|
||||
# Keep the older per-record calls as a rolling-deploy compatibility path.
|
||||
projection_snapshot = _api_get(
|
||||
api_base,
|
||||
f"/repos/{repo_slug}/work-record-projection/snapshot",
|
||||
return_error=True,
|
||||
)
|
||||
snapshot_available = (
|
||||
isinstance(projection_snapshot, dict)
|
||||
and projection_snapshot.get("schema")
|
||||
== "state-hub.repository-projection-snapshot.v1"
|
||||
and str(projection_snapshot.get("repo_id")) == repo_id
|
||||
)
|
||||
snapshot_workplans_by_id: dict[str, dict] = {}
|
||||
snapshot_tasks_by_workplan: dict[str, list[dict]] = {}
|
||||
snapshot_dependencies_by_workplan: dict[str, list[dict]] = {}
|
||||
if snapshot_available:
|
||||
for row in projection_snapshot.get("workplans", []):
|
||||
if isinstance(row, dict) and row.get("id"):
|
||||
snapshot_workplans_by_id[str(row["id"])] = row
|
||||
for row in projection_snapshot.get("tasks", []):
|
||||
if isinstance(row, dict) and row.get("workplan_id"):
|
||||
snapshot_tasks_by_workplan.setdefault(
|
||||
str(row["workplan_id"]), []
|
||||
).append(row)
|
||||
for row in projection_snapshot.get("dependencies", []):
|
||||
if isinstance(row, dict) and row.get("from_workplan_id"):
|
||||
snapshot_dependencies_by_workplan.setdefault(
|
||||
str(row["from_workplan_id"]), []
|
||||
).append(row)
|
||||
|
||||
# Per-workplan checks
|
||||
for wp_file, meta, body in workplan_infos:
|
||||
fname = workplan_display_path(repo_dir, wp_file)
|
||||
|
|
@ -1396,7 +1421,11 @@ def check_repo(
|
|||
)
|
||||
continue
|
||||
|
||||
ws = _api_get(api_base, f"/workplans/{ws_id}")
|
||||
ws = (
|
||||
snapshot_workplans_by_id.get(ws_id)
|
||||
if snapshot_available
|
||||
else _api_get(api_base, f"/workplans/{ws_id}")
|
||||
)
|
||||
if ws is None:
|
||||
wp_id = str(meta.get("id", "")).strip()
|
||||
if wp_id and (
|
||||
|
|
@ -1461,7 +1490,16 @@ def check_repo(
|
|||
# Continue to check drift even with mismatched repo
|
||||
|
||||
tasks = get_tasks_from_workplan(meta, body)
|
||||
db_tasks = _api_get(api_base, "/tasks", {"workplan_id": ws_id})
|
||||
db_tasks = (
|
||||
snapshot_tasks_by_workplan.get(ws_id, [])
|
||||
if snapshot_available
|
||||
else _api_get(api_base, "/tasks", {"workplan_id": ws_id})
|
||||
)
|
||||
db_tasks_by_id = {
|
||||
str(row.get("id")): row
|
||||
for row in db_tasks
|
||||
if isinstance(row, dict) and row.get("id")
|
||||
} if isinstance(db_tasks, list) else {}
|
||||
file_task_statuses = [
|
||||
str(task.get("status", "")).strip()
|
||||
for task in tasks
|
||||
|
|
@ -1632,7 +1670,11 @@ def check_repo(
|
|||
for t in db_tasks:
|
||||
db_task_by_id[t["id"]] = t
|
||||
|
||||
existing_deps = _api_get(api_base, f"/workplans/{ws_id}/dependencies") or []
|
||||
existing_deps = (
|
||||
snapshot_dependencies_by_workplan.get(ws_id, [])
|
||||
if snapshot_available
|
||||
else _api_get(api_base, f"/workplans/{ws_id}/dependencies") or []
|
||||
)
|
||||
existing_dep_keys = set()
|
||||
if isinstance(existing_deps, list):
|
||||
for dep in existing_deps:
|
||||
|
|
@ -1718,7 +1760,7 @@ def check_repo(
|
|||
|
||||
if t_sh_id:
|
||||
file_task_sh_ids.add(t_sh_id)
|
||||
db_task = _api_get(api_base, f"/tasks/{t_sh_id}")
|
||||
db_task = db_tasks_by_id.get(t_sh_id)
|
||||
if db_task is None:
|
||||
if t_id and t_sh_id == _derived_work_record_uuid(t_id):
|
||||
report.add(
|
||||
|
|
@ -2818,8 +2860,8 @@ def _skip_non_registrar_mint(report: "ConsistencyReport", check_id: str, label:
|
|||
f"{check_id} skipped: this instance is not the identifier registrar "
|
||||
f"({label}; do not retry or set STATEHUB_REGISTRAR directly; after "
|
||||
"committing and pushing file-backed work run once: "
|
||||
"uv run --project ~/repo-manager rmgr registrar-reconcile "
|
||||
"--path . --confirm-primary --push; ADR-007 interim / RMGR-WP-0005-T01)"
|
||||
"uv run --project ~/repo-manager rmgr sync --path . --push; "
|
||||
"ADR-012 / STATE-WP-0086)"
|
||||
)
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -114,19 +114,12 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
|||
3. Log: `POST /progress/` with a summary of what changed (name handoff ids)
|
||||
4. After workplan file changes, run:
|
||||
```bash
|
||||
statehub fix-consistency
|
||||
uv run --project ~/repo-manager rmgr sync --path . --push
|
||||
```
|
||||
Coding agents should run this directly; ask the operator only if the CLI or
|
||||
State Hub API is unavailable. This syncs task status from files into the hub DB.
|
||||
If C-06/C-11 reports that this host is not the identifier registrar, do not
|
||||
retry, export `STATEHUB_REGISTRAR`, or register records by hand. Commit and
|
||||
push the file-backed work first, then run the repo-manager fallback once:
|
||||
```bash
|
||||
uv run --project ~/repo-manager rmgr registrar-reconcile \
|
||||
--path . --confirm-primary --push
|
||||
```
|
||||
If unavailable, send one deduplicated registrar request to `repo-manager`
|
||||
naming the repo and canonical ids; UUID absence does not block local work.
|
||||
This assigns only missing deterministic identifiers, verifies the pushed
|
||||
Forgejo commit and `primary/railliance01`, then requests one central
|
||||
reconciliation. A queued receipt is pending evidence; rerun after
|
||||
connectivity returns. Use `statehub fix-consistency` for a separate deep audit.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -172,7 +165,7 @@ owner: codex
|
|||
topic_slug: ...
|
||||
created: "YYYY-MM-DD"
|
||||
updated: "YYYY-MM-DD"
|
||||
state_hub_workstream_id: "<uuid>" # fix-consistency — do not edit (legacy field name; workplan UUID)
|
||||
state_hub_workstream_id: "<uuid>" # deterministic UUIDv5; managed by Repo Manager
|
||||
---
|
||||
```
|
||||
|
||||
|
|
@ -193,7 +186,7 @@ API/MCP/frontmatter bridges until `STATE-WP-0069` retires them — see
|
|||
id: {WP_PREFIX}-NNNN-T01
|
||||
status: wait | todo | progress | done | cancel
|
||||
priority: high | medium | low
|
||||
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
|
||||
state_hub_task_id: "<uuid>" # deterministic UUIDv5; managed by Repo Manager
|
||||
` ` `
|
||||
|
||||
Task description text.
|
||||
|
|
@ -208,6 +201,5 @@ not a kind. Fleet list lives on State Hub, not in `SCOPE.md`.
|
|||
|
||||
To create a new workplan:
|
||||
1. Write the file following the format above
|
||||
2. Run `statehub fix-consistency` locally.
|
||||
3. On a non-registrar C-06/C-11 skip, use the repo-manager fallback documented
|
||||
above exactly once; never set registrar authority directly.
|
||||
2. Run `uv run --project ~/repo-manager rmgr sync --path . --push`.
|
||||
3. Run `statehub fix-consistency` only when a separate deep audit is needed.
|
||||
|
|
|
|||
|
|
@ -15,31 +15,19 @@ Look for TODOs, open branches, half-finished files. Note done vs. started but in
|
|||
Propose 1–3 workplans — each a coherent strand, weeks to months, anchored to a
|
||||
roadmap phase. **Wait for approval before creating.**
|
||||
|
||||
**Step 4 — Write the workplan file; fix-consistency registers it (ADR-001)**
|
||||
**Step 4 — Write the workplan file; Repo Manager projects it (ADR-001)**
|
||||
```
|
||||
workplans/{WP_PREFIX}-NNNN-<slug>.md ← write this, commit it
|
||||
```
|
||||
Then register by running the consistency check — do **not** call
|
||||
`create_workplan`/`create_task` yourself; manual registration duplicates what
|
||||
C-06 creates from the file:
|
||||
Then run the deterministic, forge-derived sync — do **not** call
|
||||
`create_workplan`/`create_task` yourself:
|
||||
```bash
|
||||
statehub fix-consistency --repo {REPO_SLUG}
|
||||
uv run --project ~/repo-manager rmgr sync --path . --push
|
||||
```
|
||||
C-06 creates the hub workplan + tasks and writes `state_hub_workstream_id`
|
||||
(legacy frontmatter name — holds the workplan UUID) and `state_hub_task_id`
|
||||
back into the file.
|
||||
|
||||
If C-06/C-11 is skipped because the host is not the identifier registrar,
|
||||
commit and push the new workplan, then invoke the governed fallback once:
|
||||
|
||||
```bash
|
||||
uv run --project ~/repo-manager rmgr registrar-reconcile \
|
||||
--path . --confirm-primary --push
|
||||
```
|
||||
|
||||
Never export `STATEHUB_REGISTRAR` or create the hub records manually. If the
|
||||
fallback is unavailable, send one request to `repo-manager` and keep working
|
||||
from the authoritative files.
|
||||
Repo Manager writes deterministic `state_hub_workstream_id` and
|
||||
`state_hub_task_id` values, pushes the file, verifies `primary/railliance01`,
|
||||
and asks central to derive that exact Forgejo commit. If connectivity is down,
|
||||
the queued receipt is pending evidence; rerun the same command later.
|
||||
|
||||
**Step 5 — Record the setup**
|
||||
```
|
||||
|
|
|
|||
|
|
@ -58,14 +58,10 @@ If no workplans: follow First Session Protocol (`first-session.md`).
|
|||
|
||||
> State Hub is a *read model*. **Never register workplans or tasks by hand**
|
||||
> (`create_workplan`, `create_task`) — write the workplan file in `workplans/`
|
||||
> and run `fix-consistency`; C-06 registers the workplan and tasks and writes
|
||||
> IDs back into the file. Manual registration creates duplicates when
|
||||
> fix-consistency runs. Work structure belongs in repo files (ADR-001).
|
||||
> If C-06/C-11 is skipped on a non-registrar host, do not retry or set registrar
|
||||
> authority directly. Commit and push the file changes, then run once:
|
||||
> `uv run --project ~/repo-manager rmgr registrar-reconcile --path .
|
||||
> --confirm-primary --push`. If unavailable, send one deduplicated request to
|
||||
> `repo-manager` and continue from the files.
|
||||
> and run `uv run --project ~/repo-manager rmgr sync --path . --push`.
|
||||
> Repo Manager assigns missing deterministic IDs; central derives the exact
|
||||
> pushed Forgejo commit. Manual registration creates duplicate ownership.
|
||||
> Work structure belongs in repo files (ADR-001).
|
||||
>
|
||||
> Legacy: `create_workstream` and `/workstreams/` remain as metered aliases —
|
||||
> see `workplan-convention.md` (compatibility footnote).
|
||||
|
|
@ -77,9 +73,8 @@ If no workplans: follow First Session Protocol (`first-session.md`).
|
|||
a child workplan / decision / engagement). Do not leave actionable leftovers
|
||||
only as prose or in `SCOPE.md`. See work-record-types § Residuals.
|
||||
3. Log progress (below).
|
||||
4. `statehub fix-consistency` when workplan/queue files changed.
|
||||
A non-registrar C-06/C-11 skip uses the scoped repo-manager fallback above;
|
||||
repeated consistency runs cannot assign the missing UUIDs.
|
||||
4. `uv run --project ~/repo-manager rmgr sync --path . --push` when workplan
|
||||
files changed. Use `statehub fix-consistency` separately for a deep audit.
|
||||
|
||||
With MCP tools:
|
||||
```
|
||||
|
|
@ -95,13 +90,7 @@ If workplan files were modified, ensure the local copy is up to date first,
|
|||
then sync from the repo checkout:
|
||||
```bash
|
||||
git pull --ff-only
|
||||
statehub fix-consistency
|
||||
uv run --project ~/repo-manager rmgr sync --path . --push
|
||||
```
|
||||
For repos where implementation runs on a remote machine (e.g. CoulombCore),
|
||||
use the pull-before-fix mode from any shell with the State Hub CLI:
|
||||
```bash
|
||||
statehub fix-consistency --repo {REPO_SLUG} --remote
|
||||
```
|
||||
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
|
||||
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
|
||||
until you pull — intentional to prevent clobbering remote progress.
|
||||
The sync refuses uncommitted workplan files and a branch behind its upstream.
|
||||
This prevents a workstation projection from getting ahead of the forge source.
|
||||
|
|
|
|||
|
|
@ -25,23 +25,17 @@ Promote anything requiring analysis, design, approval, dependencies, or multiple
|
|||
planned phases into a normal workplan.
|
||||
|
||||
Ecosystem todos from other agents arrive as `[repo:{REPO_SLUG}]` hub tasks —
|
||||
visible at session start. Pick one up by creating the workplan file, committing,
|
||||
and running `statehub fix-consistency` — C-06 registers the workplan in the hub.
|
||||
Never register by hand with `create_workplan` (legacy MCP alias: `create_workstream`).
|
||||
|
||||
If `fix-consistency` reports C-06/C-11 skipped because this host is not the
|
||||
identifier registrar, further retries cannot help. Do not set
|
||||
`STATEHUB_REGISTRAR` and do not create hub rows manually. Commit and push the
|
||||
file-backed work, then run the scoped repo-manager fallback once:
|
||||
visible at session start. Pick one up by creating the workplan file, then run
|
||||
the fast authoritative projection path:
|
||||
|
||||
```bash
|
||||
uv run --project ~/repo-manager rmgr registrar-reconcile \
|
||||
--path . --confirm-primary --push
|
||||
uv run --project ~/repo-manager rmgr sync --path . --push
|
||||
```
|
||||
|
||||
If it is unavailable, send one request to `repo-manager` naming the repository
|
||||
and missing canonical ids. Continue local work from files; hub UUID absence is
|
||||
an indexing delay, not a reason to repeat the same checks.
|
||||
Repo Manager assigns only missing deterministic identifiers. Central reads the
|
||||
exact pushed Forgejo commit and updates its replaceable projection. Never
|
||||
register by hand with `create_workplan` or `create_task`. Use
|
||||
`statehub fix-consistency` separately for a deep audit.
|
||||
|
||||
Task blocks use this shape:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue