From 3f34c0767e8544a662a0f794a300ebe27d38eb03 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 12:39:48 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20intake=20formalise=20mode=20=E2=80=94?= =?UTF-8?q?=20retroactively=20header=20the=20backlog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit formalise (once codenamed repair; renamed because raw prompts are intended word-vomit awaiting conception, not defects) selects census records with missing header fields and inserts the derived lines in place: Type/Target from the taxonomy folder (authoritative), Difficulty/Autonomy/Priority via the same sizing-faculty path used at conception. Every existing line survives byte-verbatim — including CRLF files, which are read newline-preserving and written back with their own endings. Work-type disagreements become re-home suggestions; files are never moved or deleted. Optional path-prefix scopes the run; dry-run proposes, --apply writes; re-runs are no-ops. Closes #32. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016ttFH96vcXuCVZfSBZEytf --- agents/conductors/intake/AGENTS.md | 11 +- agents/conductors/intake/INTAKE_TAXONOMY.md | 12 ++ agents/conductors/intake/_intake.py | 132 ++++++++++++++++++++ agents/conductors/intake/intake.sh | 7 +- skills/intake/intake.md | 5 + 5 files changed, 163 insertions(+), 4 deletions(-) diff --git a/agents/conductors/intake/AGENTS.md b/agents/conductors/intake/AGENTS.md index 88937b3..dd09631 100644 --- a/agents/conductors/intake/AGENTS.md +++ b/agents/conductors/intake/AGENTS.md @@ -74,13 +74,18 @@ schema — light structure over free-form prose. | **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` | +| **formalise** | `intake formalise [prefix]` | retroactively header the prompts census flags — derive the missing fields, insert in place, prose untouched; `--apply` writes | 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. +than errors. Formalise closes those flags (once codenamed `repair`, renamed +because raw prompts are intended word-vomit awaiting conception, not defects): +it derives Difficulty/Autonomy/Priority via the sizing faculty, writes +Type/Target from the folder, keeps every existing field value and every line of +prose, and turns work-type disagreements into **re-home suggestions** — it never +moves or deletes a file. ## Run @@ -93,6 +98,8 @@ bin/pyauto-brain intake --apply ideas # write them 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 +bin/pyauto-brain intake formalise # propose retroactive headers (dry-run) +bin/pyauto-brain intake --apply formalise bug/ # write them, only under bug/ ``` **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 db7609f..e26b81b 100644 --- a/agents/conductors/intake/INTAKE_TAXONOMY.md +++ b/agents/conductors/intake/INTAKE_TAXONOMY.md @@ -109,6 +109,18 @@ itemised. *backlog* page (never health; that is Heart's). Census is always read-only; dashboard writes only under `--apply`. +**formalise** closes the hygiene flags retroactively (once codenamed `repair`; +renamed — raw prompts are intended word-vomit awaiting conception, not defects). +Per flagged prompt it derives the missing fields (Type/Target from the folder, +Difficulty/Autonomy/Priority via the same sizing-faculty path conception uses) +and inserts only those lines: after the last field of a partial header block, +below an existing `# heading`, or under a derived `# ` when there is +neither — every existing line survives verbatim, plus a retroactive-provenance +comment. Where the body classifier disagrees with the folder at medium/high +confidence it emits a *re-home suggestion*; it never moves or deletes a file. +Optional path-prefix argument scopes the run (`intake formalise bug/`); writes +only under `--apply` and re-runs are no-ops (nothing left missing). + ## 7. What intake does NOT own - The difficulty heuristic, prompt parsing, repo sets → the **sizing faculty**. diff --git a/agents/conductors/intake/_intake.py b/agents/conductors/intake/_intake.py index 28a456d..7a9af21 100755 --- a/agents/conductors/intake/_intake.py +++ b/agents/conductors/intake/_intake.py @@ -466,6 +466,127 @@ def _fmt(counts, top=None): "header (--json lists them)") +# --- formalise (retroactive conception) ---------------------------------------- +# The backlog's raw prompts are intended word-vomit — conception deferred, not +# defects (hence *formalise*, not "repair"). Formalise derives the missing header +# fields and inserts them without touching a single existing line of prose. +_FIELD_LINE = re.compile( + r"(Type|Target|Difficulty|Autonomy|Priority|Status):\s*\S") + + +def _derive_fields(text: str, work_type: str, target: str) -> dict: + """Derive a full header for a prompt body, folder identity authoritative. + + Type/Target come from the taxonomy folder; Difficulty/Autonomy/Priority run + the same sizing-faculty path `analyse` uses at conception time. + """ + repos = _repos_in(text) + tgt = normalise_repo(target) if target != "-" else "?" + if tgt in KNOWN_REPOS and tgt not in repos: + repos = sorted(set(repos) | {tgt}) + p = {"text": text, "repos": repos, "words": len(text.split()), + "target": target, "work_type": work_type} + level, _score, factors = estimate_difficulty(p) + return { + "type": work_type, + "target": REPO_DISPLAY.get(tgt, target if target != "-" else "?"), + "difficulty": level, + "autonomy": infer_autonomy(level, factors), + "priority": infer_priority(text), + "status": "formalised", + } + + +def _insert_fields(text: str, add: dict, has_header: bool, title: str) -> str: + """Insert the missing `Field: value` lines, preserving every existing line. + + Partial header -> append after the last recognised field line in the leading + block (non-field lines like `Repos:` / `Milestone:` stay put). No header but + a leading `# heading` -> insert below it. Neither -> prepend a derived + `# <title>` so the file lands on the blessed shape. + """ + lines = text.splitlines() + field_lines = [f"{f.capitalize()}: {add[f]}" for f in HEADER_FIELDS if f in add] + if has_header: + last = max(i for i, ln in enumerate(lines[:30]) + if _FIELD_LINE.match(ln.strip())) + lines[last + 1:last + 1] = field_lines + else: + first = next((i for i, ln in enumerate(lines) if ln.strip()), 0) + if lines and lines[first].lstrip().startswith("#"): + lines[first + 1:first + 1] = [""] + field_lines + else: + lines[:0] = [f"# {title}", ""] + field_lines + [""] + # Preserve the file's own line endings — "verbatim" includes bytes, and a + # CRLF prompt must not come back LF-normalised with every line rewritten. + nl = "\r\n" if "\r\n" in text else "\n" + return nl.join(lines) + (nl if text.endswith("\n") else "") + + +def formalise(mind: Path, prefix: str = "", apply: bool = False) -> dict: + """Retroactively formalise headerless / incomplete backlog prompts in place. + + Reuses the census to select records with missing fields; writes ONLY under + --apply. Never moves or deletes a file — a work-type disagreement between + the body classifier and the taxonomy folder becomes a re-home *suggestion* + for a human, because the folder is authoritative. + """ + c = census(mind) + proposals, suggestions = [], [] + for r in c["records"]: + if prefix and not r["path"].startswith(prefix): + continue + if not r["missing"]: + continue + path = mind / r["path"] + # newline="" keeps \r\n intact — read_text's universal-newline mode + # would silently translate it and defeat the verbatim write-back. + with path.open(encoding="utf-8", errors="replace", newline="") as fh: + text = fh.read() + derived = _derive_fields(text, r["work_type"], r["target"]) + add = {f: derived[f] for f in r["missing"]} + proposals.append({"path": r["path"], "title": r["title"], + "add": add, "keep": r["header"]}) + if apply: + new = _insert_fields(text, add, bool(r["header"]), r["title"]) + stamp = (f"<!-- formalised retroactively by the Intake (Conception) " + f"Agent on {_dt.date.today().isoformat()} -->") + if "formalised retroactively" not in new: + nl = "\r\n" if "\r\n" in new else "\n" + new = new.rstrip("\r\n") + nl + nl + stamp + nl + path.write_text(new, encoding="utf-8", newline="") + wt_guess, conf, _hits_ = classify_work_type(text) + if (conf != "low" and wt_guess != r["work_type"] + and r["work_type"] != "triage"): + suggestions.append(f"{r['path']} — classifier reads as {wt_guess} " + f"({conf}); filed under {r['work_type']}/") + return { + "generated": _dt.date.today().isoformat(), + "scanned": c["total"], + "formalised": len(proposals), + "applied": bool(apply), + "proposals": proposals, + "rehome_suggestions": suggestions, + } + + +def emit_formalise(res: dict): + verb = "formalised" if res["applied"] else "to formalise" + print(f"== Intake formalise: {res['formalised']} prompt(s) {verb} " + f"(of {res['scanned']} scanned) ==") + for p in res["proposals"]: + adds = " · ".join(f"{f.capitalize()}: {v}" for f, v in p["add"].items()) + print(f" {p['path']}") + print(f" + {adds}") + if res["rehome_suggestions"]: + print(f"Re-home suggestions ({len(res['rehome_suggestions'])}) — " + "folder stays authoritative; move by hand if the classifier is right:") + for s in res["rehome_suggestions"]: + print(f" - {s}") + if not res["applied"]: + print("\n(dry-run — re-run `intake --apply formalise` to write the headers)") + + # --- ideas.md scanning -------------------------------------------------------- def scan_ideas(mind: Path): """Yield (bullet_text, context_header) for substantive ideas.md lines.""" @@ -552,9 +673,20 @@ def main(argv=None): sub.add_parser("dashboard", help="render the census as the Mind backlog page; " "--apply writes dashboard.md") + fm = sub.add_parser("formalise", help="retroactively header the backlog " + "prompts census flags; --apply writes") + fm.add_argument("prefix", nargs="?", default="", + help="only formalise prompts under this path prefix " + "(e.g. bug/)") + a = ap.parse_args(argv) mind = Path(a.mind) + if a.cmd == "formalise": + res = formalise(mind, prefix=a.prefix, apply=a.apply) + print(json.dumps(res, indent=2)) if a.as_json else emit_formalise(res) + return 0 + if a.cmd == "census": c = census(mind) print(json.dumps(c, indent=2)) if a.as_json else emit_census(c) diff --git a/agents/conductors/intake/intake.sh b/agents/conductors/intake/intake.sh index d682ae6..11ac09d 100755 --- a/agents/conductors/intake/intake.sh +++ b/agents/conductors/intake/intake.sh @@ -21,6 +21,9 @@ # intake census inventory all filed prompts (always read-only) # intake dashboard render the census as the Mind backlog page; # --apply writes PyAutoMind/dashboard.md +# intake formalise [prefix] retroactively header the backlog prompts the +# census flags (word-vomit is intent, not defect); +# --apply writes the headers in place # # Flags (place before the subcommand; both default OFF): # --apply write the formal prompt file(s) / dashboard.md; else dry-run only @@ -42,7 +45,7 @@ apply=0 forward=() while [[ $# -gt 0 ]]; do case "$1" in - -h|--help) sed -n '2,33p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; + -h|--help) sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; --json) as_json=1; shift ;; --apply) apply=1; shift ;; *) forward+=("$1"); shift ;; @@ -58,7 +61,7 @@ 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. case "${forward[0]}" in - classify|ideas|census|dashboard) ;; + classify|ideas|census|dashboard|formalise) ;; *) forward=(classify "${forward[@]}") ;; esac diff --git a/skills/intake/intake.md b/skills/intake/intake.md index a41ed41..9c6b981 100644 --- a/skills/intake/intake.md +++ b/skills/intake/intake.md @@ -33,6 +33,11 @@ dev workflow (issue, branch, plan). Do not bypass the Brain. **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. +- `bin/pyauto-brain intake formalise [prefix]` — retroactively headers the + prompts census flags (word-vomit is intent, not defect): derives the missing + fields, inserts them in place with all prose verbatim, reports re-home + suggestions instead of ever moving files. Dry-run proposes; `--apply` writes + (then regenerate the dashboard). ## Boundary