Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions agents/conductors/intake/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,15 @@ schema — light structure over free-form prose.
|------|---------|--------------|
| **classify** | `intake "<text>"` / `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

Expand All @@ -85,6 +90,9 @@ bin/pyauto-brain intake --json "<text>" # 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
Expand Down
17 changes: 16 additions & 1 deletion agents/conductors/intake/INTAKE_TAXONOMY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
166 changes: 166 additions & 0 deletions agents/conductors/intake/_intake.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<work-type>/**/*.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,
Comment on lines +372 to +374
# 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)")
Comment on lines +386 to +387

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"<!-- generated by `pyauto-brain intake dashboard --apply` on "
f"{c['generated']} — regenerate, do not hand-edit -->",
"",
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.", "",
"<details>", "<summary>Headerless prompts</summary>", ""]
L += [f"- `{h.split(' — ')[0]}`" for h in c["hygiene"]]
L += ["", "</details>"]
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."""
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 10 additions & 6 deletions agents/conductors/intake/intake.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
# intake "<raw text>" 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.
Expand All @@ -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 ;;
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion skills/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
10 changes: 10 additions & 0 deletions skills/intake/intake.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>` 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`**
Expand Down