Removed obsolete TODO.md

This commit is contained in:
2026-04-30 14:04:22 +02:00
parent 3620e2c7fc
commit 39612bde53
4 changed files with 337 additions and 97 deletions

76
TODO.md
View File

@@ -1,76 +0,0 @@
# TODO — Custodian Integration Notes
These notes are for the Codex agent working in this repository. They document the
current state of the Custodian State Hub integration and what to be aware of.
---
## Git Push Credentials (C-17 Warning)
The Custodian consistency checker warns when local commits have not been pushed to
the remote. The onboarding commit (AGENTS.md, SCOPE.md, ADR-001 workplans) is
currently unpushed because Gitea HTTP credentials are not configured in this
environment.
This does not block normal work, but the checker will warn at each run.
To resolve, either configure HTTP credentials:
```bash
git config credential.helper store
git push # prompts once, then stores
```
Or switch the remote to SSH (if an SSH key is registered on Gitea):
```bash
git remote set-url origin git@92.205.130.254:coulomb/repo-registry.git
git push
```
Until resolved, the consistency checker will log C-17 and skip file write-backs.
Task statuses you update in workplan files will still be visible locally; they will
sync to the hub DB once the push succeeds.
---
## AGENTS.md
`AGENTS.md` at the repo root is your primary instruction file for interacting with
the Custodian State Hub. Read it at the start of each session. It documents:
- State Hub HTTP API endpoints (orient, inbox, progress logging)
- Session protocol (start / during / close)
- Workplan file convention (ADR-001 format)
- How to create new workplans and notify the hub
---
## Active Workplan
`workplans/RREG-WP-0004-characteristic-classification-navigation.md` is the
current active workplan. Start with T01 (P0: Characteristic Classification Fields)
as the highest-priority item.
---
## Custodian Tooling Improvements (2026-04-26)
The following improvements were made to the Custodian registration tooling as a
result of onboarding this repository as the first Codex-based repo:
1. **`register_project.sh --codex` flag** — new flag skips MCP registration check
(Claude Code-specific) and generates `AGENTS.md` from an HTTP-API-based template
instead of `CLAUDE.md` and `.claude/rules/`. Future Codex repos can use:
```bash
cd ~/the-custodian/state-hub
make register-codex-project DOMAIN=<domain> PROJECT_PATH=/path/to/repo
```
2. **`agents-codex.template`** — new parameterised template in
`state-hub/scripts/project_rules/` that produces the HTTP REST session protocol
for Codex agents (this file was used to generate `AGENTS.md` in this repo).
These changes are committed in `the-custodian`. If the tooling needs further
refinement based on how Codex works with this integration, note it here and the
custodian operator will update the templates.

View File

