From 4de362f9e528096cd26c8fad92270d3ba09107b4 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 15:03:30 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20intake=20reconcile=20mode=20=E2=80=94?= =?UTF-8?q?=20flag=20shipped-but-stale=20backlog=20prompts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A prompt's Status: header is not a completeness signal (formalise preserves an existing Status verbatim, so shipped work can still read Status: planned — the PyAutoHeart M0-M5 cluster sat exactly like that). reconcile makes the manual audit repeatable: per backlog prompt it collects four Mind-local signals — complete.md path references (follow-up wording downgrades to likely-open), duplicate basenames in issued/, token overlap with completed task headers, and hand-set Status values — then ranks suspects high/medium/low with the evidence shown. Always read-only, even under --apply: the final verification (target repo git log / merged PRs) and the retirement to issued/ stay human. Closes #52. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016ttFH96vcXuCVZfSBZEytf --- agents/conductors/intake/AGENTS.md | 10 ++ agents/conductors/intake/INTAKE_TAXONOMY.md | 11 ++ agents/conductors/intake/_intake.py | 131 ++++++++++++++++++++ agents/conductors/intake/intake.sh | 7 +- skills/intake/intake.md | 5 + 5 files changed, 162 insertions(+), 2 deletions(-) diff --git a/agents/conductors/intake/AGENTS.md b/agents/conductors/intake/AGENTS.md index df839db..483f8f1 100644 --- a/agents/conductors/intake/AGENTS.md +++ b/agents/conductors/intake/AGENTS.md @@ -75,6 +75,7 @@ schema — light structure over free-form prose. | **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 | +| **reconcile** | `intake reconcile [prefix]` | rank backlog prompts that look already-shipped (vs `complete.md` / `issued/`); always read-only — retiring stays human | Census/dashboard are the Mind *backlog* view — deliberately distinct from Heart's `/health status` health view (see "must never do"). The prompt-taxonomy @@ -87,6 +88,14 @@ 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. +Reconcile exists because a prompt's `Status:` header is **not** a completeness +signal — formalise preserves an existing Status verbatim, so shipped work can +still read `Status: planned`. It cross-references each backlog prompt against +`complete.md` (path references + `## header` topic overlap), `issued/` +basenames, and hand-set Status values, then ranks suspects (high/medium/low) +with the evidence shown. The final verification — the target repo's git log / +merged PRs — and the retirement itself stay human. + ## Machine sources (one staging surface) Conception input increasingly arrives from the organism itself — research @@ -120,6 +129,7 @@ bin/pyauto-brain intake dashboard # backlog pag 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/ +bin/pyauto-brain intake reconcile # rank shipped-but-stale suspects (read-only) ``` **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 e26b81b..885971c 100644 --- a/agents/conductors/intake/INTAKE_TAXONOMY.md +++ b/agents/conductors/intake/INTAKE_TAXONOMY.md @@ -121,6 +121,17 @@ 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). +**reconcile** audits the opposite failure: prompts whose work shipped but whose +status went stale (a header `Status:` is display metadata — formalise preserves +an existing value verbatim, so `Status: planned` can outlive the work by +months). Per backlog prompt it collects four Mind-local signals — a +`complete.md` line referencing its path (follow-up/restore/parked wording +downgrades it to likely-open), a duplicate basename in `issued/`, token overlap +with a completed task's `## header`, and a hand-set Status — then ranks +suspects high/medium/low with the evidence shown. Always read-only: the final +verification (target repo git log / merged PRs) and the retirement to +`issued/` stay human. + ## 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 7a9af21..fbf845c 100755 --- a/agents/conductors/intake/_intake.py +++ b/agents/conductors/intake/_intake.py @@ -587,6 +587,124 @@ def emit_formalise(res: dict): print("\n(dry-run — re-run `intake --apply formalise` to write the headers)") +# --- reconcile (shipped-but-stale audit) ---------------------------------------- +# A prompt's Status: header is NOT a completeness signal — formalise preserves an +# existing Status verbatim, so shipped work can still read "Status: planned" (the +# PyAutoHeart M0-M5 cluster sat exactly like that). Reconcile cross-references +# the backlog against the Mind's shipped-state records and RANKS suspects for a +# human to retire. Read-only, always: retiring a prompt (moving it to issued/) +# stays a human act, and the final verification — the target repo's git log / +# merged PRs — stays out of scope by design. +_STOPWORDS = frozenset( + "the a an of to in for and or is are be with on by via from into as at it " + "this that use using make add new fix update support get set can we i you " + "our my need should will when once each all its".split()) +# Wording in a complete.md reference line that marks the prompt as a deferred +# follow-up (still open) rather than the shipped task itself. +_FOLLOWUP_WORDS = ("follow", "restore", "parked", "remain", "blocked", "later", + "next step", "next-step", "deferred") + + +def _tokens(s: str) -> set: + return {w for w in re.findall(r"[a-z0-9]+", s.lower()) + if len(w) > 2 and w not in _STOPWORDS} + + +def reconcile(mind: Path, prefix: str = "") -> dict: + """Rank backlog prompts that look already-shipped, for a human to retire. + + Four Mind-local signals per prompt: a complete.md line referencing its path + (follow-up wording downgrades it), a duplicate basename in issued/, token + overlap with a completed task's `## header`, and a hand-set Status the + formalise pass deliberately preserved. Never writes anything. + """ + c = census(mind) + comp = mind / "complete.md" + comp_lines = (comp.read_text(encoding="utf-8", errors="replace").splitlines() + if comp.is_file() else []) + headers = [(ln[3:].strip(), _tokens(ln[3:].replace("-", " "))) + for ln in comp_lines if ln.startswith("## ")] + issued = mind / "issued" + issued_names = ({p.name for p in issued.glob("*.md")} + if issued.is_dir() else set()) + + suspects = [] + for r in c["records"]: + if prefix and not r["path"].startswith(prefix): + continue + path = r["path"] + base = path.rsplit("/", 1)[-1] + sans_wt = path.split("/", 1)[1] if "/" in path else path + findings = [] + score = 0.0 + + for ln in comp_lines: + if base in ln or sans_wt in ln: + kind = ("referenced-followup" + if any(w in ln.lower() for w in _FOLLOWUP_WORDS) + else "referenced") + findings.append((kind, ln.strip())) + + if base in issued_names: + findings.append(("issued-duplicate", f"issued/{base} already exists")) + + sig = _tokens(base.replace("_", " ")) | _tokens(r["title"]) + best = (0.0, "", set()) + for h, ht in headers: + if not sig or not ht: + continue + shared = sig & ht + j = len(shared) / len(sig | ht) + if (j, len(shared)) > (best[0], len(best[2])): + best = (j, h, shared) + if best[0] >= 0.40 or len(best[2]) >= 3: + score = best[0] + findings.append(("topic-overlap", + f"complete.md '## {best[1]}' " + f"(shared: {', '.join(sorted(best[2]))})")) + + if r["status"] not in ("-", "formalised"): + findings.append(("stale-status", + f"Status: {r['status']} — hand-set; verify against " + "shipped state")) + + if findings: + kinds = {k for k, _ in findings} + if "issued-duplicate" in kinds or "referenced" in kinds: + conf = "high" + elif "topic-overlap" in kinds: + conf = "medium" + else: + conf = "low" # follow-up reference / stale status only + suspects.append({ + "path": path, "title": r["title"], "confidence": conf, + "overlap_score": round(score, 2), + "findings": [{"kind": k, "evidence": e} for k, e in findings], + }) + + order = {"high": 0, "medium": 1, "low": 2} + suspects.sort(key=lambda s: (order[s["confidence"]], + -s["overlap_score"], s["path"])) + return {"generated": _dt.date.today().isoformat(), "scanned": c["total"], + "suspects": suspects} + + +def emit_reconcile(res: dict): + print(f"== Intake reconcile: {len(res['suspects'])} suspect(s) of " + f"{res['scanned']} scanned ==") + if not res["suspects"]: + print(" backlog reconciles clean against complete.md / issued/.") + for s in res["suspects"]: + print(f"[{s['confidence']:>6}] {s['path']}") + for f in s["findings"]: + ev = f["evidence"] + if len(ev) > 160: + ev = ev[:157] + "…" + print(f" {f['kind']}: {ev}") + print("\nRetiring a prompt stays human: verify against the target repo's " + "git log / merged\nPRs, then move it to issued/ by hand.") + + # --- ideas.md scanning -------------------------------------------------------- def scan_ideas(mind: Path): """Yield (bullet_text, context_header) for substantive ideas.md lines.""" @@ -679,6 +797,11 @@ def main(argv=None): help="only formalise prompts under this path prefix " "(e.g. bug/)") + rc = sub.add_parser("reconcile", help="rank backlog prompts that look " + "already-shipped (always read-only)") + rc.add_argument("prefix", nargs="?", default="", + help="only reconcile prompts under this path prefix") + a = ap.parse_args(argv) mind = Path(a.mind) @@ -687,6 +810,14 @@ def main(argv=None): print(json.dumps(res, indent=2)) if a.as_json else emit_formalise(res) return 0 + if a.cmd == "reconcile": + if a.apply: + print("intake reconcile is read-only — retiring prompts stays " + "human (--apply ignored).", file=sys.stderr) + res = reconcile(mind, prefix=a.prefix) + print(json.dumps(res, indent=2)) if a.as_json else emit_reconcile(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 11ac09d..6f4fd42 100755 --- a/agents/conductors/intake/intake.sh +++ b/agents/conductors/intake/intake.sh @@ -24,6 +24,9 @@ # intake formalise [prefix] retroactively header the backlog prompts the # census flags (word-vomit is intent, not defect); # --apply writes the headers in place +# intake reconcile [prefix] rank backlog prompts that look already-shipped +# (vs complete.md / issued/); always read-only — +# retiring a prompt stays human # # Flags (place before the subcommand; both default OFF): # --apply write the formal prompt file(s) / dashboard.md; else dry-run only @@ -45,7 +48,7 @@ apply=0 forward=() while [[ $# -gt 0 ]]; do case "$1" in - -h|--help) sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; + -h|--help) sed -n '2,39p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; --json) as_json=1; shift ;; --apply) apply=1; shift ;; *) forward+=("$1"); shift ;; @@ -61,7 +64,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|formalise) ;; + classify|ideas|census|dashboard|formalise|reconcile) ;; *) forward=(classify "${forward[@]}") ;; esac diff --git a/skills/intake/intake.md b/skills/intake/intake.md index 330e970..7844d21 100644 --- a/skills/intake/intake.md +++ b/skills/intake/intake.md @@ -38,6 +38,11 @@ dev workflow (issue, branch, plan). Do not bypass the Brain. 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). +- `bin/pyauto-brain intake reconcile [prefix]` — ranks backlog prompts that + look already-shipped (cross-referenced against `complete.md` and `issued/`; + a stale hand-set `Status:` is a signal, never proof). Always read-only: + verify each suspect against the target repo's git log / merged PRs, then + retire it to `issued/` by hand. ## Boundary