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
11 changes: 9 additions & 2 deletions agents/conductors/intake/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions agents/conductors/intake/INTAKE_TAXONOMY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `# <title>` 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**.
Expand Down
132 changes: 132 additions & 0 deletions agents/conductors/intake/_intake.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +515 to +517
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="")
Comment on lines +554 to +557
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),
Comment on lines +564 to +566
"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."""
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions agents/conductors/intake/intake.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ;;
Expand All @@ -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

Expand Down
5 changes: 5 additions & 0 deletions skills/intake/intake.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down