@@ -765,6 +765,16 @@ def repository_detail(
<h2>Review Decisions</h2> <h2>Review Decisions</h2>
{render_review_decisions(decisions)} {render_review_decisions(decisions)}
</section> </section>
<section class="panel" style="margin-top:18px">
<h2>Classification Quality Feedback</h2>
{render_expectation_gap_form(
action=f"/ui/repos/{repository_id}/expectation-gaps",
default_type="classification",
default_name="Wrong or missing characteristic classification",
default_notes="",
)}
{render_expectation_gaps(service.list_expectation_gaps(repository_id))}
</section>
<section class="panel" style="margin-top:18px"> <section class="panel" style="margin-top:18px">
<h2>Delete Repository</h2> <h2>Delete Repository</h2>
<form class="stack" method="post" action="/ui/repos/{repository_id}/delete"> <form class="stack" method="post" action="/ui/repos/{repository_id}/delete">
@@ -1192,15 +1202,12 @@ def analysis_run_detail(
</section> </section>
<section class="panel" style="margin-top:18px"> <section class="panel" style="margin-top:18px">
<h2>Expectation Gaps</h2> <h2>Expectation Gaps</h2>
<form class="stack" method="post" action="/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/expectation-gaps"> {render_expectation_gap_form(
<div class="grid"> action=f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/expectation-gaps",
<label>Expected type <input name="expected_type" placeholder="capability, feature, fact, classification" required></label> default_type="classification",
<label>Expected name <input name="expected_name" placeholder="Use OpenRouter Models" required></label> default_name="Missing or wrong classification",
<label>Source <input name="source" value="human" required></label> default_notes="",
<label>Notes <input name="notes" placeholder="What made you expect this?"></label> )}
</div>
<button type="submit">Record Gap</button>
</form>
{render_expectation_gaps(expectation_gaps)} {render_expectation_gaps(expectation_gaps)}
</section> </section>
""" """
@@ -1231,6 +1238,25 @@ def create_expectation_gap_from_form(
) )
@router.post("/ui/repos/{repository_id}/expectation-gaps")
def create_repository_expectation_gap_from_form(
repository_id: int,
expected_type: str = Form(...),
expected_name: str = Form(...),
source: str = Form("human"),
notes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.record_expectation_gap(
repository_id,
expected_type=expected_type,
expected_name=expected_name,
source=source,
notes=notes,
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@router.get("/ui/repos/{repository_id}/elements") @router.get("/ui/repos/{repository_id}/elements")
def repository_element_listing( def repository_element_listing(
repository_id: int, repository_id: int,
@@ -1308,6 +1334,13 @@ def repository_element_listing(
<h1 style="margin-right:auto">{escape(title)}</h1> <h1 style="margin-right:auto">{escape(title)}</h1>
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a> <a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
</div> </div>
{render_element_breadcrumbs(
repository_id=repository_id,
scope=scope,
item_type=type,
entry_filter=entry_filter,
analysis_run_id=analysis_run_id,
)}
<section class="panel" style="margin-bottom:18px"> <section class="panel" style="margin-bottom:18px">
<form class="stack" method="get" action="{filter_action}"> <form class="stack" method="get" action="{filter_action}">
<input type="hidden" name="scope" value="{escape(listing_scope)}"> <input type="hidden" name="scope" value="{escape(listing_scope)}">
@@ -1331,6 +1364,13 @@ def repository_element_listing(
</form> </form>
</section> </section>
<section class="panel"> <section class="panel">
{render_element_type_nav(
repository_id=repository_id,
scope=listing_scope,
item_type=type,
entry_filter=entry_filter,
analysis_run_id=analysis_run_id,
)}
<table> <table>
<thead><tr><th>Entry</th><th>Class</th><th>Name</th><th>Parent</th><th>Source</th><th>Actions</th></tr></thead> <thead><tr><th>Entry</th><th>Class</th><th>Name</th><th>Parent</th><th>Source</th><th>Actions</th></tr></thead>
<tbody>{rows}</tbody> <tbody>{rows}</tbody>
@@ -2318,14 +2358,25 @@ def graph_element_rows(
ability["name"], ability["name"],
"", "",
ability.get("source_refs", []), ability.get("source_refs", []),
item_id=ability.get("id"), item_id=ability.get("id"),
item_kind="abilities", item_kind="abilities",
description=ability.get("description", ""), description=ability.get("description", ""),
confidence=ability.get("confidence", 1.0), confidence=ability.get("confidence", 1.0),
attributes=ability.get("attributes", []), attributes=ability.get("attributes", []),
status=ability.get("status", ""), child_counts={
entry_state=entry_state, "capabilities": len(ability.get("capabilities", [])),
) "features": sum(
len(capability.get("features", []))
for capability in ability.get("capabilities", [])
),
"supports": sum(
len(capability.get("evidence", []))
for capability in ability.get("capabilities", [])
),
},
status=ability.get("status", ""),
entry_state=entry_state,
)
) )
for capability in ability.get("capabilities", []): for capability in ability.get("capabilities", []):
if item_type == "capabilities": if item_type == "capabilities":
@@ -2340,6 +2391,10 @@ def graph_element_rows(
description=capability.get("description", ""), description=capability.get("description", ""),
confidence=capability.get("confidence", 1.0), confidence=capability.get("confidence", 1.0),
attributes=capability.get("attributes", []), attributes=capability.get("attributes", []),
child_counts={
"features": len(capability.get("features", [])),
"supports": len(capability.get("evidence", [])),
},
inputs=capability.get("inputs", []), inputs=capability.get("inputs", []),
outputs=capability.get("outputs", []), outputs=capability.get("outputs", []),
status=capability.get("status", ""), status=capability.get("status", ""),
@@ -2360,6 +2415,7 @@ def graph_element_rows(
attributes=feature.get("attributes", []), attributes=feature.get("attributes", []),
confidence=feature.get("confidence", 1.0), confidence=feature.get("confidence", 1.0),
location=feature.get("location", ""), location=feature.get("location", ""),
child_counts={"facts": len(feature.get("source_refs", []))},
status=feature.get("status", ""), status=feature.get("status", ""),
entry_state=entry_state, entry_state=entry_state,
) )
@@ -2533,7 +2589,7 @@ def render_element_row(
<tr> <tr>
<td>{render_entry_badge(row)}</td> <td>{render_entry_badge(row)}</td>
<td><span class="pill">{escape(str(row["primary_class"]))}</span>{render_attribute_pills(row)}</td> <td><span class="pill">{escape(str(row["primary_class"]))}</span>{render_attribute_pills(row)}</td>
<td>{escape(str(row["name"]))}</td> <td>{escape(str(row["name"]))}{render_drilldown_links(row, repository_id, analysis_run_id)}</td>
<td>{escape(str(row["parent"]))}</td> <td>{escape(str(row["parent"]))}</td>
<td>{render_element_source_detail(row)}</td> <td>{render_element_source_detail(row)}</td>
<td>{render_element_actions(row, repository_id, analysis_run_id)}</td> <td>{render_element_actions(row, repository_id, analysis_run_id)}</td>
@@ -2541,6 +2597,109 @@ def render_element_row(
""" """
def render_element_breadcrumbs(
*,
repository_id: int,
scope: str,
item_type: str,
entry_filter: str,
analysis_run_id: int | None,
) -> str:
scope_label = "Facts" if scope == "facts" else (entry_filter or "all")
href = (
f"/ui/repos/{repository_id}/elements?scope={escape(scope)}"
f"&type={escape(item_type)}{render_analysis_run_query_suffix(analysis_run_id)}"
)
return f"""
<p class="actions">
<a class="pill" href="/ui/repos/{repository_id}">Repository</a>
<span class="pill">{escape(scope_label)}</span>
<a class="pill" href="{href}">{escape(item_type)}</a>
</p>
"""
def render_element_type_nav(
*,
repository_id: int,
scope: str,
item_type: str,
entry_filter: str,
analysis_run_id: int | None,
) -> str:
items = [
("scopes", "Scope"),
("abilities", "Abilities"),
("capabilities", "Capabilities"),
("features", "Features"),
("supports", "Supports"),
("facts", "Facts"),
]
links = []
for value, label in items:
link_scope = "facts" if value == "facts" else scope
params = (
f"scope={quote_plus(link_scope)}&type={quote_plus(value)}"
f"{render_analysis_run_query_suffix(analysis_run_id)}"
)
if value != "facts" and entry_filter:
params += f"&entry_filter={quote_plus(entry_filter)}"
active = " active" if value == item_type else ""
links.append(
f'<a class="pill{active}" href="/ui/repos/{repository_id}/elements?{params}">{escape(label)}</a>'
)
return f'<p class="actions">{"".join(links)}</p>'
def render_drilldown_links(
row: dict,
repository_id: int,
analysis_run_id: int | None,
) -> str:
item_kind = row.get("item_kind")
entry_state = str(row.get("entry_state") or "approved")
counts = row.get("child_counts", {})
links: list[tuple[str, str, str]] = []
if item_kind == "scope":
links.append(("abilities", "abilities", ""))
elif item_kind == "abilities":
links.extend(
[
("capabilities", "capabilities", str(row["name"])),
("features", "features", str(row["name"])),
("supports", "supports", str(row["name"])),
]
)
elif item_kind == "capabilities":
links.extend(
[
("features", "features", str(row["name"])),
("supports", "supports", str(row["name"])),
]
)
elif item_kind == "features":
fact_query = str(row.get("location") or row.get("name") or "")
links.append(("facts", "facts", fact_query))
if not links:
return ""
rendered = []
for item_type, label, query in links:
count = counts.get(item_type)
label_text = f"{count} {label}" if count is not None else label
scope = "facts" if item_type == "facts" else "all"
params = f"scope={scope}&type={item_type}"
if item_type != "facts":
params += f"&entry_filter={quote_plus(entry_state)}"
if query:
params += f"&q={quote_plus(query)}"
if analysis_run_id is not None:
params += f"&analysis_run_id={analysis_run_id}"
rendered.append(
f'<a class="pill" href="/ui/repos/{repository_id}/elements?{params}">{escape(label_text)}</a>'
)
return f'<p class="actions">{ "".join(rendered) }</p>'
def render_attribute_pills(row: dict) -> str: def render_attribute_pills(row: dict) -> str:
attributes = [ attributes = [
str(attribute) str(attribute)
@@ -3028,6 +3187,54 @@ def render_review_decisions(decisions: list) -> str:
""" """
def render_expectation_gap_form(
*,
action: str,
default_type: str,
default_name: str,
default_notes: str,
) -> str:
return f"""
<form class="stack" method="post" action="{action}">
<div class="grid">
<label>Expected type
<select name="expected_type">
{render_gap_type_options(default_type)}
</select>
</label>
<label>Expected name <input name="expected_name" value="{escape(default_name)}" required></label>
<label>Source <input name="source" value="human" required></label>
<label>Notes <input name="notes" value="{escape(default_notes)}" placeholder="Expected class, attribute, or organization smell"></label>
</div>
<div class="actions">
<button type="submit">Record Gap</button>
<button class="secondary" type="submit" name="expected_type" value="classification-primary">Missing Primary Class</button>
<button class="secondary" type="submit" name="expected_type" value="classification-attribute">Wrong Attribute</button>
<button class="secondary" type="submit" name="expected_type" value="classification-granularity">Granularity Smell</button>
<button class="secondary" type="submit" name="expected_type" value="classification-support">Support Organization Smell</button>
</div>
</form>
"""
def render_gap_type_options(selected: str) -> str:
options = [
"ability",
"capability",
"feature",
"fact",
"classification",
"classification-primary",
"classification-attribute",
"classification-granularity",
"classification-support",
]
return "".join(
f'<option value="{escape(option)}"{" selected" if option == selected else ""}>{escape(option)}</option>'
for option in options
)
def render_expectation_gaps(gaps: list) -> str: def render_expectation_gaps(gaps: list) -> str:
if not gaps: if not gaps:
return '<p class="muted">No expectation gaps recorded for this run.</p>' return '<p class="muted">No expectation gaps recorded for this run.</p>'
@@ -3279,7 +3486,10 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
<li id="capability-{capability['id']}"> <li id="capability-{capability['id']}">
<strong>{escape(capability['name'])}</strong> <strong>{escape(capability['name'])}</strong>
<span class="pill">ID {capability['id']}</span> <span class="pill">ID {capability['id']}</span>
<span class="pill">{escape(capability.get('primary_class', 'capability'))}</span>
{render_characteristic_attributes(capability)}
<span class="pill">{capability['confidence']:.2f} {escape(capability['confidence_label'])}</span> <span class="pill">{capability['confidence']:.2f} {escape(capability['confidence_label'])}</span>
{render_approved_tree_drilldown(repository_id, "capabilities", capability)}
<p class="muted">{escape(capability['description'])}</p> <p class="muted">{escape(capability['description'])}</p>
{render_approved_capability_forms(capability, repository_id)} {render_approved_capability_forms(capability, repository_id)}
<h3>Features</h3> <h3>Features</h3>
@@ -3294,7 +3504,10 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
<li id="ability-{ability['id']}"> <li id="ability-{ability['id']}">
<strong>{escape(ability['name'])}</strong> <strong>{escape(ability['name'])}</strong>
<span class="pill">ID {ability['id']}</span> <span class="pill">ID {ability['id']}</span>
<span class="pill">{escape(ability.get('primary_class', 'ability'))}</span>
{render_characteristic_attributes(ability)}
<span class="pill">{ability['confidence']:.2f} {escape(ability['confidence_label'])}</span> <span class="pill">{ability['confidence']:.2f} {escape(ability['confidence_label'])}</span>
{render_approved_tree_drilldown(repository_id, "abilities", ability)}
<p class="muted">{escape(ability['description'])}</p> <p class="muted">{escape(ability['description'])}</p>
{render_approved_ability_forms(ability, repository_id)} {render_approved_ability_forms(ability, repository_id)}
<ul>{''.join(capabilities)}</ul> <ul>{''.join(capabilities)}</ul>
@@ -3371,8 +3584,11 @@ def render_approved_feature(feature: dict, repository_id: int) -> str:
<li> <li>
{escape(feature["name"])} {escape(feature["name"])}
<span class="pill">{escape(feature["type"])}</span> <span class="pill">{escape(feature["type"])}</span>
<span class="pill">{escape(feature.get("primary_class", feature["type"]))}</span>
{render_characteristic_attributes(feature)}
<span class="pill">{feature["confidence"]:.2f} {escape(feature["confidence_label"])}</span> <span class="pill">{feature["confidence"]:.2f} {escape(feature["confidence_label"])}</span>
<span class="source">{escape(feature["location"])}</span> <span class="source">{escape(feature["location"])}</span>
{render_approved_tree_drilldown(repository_id, "features", feature)}
{render_sources(feature.get("source_refs", []))} {render_sources(feature.get("source_refs", []))}
<form class="stack" method="post" action="/ui/repos/{repository_id}/features/{feature['id']}/edit"> <form class="stack" method="post" action="/ui/repos/{repository_id}/features/{feature['id']}/edit">
<label>Name <input name="name" value="{escape(feature['name'])}" required></label> <label>Name <input name="name" value="{escape(feature['name'])}" required></label>
@@ -3388,6 +3604,70 @@ def render_approved_feature(feature: dict, repository_id: int) -> str:
""" """
def render_characteristic_attributes(item: dict) -> str:
primary = str(item.get("primary_class", ""))
attributes = [
str(attribute)
for attribute in item.get("attributes", [])
if str(attribute) and str(attribute) != primary
]
return "".join(
f' <span class="pill">{escape(attribute)}</span>'
for attribute in attributes
)
def render_approved_tree_drilldown(
repository_id: int,
item_kind: str,
item: dict,
) -> str:
name = str(item.get("name", ""))
if item_kind == "abilities":
links = [
("capabilities", len(item.get("capabilities", [])), name),
(
"features",
sum(
len(capability.get("features", []))
for capability in item.get("capabilities", [])
),
name,
),
(
"supports",
sum(
len(capability.get("evidence", []))
for capability in item.get("capabilities", [])
),
name,
),
]
elif item_kind == "capabilities":
links = [
("features", len(item.get("features", [])), name),
("supports", len(item.get("evidence", [])), name),
]
elif item_kind == "features":
links = [("facts", len(item.get("source_refs", [])), item.get("location", name))]
else:
links = []
if not links:
return ""
rendered = []
for target_type, count, query in links:
scope = "facts" if target_type == "facts" else "all"
params = f"scope={scope}&type={target_type}"
if target_type != "facts":
params += "&entry_filter=approved"
if query:
params += f"&q={quote_plus(str(query))}"
rendered.append(
f'<a class="pill" href="/ui/repos/{repository_id}/elements?{params}">{count} {escape(target_type)}</a>'
)
return f'<p class="actions">{"".join(rendered)}</p>'
def render_approved_evidence(evidence: dict, repository_id: int) -> str: def render_approved_evidence(evidence: dict, repository_id: int) -> str:
target_kind = escape(str(evidence.get("target_kind") or "capability")) target_kind = escape(str(evidence.get("target_kind") or "capability"))
target_id = evidence.get("target_id") target_id = evidence.get("target_id")

View File

@@ -1873,6 +1873,12 @@ def test_ui_manual_registry_entry_loop(tmp_path):
assert "Edited Manual Ability" in detail_response.text assert "Edited Manual Ability" in detail_response.text
assert "Edited Manual Capability" in detail_response.text assert "Edited Manual Capability" in detail_response.text
assert "Edited Manual API" in detail_response.text assert "Edited Manual API" in detail_response.text
assert "Classification Quality Feedback" in detail_response.text
assert "classification-primary" in detail_response.text
assert "1 capabilities" in detail_response.text
assert "1 features" in detail_response.text
assert "2 supports" in detail_response.text
assert "0 facts" in detail_response.text
assert "tests/test_manual.py" in detail_response.text assert "tests/test_manual.py" in detail_response.text
assert f"references feature #{feature_id}" in detail_response.text assert f"references feature #{feature_id}" in detail_response.text
assert "downward support" in detail_response.text assert "downward support" in detail_response.text
@@ -1900,9 +1906,26 @@ def test_ui_manual_registry_entry_loop(tmp_path):
) )
assert filtered_feature_listing.status_code == 200 assert filtered_feature_listing.status_code == 200
assert "Attribute" in filtered_feature_listing.text assert "Attribute" in filtered_feature_listing.text
assert "Repository" in filtered_feature_listing.text
assert "Facts" in filtered_feature_listing.text
assert "Edited Manual API" in filtered_feature_listing.text assert "Edited Manual API" in filtered_feature_listing.text
assert "1 of 1 shown" in filtered_feature_listing.text assert "1 of 1 shown" in filtered_feature_listing.text
classification_gap_response = client.post(
f"{repository_path}/expectation-gaps",
data={
"expected_type": "classification-support",
"expected_name": "Feature references another feature too broadly",
"source": "human",
"notes": "Same-level support should be reviewed.",
},
follow_redirects=False,
)
assert classification_gap_response.status_code == 303
detail_response = client.get(repository_path)
assert "Feature references another feature too broadly" in detail_response.text
assert "classification-support" in detail_response.text
upward_support_listing = client.get( upward_support_listing = client.get(
f"/ui/repos/{repository_id}/elements", f"/ui/repos/{repository_id}/elements",
params={ params={

View File

@@ -4,7 +4,7 @@ type: workplan
title: "Repository Ability Registry - Characteristic Classification And Navigation" title: "Repository Ability Registry - Characteristic Classification And Navigation"
domain: capabilities domain: capabilities
repo: repo-registry repo: repo-registry
status: active status: done
owner: codex owner: codex
topic_slug: foerster-capabilities topic_slug: foerster-capabilities
created: "2026-04-29" created: "2026-04-29"
@@ -94,7 +94,7 @@ surface/provider attributes such as `api`, `cli`, `http`, `llm-provider`,
```task ```task
id: RREG-WP-0004-T04 id: RREG-WP-0004-T04
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "14ede41f-a0cb-4a9a-b4ba-f23b34d7ae33" state_hub_task_id: "14ede41f-a0cb-4a9a-b4ba-f23b34d7ae33"
``` ```
@@ -107,11 +107,18 @@ Acceptance: the repository page and element listings make it natural to move
from scope to abilities to lower-level support/facts, with counts, filters, and from scope to abilities to lower-level support/facts, with counts, filters, and
clear breadcrumbs. clear breadcrumbs.
Implementation note 2026-04-30: approved characteristic trees and element
listings now expose explicit drilldown links. Abilities link to capabilities,
features, and supports; capabilities link to features and supports; features
link to observed facts by source path. Element listings include breadcrumbs and
a type navigation row for scope, abilities, capabilities, features, supports,
and facts.
## P2: Classification Quality Feedback ## P2: Classification Quality Feedback
```task ```task
id: RREG-WP-0004-T05 id: RREG-WP-0004-T05
status: todo status: done
priority: low priority: low
state_hub_task_id: "691f3cb7-c8a2-4f80-a6c2-29cb5a0c7a96" state_hub_task_id: "691f3cb7-c8a2-4f80-a6c2-29cb5a0c7a96"
``` ```
@@ -122,3 +129,9 @@ same-level/upward support patterns that indicate suboptimal organization.
Acceptance: reviewers can record classification-specific improvement inputs that Acceptance: reviewers can record classification-specific improvement inputs that
feed the scanner coevolution workflow. feed the scanner coevolution workflow.
Implementation note 2026-04-30: classification feedback is captured through
expectation gaps with dedicated types for missing primary classes, wrong
attributes, granularity smells, and support organization smells. Run pages and
repository pages expose the feedback form so reviewers can record optimization
inputs with or without a specific analysis run.