diff --git a/skills/COMMANDS.md b/skills/COMMANDS.md index af67b82..336977d 100644 --- a/skills/COMMANDS.md +++ b/skills/COMMANDS.md @@ -85,6 +85,28 @@ call, so the Brain is not bypassed): local-only steps. Interactive/terminal only (the automated morning webhooks are separate). +**5. Maintenance doors** — periodic sweeps that reason about accumulated debris +and then execute their own cleanup after per-bucket human confirmation. They own +no agent, but unlike the composition doors they *mutate*, so each is +audit-first and confirmation-gated: + +- **`/repo_cleanup`** — git debris across the canonical checkouts and worktrees: + stale feature branches, `[gone]` tracking refs, stashes, dirty checkouts, + orphan worktrees. Local-dev only (needs the checkouts). The Hygiene Agent's + `tidy` mode is its front door; this skill is the executor. +- **`/issue_cleanup`** — the GitHub **issue trackers**: audit every open issue + against the PyAutoMind completion records and merged PRs, bucket them + (shipped / weak evidence / deliberately open / in flight / external / + unreconciled), close only what a human confirms. Runs anywhere `gh` is + authenticated, including mobile/Codex. Closing needs **two independent + evidence legs**, and the record header *key* decides meaning — `issue:` + completes, `followup-issue:` / `parent-issue:` / `plan:` mean the record + *spawned* a still-open issue. `/wake_up` runs its audit half read-only. + +These two are complements, not overlaps: `/repo_cleanup` never touches issues, +`/issue_cleanup` never touches git. Neither handles **external** users' issues — +that is `/community`, whose drafts stay human-approved. + Codex skills also expose the remaining public CLI agents directly: the `clone` conductor, the `release` conductor, and the read-only `vitals`, `review`, `memory`, and `samplers` faculties. They do not gain new diff --git a/skills/issue_cleanup/SKILL.md b/skills/issue_cleanup/SKILL.md new file mode 100644 index 0000000..5051c90 --- /dev/null +++ b/skills/issue_cleanup/SKILL.md @@ -0,0 +1,28 @@ +--- +name: issue-cleanup +description: Reconcile the PyAuto GitHub issue trackers — audit every open issue against the PyAutoMind completion records and merged PRs, bucket them (shipped / weak evidence / deliberately open / in flight / external / unreconciled), then close only what a human confirms. Use for tracker drift, stale issue backlogs, or "are any of these already done?". +--- + +# Issue Cleanup + +Thin discovery wrapper. The canonical command body is +[`issue_cleanup.md`](issue_cleanup.md) in this directory — follow it exactly; +the long-form detail (header taxonomy, evidence rules, dashboard layout, +per-bucket execution, regression bar) is in [`reference.md`](reference.md). + +The issue-tracker counterpart to `$repo-cleanup`'s git-debris sweep: audit → +bucketed dashboard → per-bucket human confirmation → execute → recap. + +**Two things to know before running it:** + +- **The record header *key* decides everything.** `issue:` means a PyAutoMind + record completed that issue; `followup-issue:`, `library-followup-issue:`, + `parent-issue:`, `upstream-issues-filed:` and `plan:` mean the record + *spawned* it and it is legitimately open. Treat the completing keys as an + allowlist — a loose `*issue*:` match closes live follow-ups. +- **Closing requires two independent evidence legs** (record header + merged + PR) and always a human confirmation. The audit half is read-only and safe to + run unattended; `$wake-up` does exactly that and reports counts only. + +Do not duplicate or reinterpret the workflow here — if it changes, edit +`issue_cleanup.md`. diff --git a/skills/issue_cleanup/issue_cleanup.md b/skills/issue_cleanup/issue_cleanup.md new file mode 100644 index 0000000..83499c6 --- /dev/null +++ b/skills/issue_cleanup/issue_cleanup.md @@ -0,0 +1,146 @@ +# Issue Cleanup — reconcile the GitHub issue trackers + +Periodic reconciliation sweep across every PyAuto repo's **GitHub issue +tracker**. Finds issues whose work has demonstrably shipped but which were never +closed, separates them from issues that are deliberately open, still in flight, +or owned by the community, and closes only what a human confirms. + +Run it as `$issue-cleanup` (`/issue_cleanup` in Claude) — it needs only an +authenticated `gh` and a readable `PyAutoMind/`, so unlike `$repo-cleanup` it +works on **any harness and any environment**, including mobile Claude Code chat +and Codex, with no local library checkout. + +A **PyAutoBrain dev-workflow** hygiene skill — the issue-tracker counterpart to +`$repo-cleanup`'s git-debris sweep. Like that skill it **reasons** about what is +safe to close and then runs its own `gh` mechanics: it reads the PyAutoMind +completion records to know what shipped, cross-checks GitHub for merged PRs, and +executes the closes. Cleanup is not release work, so it never touches +PyAutoHands. Organ boundary + execution-environment model: +[`../WORKFLOW.md`](../WORKFLOW.md). + +**Distinct from:** `$repo-cleanup` (branches, refs, stashes, worktrees — git +debris, never issues); `$community` (**external** users' issues awaiting *our +reply* — this skill never touches those, it routes them there); `$create-issue` +and `$update-issue` (single-issue primitives); `$hygiene` (code-quality debt, not +the tracker). + +## Why this needs care + +The obvious rule — *"a PyAutoMind `complete/` record references this issue, so +close it"* — is **wrong five different ways**. Each was found by verification, +not inspection: four during the 2026-07-28 sweep that took the trackers from 82 +open to 47, and the fifth when this skill's own regression bar was first run +against that result. Encode all five or the skill will close live work: + +1. **Body mentions are not claims.** Grepping record *files* for an issue URL + over-matches. `complete/2026/05/many-vis-prep-dft.md` discusses + `PyAutoArray#326` in its prose while its own `- issue:` header reads + `(CI-triage cluster G, no GitHub issue)`. **Only the header line is + evidence.** + +2. **The header *key* carries the meaning.** This is the load-bearing rule. + `issue:` means *this record completes that issue*. The spawn-style keys mean + the **opposite** — the record *created* that issue and it is legitimately + open. Full taxonomy in [`reference.md`](reference.md); matching loosely on + `*issue*:` closes live follow-ups. + +3. **Inline annotations override everything.** Records mark deliberate + exceptions in the text after the URL: `(open — findings census stays as + reference)`, `(STAYS OPEN — real finding + resumable fit)`. Any `open` token + means hands off — and these appear on `plan:` lines too, not just `issue:`. + +4. **A record in `complete/` does not mean the *work* completed.** + `ep-hierarchical-scale-collapse.md` carries `Status: issued` — it *filed* + `PyAutoFit#1405` and reported two defects on it. The record is complete; the + issue is a live bug. **Read the status field, not the directory.** + +5. **A phase-scoped claim does not complete an umbrella issue.** Four separate + records each claim a *Phase 5 item* of `PyAutoBrain#130` — `(Phase 5 item 4)`, + `(Phase 5 F5 item)`, and so on. Each finished its piece; none establishes the + issue is done. Scoped claims go to bucket B for a human, never to A. + +And a corroborating sixth: `PyAutoReduce#8`'s record already said `(CLOSED)` +while GitHub still had it open. Closes silently fail to land, which is part of +why reconciling is worth doing at all. + +## Safety principles (non-negotiable) + +1. **Audit first, act second.** The audit is read-only and safe to run + unattended. Nothing closes without an explicit per-bucket confirmation. +2. **Two independent evidence legs** before an issue is even *proposed* for + closing: a completing header key in a record, **and** a merged PR. +3. **Never close an external user's issue.** Author not in the maintainer set → + bucket E, routed to `$community`, never closed here. +4. **Never close anything claimed** in `PyAutoMind/active.md` / `parked.md`. +5. **Age is not evidence.** A 2700-day issue may still be valid; probe whether + the API or infrastructure it names still exists (see "Obsolescence" below). +6. **Every close leaves a comment naming its evidence** (record path + PR), so + the decision is auditable and reversible. + +## Scope + +**Swept:** every repo in `PyAutoMind/repos.yaml` with an issue tracker — the +organs, the libraries, the workspaces incl. `_test`/`_developer` variants, the +HowTo repos and the assistants. + +**Never touched:** pull requests (the issues endpoint returns them too — filter +`select(.pull_request == null)`); issues in bucket C, D or E. + +## Steps + +### 1. Setup + +Confirm `gh auth status`; if unauthenticated, stop. Resolve repo owners from +`PyAutoMind/repos.yaml` — do not hard-code legacy owner defaults. + +### 2. Audit (read-only) + +Collect every open issue, then build the evidence sets from the PyAutoMind +records. Commands, the header taxonomy, and the annotation rule: +[`reference.md`](reference.md) → "Collect open issues", "Parse the records", +"Evidence legs". + +### 3. Dashboard + +Present the audit grouped into fixed buckets A–F (omit empty buckets; always +print the Summary counts). Layout: [`reference.md`](reference.md) → "Dashboard +layout". + +### 4. Per-bucket confirmation + execution + +Work the buckets in the fixed order, printing what will be closed and getting +approval before each destructive step. Recipes: [`reference.md`](reference.md) +→ "Per-bucket execution". + +### 5. Recap + +Print what closed, what was held back and why, so the next sweep resumes from +there. Format: [`reference.md`](reference.md) → "Recap". + +## Obsolescence needs a real check, not an age threshold + +The eight oldest issues in the first sweep split on **evidence**, not age: + +- The six `PyAutoCTI` ones (2693–2785 days) named `FrameGeometry`, `CIFrame`, + `CIData`, `ci_data_analysis`, `ci_pattern`, and the `phase.py` / `pipeline` + module. A grep of `autocti/` returned **zero** for every one — the CTI + resurrection removed that surface entirely — so they closed `not_planned`. +- `PyAutoHands#16` (test against pre-release deps) and `#17` (fail the build if + the RTD docs build fails) are the same vintage (~1337 days) but were **left + open**: no `--pre` workflow and no RTD gating exist in + `PyAutoHands/.github/workflows/`, so they are still-valid unimplemented asks. + +Age correlates with obsolescence; it never establishes it. Probe whether the +named API or infrastructure still exists before proposing a close. + +## Notes + +- `gh issue close` is broken in this environment — comment, then + `gh api -X PATCH … -f state=closed`. Full recipe in [`reference.md`](reference.md). +- Bot-authored self-refreshing issues (`[url-check]`, `[heart-health]`) are + recognised and excluded from staleness ranking — they are a live signal, not + debris. +- `$wake-up` runs the **audit half only** and reports counts in its digest; + every close still comes back through this skill's confirmation. +- If a record's claim and GitHub disagree, trust GitHub for *state* and the + record for *intent* — then reconcile, and say which one was stale. diff --git a/skills/issue_cleanup/reference.md b/skills/issue_cleanup/reference.md new file mode 100644 index 0000000..51f6bfe --- /dev/null +++ b/skills/issue_cleanup/reference.md @@ -0,0 +1,259 @@ +# issue_cleanup — reference detail + +Factored out of `issue_cleanup.md`. The body is authoritative for the flow; this +holds the collection commands, the record header taxonomy, the evidence rules, +the dashboard layout, the per-bucket execution recipes, the recap, and the +execution-environment fallback. + +## Collect open issues + +Resolve the repo list and owners from `PyAutoMind/repos.yaml`. For each repo: + +```bash +gh api "repos///issues?state=open&per_page=100" --paginate \ + --jq '[.[] | select(.pull_request == null) | {num:.number, title:.title, + author:.user.login, created:.created_at, updated:.updated_at, + labels:[.labels[].name], comments:.comments}]' +``` + +`select(.pull_request == null)` is **required** — the issues endpoint returns +pull requests too, and counting them inflates every total. + +Note: this `gh` build rejects several documented `--json` fields on +`gh issue list` / `gh run view` (`displayTitle`, `jobs`, …). Use `gh api` for +anything structured; it is the reliable path here. + +## Parse the records + +Evidence comes from `PyAutoMind/complete/**/*.md`. Two rules govern it. + +### Rule 1 — only *known* header lines count + +Match `^\s*-?\s*:\s*` and read the issue URL out of +``. A URL appearing anywhere else in the file is **prose**, not a +claim: `complete/2026/05/many-vis-prep-dft.md` discusses `PyAutoArray#326` in +its notes while its own `- issue:` header says `(CI-triage cluster G, no GitHub +issue)`. + +**`` must be in the known set below — an arbitrary `- :` is not a +header.** Records use `- notes:` for long prose paragraphs that routinely cite +issue URLs, so a bare "any `- key:` line" regex re-admits exactly the prose +Rule 1 exists to exclude. `PyAutoGalaxy#417` is the worked example: it appears +only inside another record's `- notes:` field, so nothing claims it and it +belongs in the unreconciled backlog. + +### Rule 2 — the header key decides the meaning + +| Key | Uses¹ | Meaning | Closable? | +|-----|------:|---------|-----------| +| `issue:` | 630 | This record **completes** that issue | **yes** | +| `issues:` | 6 | Same, several issues on one line | **yes** | +| `followup-issue:` | 2 | Record **spawned** it; still open | **no** | +| `follow-up-issue:` | 1 | Record **spawned** it; still open | **no** | +| `library-followup-issue:` | 1 | Record **spawned** it; still open | **no** | +| `parent-issue:` | 1 | Umbrella this record is one part of | **no** | +| `upstream-issues-filed:` | 1 | Filed upstream, not ours to close | **no** | +| `issued:` | 1 | Filed only — see Rule 3 | **no** | +| `plan:` | — | Implementation backlog, deliberately open | **no** | + +¹ counts from the 2026-07-28 census of 800 records; re-derive rather than trust +these, but the **ratio** is the point — the completing key dominates, so a loose +`*issue*:` match looks right on a spot-check and silently closes live +follow-ups. + +Treat the completing set as an **allowlist** (`issue:`, `issues:`), never the +spawn set as a denylist — a new spawn-style key added later must fail closed. + +The table above is also the **known-key set** for Rule 1: a `- :` line +whose key is not in it (`notes:`, `summary:`, `repos:`, …) is prose, and its +URLs are not claims of any kind. + +### Rule 3 — annotations and status override + +- Any `open` token (case-insensitive) in the text **after** the URL means hands + off: `(open)`, `(open — findings census stays as reference)`, `(STAYS OPEN — + real finding + resumable fit)`, `(both stay OPEN for parked design work + only)`. Scan `plan:` lines for these too. +- A record carrying `Status: issued` **filed** its issue rather than completing + it (`ep-hierarchical-scale-collapse.md` → `PyAutoFit#1405`, a live bug). + Read the status field; `complete/` is the directory, not the verdict. + +### Rule 4 — a phase-scoped claim does not complete an umbrella + +If the annotation scopes the claim to part of the work — `(Phase 5 F5 item)`, +`(Phase 5 item 4)`, `(WP 2)`, `(stage b)` — the record completed *that piece*, +not the issue. Several records may each claim a different phase of the same +umbrella without any of them finishing it. + +Route these to **bucket B** (weak evidence), never A. `PyAutoBrain#130` is the +worked example: four separate records claim Phase-5 items of it, and no +combination of them establishes that Phase 5 as a whole is done. Deciding that +needs a human reading the issue, which is exactly what bucket B is for. + +## Evidence legs + +An issue reaches bucket A only with **both**: + +1. **Record leg** — a completing header key claims it, with no `open` + annotation and no `Status: issued`. +2. **PR leg** — a merged PR. Check **both** paths; the timeline alone + under-reports (12 of 29 in the first sweep had only the second): + +```bash +# path 1 — GitHub's own cross-reference +gh api "repos///issues//timeline?per_page=100" \ + --jq '[.[] | select(.event=="cross-referenced" and .source.issue.pull_request != null) + | {pr:.source.issue.number, merged:(.source.issue.pull_request.merged_at != null)}]' + +# path 2 — a PR number named in the record body, verified merged +gh api "repos///pulls/" --jq 'if .merged_at then "MERGED" else .state end' +``` + +## Buckets + +| Bucket | Definition | Action | +|---|---|---| +| **A** shipped | both evidence legs pass | closable, on confirmation | +| **B** weak evidence | a record exists but a leg fails | report only | +| **C** deliberately open | annotation or `Status: issued` says so | never touch | +| **D** in flight | claimed in `active.md` / `parked.md` | never touch | +| **E** external | author not in the maintainer set | route to `$community` | +| **F** unreconciled | no record at all | the real backlog; sub-split by age | + +The maintainer set comes from `repos.yaml` ownership; everyone else is external. +Bot authors (`github-actions[bot]`) are their own sub-group: self-refreshing +issues like `[url-check]` and `[heart-health]` re-open themselves by design and +must be excluded from staleness ranking. + +## Dashboard layout + +``` +Issue Cleanup — Audit +===================== +Summary + 18 repos scanned · 82 open issues + A 29 closable · B 7 weak · C 5 held · D 6 in flight · E 8 external · F 27 unreconciled + +Bucket A — Shipped, closable (record header + merged PR) +Bucket B — Record exists, evidence incomplete (report only) +Bucket C — Deliberately open (annotated in the record) +Bucket D — In flight (active.md / parked.md) +Bucket E — External, awaiting our reply → $community +Bucket F — Unreconciled backlog (no record) — split: ancient / live / bot +``` + +One section per bucket, issues grouped by repo, each line carrying the evidence +that classified it (record path, PR number, or the annotation text). Omit empty +buckets; always print the Summary counts even if zero. + +## Per-bucket execution (fixed order) + +Print the exact list and get approval before each destructive step. + +1. **Bucket A** — batch confirm, then per issue: comment the evidence, then + close as completed. + + ```bash + gh api repos///issues//comments -f body="$(cat <<'EOF' + Closing as complete — swept by an issue-tracker reconciliation pass. + + This work shipped and is recorded in PyAutoMind: + - `PyAutoMind/complete///.md` + + Verified via two independent legs: the record claims this issue in its + `- issue:` header, and its pull request is confirmed merged. Reopen if + anything here is still outstanding. + EOF + )" + gh api -X PATCH repos///issues/ \ + -f state=closed -f state_reason=completed + ``` + + `gh issue close` is broken in this environment — it must be the comment + + `PATCH` pair. `--jq .state` on the PATCH gives a one-word confirmation per + issue; a run that prints anything other than `closed` needs investigating, + not retrying. + +2. **Bucket F, ancient sub-group** — never batch. Per issue: read the body, + grep the current source for the API/module/infrastructure it names, and + decide. + - Named surface is **gone** → close with `state_reason=not_planned` and a + comment saying what was greped and that it returned zero. + - Named surface is **absent but still wanted** (the ask was never + implemented) → **keep open**, and say so in the recap. + +3. **Buckets B, C, D, E** — no action. Report them so the next sweep does not + re-derive the same conclusions, and point bucket E at `$community`. + +## Recap + +``` +Issue Cleanup — Recap + Closed N as completed (bucket A) + Closed N as not_planned (obsolete, bucket F) + Held N deliberately-open · N in-flight · N external + Left N unreconciled — the real backlog + Kept open despite age: +``` + +List everything kept and why, so the next sweep resumes from there rather than +re-litigating it. + +## Regression bar + +The audit is deterministic given the same trackers and records, so it can be +checked rather than eyeballed. Against the post-sweep state of 2026-07-28 it +must reproduce exactly: + +| Bucket | Count | Notes | +|---|---:|---| +| open issues | 47 | across the 18 repos with trackers | +| **A** closable | **0** | all 29 were closed that day | +| **B** partial/umbrella | 1 | `PyAutoBrain#130` — Rule 4 | +| **C** deliberately open | 7 | see below | +| spawn-key held | 3 | Rule 2 | +| **D** in flight | 6 | `active.md` / `parked.md` | +| **E** external | 8 | community backlog | +| **F** unreconciled | 22 | the real backlog | + +Bucket C must contain exactly `PyAutoArray#377`, `PyAutoFit#1330`, +`PyAutoFit#1332`, `PyAutoFit#1338`, `PyAutoFit#1405`, `PyAutoReduce#13`, +`PyAutoReduce#17` — note `#1338` arrives via an annotated `plan:` line and +`#1405` via `Status: issued`, so this count also exercises Rule 3. + +The three spawn-key holds are `PyAutoArray#326` (`library-followup-issue`), +`autolens_workspace_test#106` (`follow-up-issue`) and `autolens_workspace_test#77` +(`followup-issue`) — the check that catches a regression to loose `*issue*:` +matching. + +Two traps this bar exists to catch, both of which a first implementation got +wrong: + +- **`PyAutoGalaxy#417` must land in F, not in a held bucket.** It is cited only + inside another record's `- notes:` prose. If it shows up as "held", the + header-key allowlist (Rule 1) has regressed to matching any `- word:` line — + right answer, wrong reason. +- **`PyAutoBrain#130` must land in B, not A.** If it appears as closable, Rule 4 + has regressed and umbrella issues are being closed on partial evidence. +- `PyAutoHands#16` and `#17` stay in F despite being ~1337 days old — match them + exactly, not by a `PyAutoHands#1*` prefix, which also catches `#127`/`#156`/`#161`. + +## Execution environments + +The audit is pure `gh` + reading `PyAutoMind/`, so it runs anywhere `gh` is +authenticated — including mobile Claude Code chat and Codex, where +`$repo-cleanup` cannot run at all. No local library checkout is needed. + +This is the harness-portable half of the pair, so keep it that way: no step may +depend on a local library checkout, a Claude-only tool, or a `~/.claude` path. +Commands in this file are plain `gh` + POSIX shell for exactly that reason. +`bin/install.sh` installs the skill into `~/.claude/skills/issue_cleanup`, +`~/.claude/commands/issue_cleanup.md` and `~/.codex/skills/issue-cleanup` (Codex +takes the hyphenated `name:` from `SKILL.md`, which is why that frontmatter must +stay hyphenated). + +The one degraded piece is the obsolescence probe in bucket F, which greps repo +source: without a local checkout, use +`gh api repos///contents/` or the code-search API, and if +neither is available report the ancient issues as **candidates only** rather +than closing them. diff --git a/skills/wake_up/wake_up.md b/skills/wake_up/wake_up.md index d2e8613..76bb672 100644 --- a/skills/wake_up/wake_up.md +++ b/skills/wake_up/wake_up.md @@ -66,6 +66,12 @@ Run in order, then emit the digest. ranking. Surface who is waiting and for how long; respond via **`/community`** (drafts are human-approved there — never auto-reply from the digest). +6b. **Issue-tracker drift** — run the **audit half of `/issue_cleanup`** + (read-only, `gh` + `PyAutoMind/complete/`): how many issues are open, and how + many look shipped-but-still-open. Report the counts only — **never close + anything from the digest**; closing is `/issue_cleanup`'s own + confirmation-gated step. A rising shipped-but-open count means ship flows are + skipping their issue close. 7. **Resume context** — pick up where you left off: in-flight / parked / queued work (`PyAutoMind/active.md`, `parked.md`, `queue.md`) + open **pending-release PRs** (`gh`); **locally** also worktrees with unpushed commits. @@ -79,6 +85,8 @@ Emit one prioritized card: - ⚠️ **Drifted** — off-main / behind repos, version-pin mismatches. - 🔄 **Resume** — in-flight / parked tasks, open pending-release PRs. - 🧹 **Cleanable** — cleanup surfaced *for approval*: list it, never auto-act. + Includes the issue-tracker counts from step 6b (point at **`/issue_cleanup`**) + alongside the git debris (**`/repo_cleanup`**). - ✅ **Clear** — say so in one line. End with a one-line verdict — *"clear to work"* or *"N things need you"* — and, in