From f4a57e60ef33c7da4023a0915d1957053f8a3b40 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 12:15:46 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20intake=20census=20+=20dashboard=20modes?= =?UTF-8?q?=20=E2=80=94=20Mind=20backlog=20inventory=20+=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit census walks every WORK_TYPES folder (incl. triage/), parses the light metadata header back out of each filed prompt, and reports records, aggregates and hygiene flags (headerless legacy prompts). dashboard renders that census as PyAutoMind/dashboard.md — the Mind backlog page, written only under --apply. Folder position stays authoritative for work-type/target; a header Target: is prose and never overrides the taxonomy. Backlog only by design — health remains the Heart's. Also fixes the intake --help sed range, which leaked shell lines past the header comment. Closes #30. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016ttFH96vcXuCVZfSBZEytf --- agents/conductors/intake/AGENTS.md | 14 +- agents/conductors/intake/INTAKE_TAXONOMY.md | 17 +- agents/conductors/intake/_intake.py | 166 ++++++++++++++++++++ agents/conductors/intake/intake.sh | 16 +- skills/COMMANDS.md | 2 +- skills/intake/intake.md | 10 ++ 6 files changed, 214 insertions(+), 11 deletions(-) diff --git a/agents/conductors/intake/AGENTS.md b/agents/conductors/intake/AGENTS.md index 5b3ddb2..88937b3 100644 --- a/agents/conductors/intake/AGENTS.md +++ b/agents/conductors/intake/AGENTS.md @@ -72,10 +72,15 @@ schema — light structure over free-form prose. |------|---------|--------------| | **classify** | `intake ""` / `intake classify --file P` | classify one raw input; `--apply` writes the prompt | | **ideas** | `intake ideas` | scan `ideas.md`, propose one prompt per bullet; `--apply` writes them | +| **census** | `intake census` | inventory every filed prompt (work-type/target/difficulty/status + hygiene flags); always read-only | +| **dashboard** | `intake dashboard` | render the census as the Mind **backlog** page; `--apply` writes `PyAutoMind/dashboard.md` | -`census` / `repair` / `dashboard` (registry hygiene + a Mind *backlog* dashboard, -distinct from Heart's `/health status` health view) are the **planned follow-up** -— not in this first cut. +Census/dashboard are the Mind *backlog* view — deliberately distinct from +Heart's `/health status` health view (see "must never do"). The prompt-taxonomy +folder is authoritative for a prompt's work-type/target; header fields are +display metadata, and headerless legacy prompts surface as hygiene flags rather +than errors. `repair` (fixing those flags in place) is the **planned follow-up** +— not in this cut. ## Run @@ -85,6 +90,9 @@ bin/pyauto-brain intake --json "" # machine-rea bin/pyauto-brain intake --apply classify --file tmp/raw.md # write the prompt bin/pyauto-brain intake ideas # scan ideas.md (dry-run) bin/pyauto-brain intake --apply ideas # write them + mark bullets +bin/pyauto-brain intake census # backlog inventory (read-only) +bin/pyauto-brain intake dashboard # backlog page to stdout (dry-run) +bin/pyauto-brain intake --apply dashboard # write PyAutoMind/dashboard.md ``` **Writes only under `--apply`; dry-run is the default.** Exit codes: `0` produced diff --git a/agents/conductors/intake/INTAKE_TAXONOMY.md b/agents/conductors/intake/INTAKE_TAXONOMY.md index 7bbfc3a..db7609f 100644 --- a/agents/conductors/intake/INTAKE_TAXONOMY.md +++ b/agents/conductors/intake/INTAKE_TAXONOMY.md @@ -94,7 +94,22 @@ No YAML, no required fields — this is the light header PyAutoMind blesses in `README.md` "Prompt file format", extended with the three intake keys (`Difficulty:/Autonomy:/Priority:`). `Type:` always matches the work-type folder. -## 6. What intake does NOT own +## 6. Census + dashboard (reading the taxonomy back) + +The write path above has a read path: **census** walks every work-type folder +(incl. `triage/`) and parses the same light header back out of each prompt +(`parse_header`), producing per-prompt records plus aggregates by work-type, +target, difficulty and priority. The **folder position is authoritative** for +work-type/target — a header `Target:` is free prose and never overrides the +taxonomy. Prompts that pre-date the header surface as *hygiene flags* (shown as +`-`), never errors. `issued/` prompts are already dispatched: counted, not +itemised. + +**dashboard** renders that census as `PyAutoMind/dashboard.md` — the Mind +*backlog* page (never health; that is Heart's). Census is always read-only; +dashboard writes only under `--apply`. + +## 7. What intake does NOT own - The difficulty heuristic, prompt parsing, repo sets → the **sizing faculty**. - The work-type → agent routing → `PyAutoMind/ROUTING.md` + the conductors. diff --git a/agents/conductors/intake/_intake.py b/agents/conductors/intake/_intake.py index e256d4d..28a456d 100755 --- a/agents/conductors/intake/_intake.py +++ b/agents/conductors/intake/_intake.py @@ -328,6 +328,144 @@ def write_prompt(mind: Path, decision: dict, body_text: str, source_note: str): return str(rel) +# --- census + dashboard --------------------------------------------------------- +# The header convention this agent writes (see _render_header); census parses the +# same fields back out of every filed prompt. Legacy prompts pre-date the header, +# so every field is optional — absence is reported, never fatal. +HEADER_FIELDS = ("type", "target", "difficulty", "autonomy", "priority", "status") + + +def parse_header(text: str) -> dict: + """Extract the light metadata header (`Field: value` lines) from a prompt. + + Only scans the top of the file so a stray "Status:" deep in prose does not + fire; first occurrence of each field wins. No YAML — the blessed convention. + """ + fields = {} + for line in text.splitlines()[:30]: + m = re.match(r"(Type|Target|Difficulty|Autonomy|Priority|Status):\s*(\S.*)", + line.strip()) + if m: + fields.setdefault(m.group(1).lower(), m.group(2).strip()) + return fields + + +def census(mind: Path) -> dict: + """Inventory every filed prompt — one record per `/**/*.md`. + + Read-only, always. Walks the WORK_TYPES folders (incl. `triage/`); `issued/` + prompts are already dispatched, so they are counted but not itemised. This is + the Mind *backlog* view — health belongs to the Heart, never here. + """ + records, hygiene = [], [] + for wt in WORK_TYPES: + folder = mind / wt + if not folder.is_dir(): + continue + for f in sorted(folder.rglob("*.md")): + if f.name == "README.md": + continue + text = f.read_text(encoding="utf-8", errors="replace") + rel = f.relative_to(mind) + header = parse_header(text) + missing = [h for h in HEADER_FIELDS if h not in header] + records.append({ + "path": str(rel), + "work_type": wt, + # Second folder = target repo/domain (authoritative — a header + # Target: is free prose and must not override the taxonomy). + "target": rel.parts[1] if len(rel.parts) > 2 else "-", + "title": _title(text), + "difficulty": header.get("difficulty", "-"), + "autonomy": header.get("autonomy", "-"), + "priority": header.get("priority", "-"), + "status": header.get("status", "-"), + "header": header, + "missing": missing, + }) + if len(missing) == len(HEADER_FIELDS): + hygiene.append(f"{rel} — no metadata header (pre-dates intake)") + + def _count(key): + out = {} + for r in records: + out[r[key]] = out.get(r[key], 0) + 1 + return dict(sorted(out.items(), key=lambda kv: (-kv[1], kv[0]))) + + issued = mind / "issued" + return { + "generated": _dt.date.today().isoformat(), + "total": len(records), + "issued_count": sum(1 for _ in issued.glob("*.md")) if issued.is_dir() else 0, + "by_work_type": _count("work_type"), + "by_target": _count("target"), + "by_difficulty": _count("difficulty"), + "by_priority": _count("priority"), + "records": records, + "hygiene": hygiene, + } + + +def _cell(value: str) -> str: + return str(value).replace("|", "\\|") + + +def render_dashboard(c: dict) -> str: + """Render the census as the Mind backlog page (`dashboard.md`). + + Backlog only, by design: no readiness verdicts, no test state — that is the + Heart's dashboard (`/health`). Links are repo-root-relative so the page + renders cleanly on GitHub. + """ + L = [ + "# PyAutoMind backlog dashboard", + "", + f"", + "", + f"**{c['total']}** filed prompts in the backlog · **{c['issued_count']}** " + "already dispatched to issues (`issued/`). Backlog view only — organism " + "health lives with the Heart (`/health`), not here.", + "", + "| Work-type | Prompts |", + "|-----------|--------:|", + ] + L += [f"| {wt} | {n} |" for wt, n in c["by_work_type"].items()] + for wt in c["by_work_type"]: + rows = [r for r in c["records"] if r["work_type"] == wt] + rows.sort(key=lambda r: (r["target"], r["path"])) + L += ["", f"## {wt} ({len(rows)})", "", + "| Prompt | Target | Difficulty | Autonomy | Priority |", + "|--------|--------|------------|----------|----------|"] + L += [f"| [{_cell(r['title'])}]({r['path']}) | {r['target']} " + f"| {r['difficulty']} | {r['autonomy']} | {r['priority']} |" + for r in rows] + if c["hygiene"]: + L += ["", "## Hygiene", "", + f"{len(c['hygiene'])} prompt(s) without a metadata header — they " + "show `-` above. Re-home or re-run intake on them when touched.", "", + "
", "Headerless prompts", ""] + L += [f"- `{h.split(' — ')[0]}`" for h in c["hygiene"]] + L += ["", "
"] + return "\n".join(L) + "\n" + + +def emit_census(c: dict): + def _fmt(counts, top=None): + items = list(counts.items())[:top] + s = " · ".join(f"{k} {n}" for k, n in items) + return s + (" · …" if top and len(counts) > top else "") + + print("== Mind census ==") + print(f"Filed prompts: {c['total']} (already issued: {c['issued_count']})") + print(f"By work-type: {_fmt(c['by_work_type'])}") + print(f"By target: {_fmt(c['by_target'], top=8)}") + print(f"By difficulty: {_fmt(c['by_difficulty'])}") + print(f"By priority: {_fmt(c['by_priority'])}") + print(f"Hygiene: {len(c['hygiene'])} prompt(s) without a metadata " + "header (--json lists them)") + + # --- ideas.md scanning -------------------------------------------------------- def scan_ideas(mind: Path): """Yield (bullet_text, context_header) for substantive ideas.md lines.""" @@ -409,9 +547,37 @@ def main(argv=None): sub.add_parser("ideas", help="scan ideas.md and propose one prompt per bullet") + sub.add_parser("census", help="inventory all filed prompts (always read-only)") + + sub.add_parser("dashboard", help="render the census as the Mind backlog page; " + "--apply writes dashboard.md") + a = ap.parse_args(argv) mind = Path(a.mind) + if a.cmd == "census": + c = census(mind) + print(json.dumps(c, indent=2)) if a.as_json else emit_census(c) + return 0 + + if a.cmd == "dashboard": + c = census(mind) + page = render_dashboard(c) + written = None + if a.apply: + (mind / "dashboard.md").write_text(page, encoding="utf-8") + written = "dashboard.md" + if a.as_json: + summary = {k: v for k, v in c.items() if k != "records"} + print(json.dumps({"census": summary, "page": page, "written": written}, + indent=2)) + elif written: + print(f"Wrote: {written} ({c['total']} prompts, " + f"{len(c['hygiene'])} hygiene flag(s))") + else: + print(page, end="") + return 0 + if a.cmd == "classify": if a.file: src_path = Path(a.file) diff --git a/agents/conductors/intake/intake.sh b/agents/conductors/intake/intake.sh index 16e7082..d682ae6 100755 --- a/agents/conductors/intake/intake.sh +++ b/agents/conductors/intake/intake.sh @@ -18,10 +18,13 @@ # intake "" classify raw text (bare text => classify mode) # intake classify --file P classify the contents of a file # intake ideas scan ideas.md; propose one prompt per bullet +# intake census inventory all filed prompts (always read-only) +# intake dashboard render the census as the Mind backlog page; +# --apply writes PyAutoMind/dashboard.md # # Flags (place before the subcommand; both default OFF): -# --apply write the formal prompt file(s); without it, dry-run only -# --json emit the machine-readable IntakeDecision +# --apply write the formal prompt file(s) / dashboard.md; else dry-run only +# --json emit the machine-readable IntakeDecision / census # # The analysis core lives in _intake.py (stdlib-only). It writes ONLY under # --apply; every other path is read-only. @@ -39,7 +42,7 @@ apply=0 forward=() while [[ $# -gt 0 ]]; do case "$1" in - -h|--help) sed -n '2,34p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; + -h|--help) sed -n '2,33p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; --json) as_json=1; shift ;; --apply) apply=1; shift ;; *) forward+=("$1"); shift ;; @@ -54,9 +57,10 @@ if [[ ${#forward[@]} -eq 0 ]]; then fi # A bare first token that is not a known subcommand -> classify mode on the rest, # so `intake "raw idea"` and `intake --file p.md` both work as the front door. -if [[ "${forward[0]}" != "classify" && "${forward[0]}" != "ideas" ]]; then - forward=(classify "${forward[@]}") -fi +case "${forward[0]}" in + classify|ideas|census|dashboard) ;; + *) forward=(classify "${forward[@]}") ;; +esac flags=() [[ "$as_json" -eq 1 ]] && flags+=(--json) diff --git a/skills/COMMANDS.md b/skills/COMMANDS.md index d47ef06..1be0239 100644 --- a/skills/COMMANDS.md +++ b/skills/COMMANDS.md @@ -29,7 +29,7 @@ readiness gate, or execution — those belong to the organs. | Command | Agent | Chain | |---------|-------|-------| -| `/intake` | Intake Agent | `bin/pyauto-brain intake` → files a PyAutoMind prompt (**before** `start_dev`) | +| `/intake` | Intake Agent | `bin/pyauto-brain intake` → files a PyAutoMind prompt (**before** `start_dev`); `census`/`dashboard` = backlog inventory / `PyAutoMind/dashboard.md` | | `/feature` | Feature Agent | `bin/pyauto-brain feature` → `start_dev` → `ship_*` | | `/bug` | Bug Agent | `bin/pyauto-brain bug` → `start_dev` → `ship_*` (health mode → vitals + Heart issues) | | `/build` | Build Agent | `bin/pyauto-brain build` → vitals faculty → Heart → PyAutoBuild | diff --git a/skills/intake/intake.md b/skills/intake/intake.md index 082dfc8..a41ed41 100644 --- a/skills/intake/intake.md +++ b/skills/intake/intake.md @@ -24,6 +24,16 @@ Intake **files a prompt; it does not start development.** It is the step *before `/start_dev`. Once the prompt is written, `/start_dev ` routes it into the dev workflow (issue, branch, plan). Do not bypass the Brain. +## Backlog view (census / dashboard) + +- `bin/pyauto-brain intake census` — read-only inventory of every filed prompt + (counts by work-type/target/difficulty/priority + hygiene flags for headerless + legacy prompts). `--json` for the full records. +- `bin/pyauto-brain intake dashboard` — renders the census as the Mind + **backlog** page; dry-run prints it, `--apply` writes + `PyAutoMind/dashboard.md` (commit via `prompt_sync_push`). Backlog only — + organism *health* is `/health`, not this page. + ## Boundary - **`/route`** infers a work-type and *dispatches* (starts dev now); **`/intake`**