|
| 1 | +--- |
| 2 | +name: triage-inbox-w-suggestions |
| 3 | +description: The operator+agent discipline for draining a client's corpus inbox (clients/<client>/corpus/inbox/, 162 pending in reach-edu as of 2026-07-25) — agent scans each pending file, stamps a triage_suggestion, and proposes destinations in confidence-banded batches; the operator sweeps. Use whenever the user says "triage the inbox", "work through the inbox", "drain the inbox", "/triage-inbox", or asks to file/sort pending inbox captures. Encodes the destination model (funder org folder, person, or domain — with SurrealDB as the single canonical index and cheap reference .md copies fanned into any additionally-relevant folder), the six task lanes (TRIAGE/EXTRACT/ENRICH/DEDUPE/FLAG/DISCARD), the tagging convention (YAML array, Train-Case with lowercase connector words), and the batch/resume rhythm. Co-pilot phase: agent proposes, operator disposes; nothing files without an operator sweep. |
| 4 | +--- |
| 5 | + |
| 6 | +# Triage Inbox with Suggestions |
| 7 | + |
| 8 | +The inbox (per [[../../specs/Corpus-Inbox-Capture-and-Triage|Corpus-Inbox-Capture-and-Triage]]) |
| 9 | +promised "capture first, triage later." Capture shipped 2026-06-09; *later* is |
| 10 | +now — reach-edu's inbox sits at **162 pending files** and the flat list broke |
| 11 | +down at 86 (see [[../../explorations/Inbox-Sort-by-Agent-Tasks|Inbox-Sort-by-Agent-Tasks]], |
| 12 | +whose taxonomy and confidence bands this skill operationalizes). The goal of a |
| 13 | +triage run: **every item leaves `inbox/` for a core folder** — an existing one, |
| 14 | +or a new one we create deliberately. Strive for a full drain; a residue of |
| 15 | +genuinely-uncertain items staying `pending` is acceptable, forcing them is not. |
| 16 | + |
| 17 | +## Ground truth (verified against code + disk, 2026-07-25) |
| 18 | + |
| 19 | +- **Inbox files** live at `clients/<client>/corpus/inbox/<date>_<slug>.md` — |
| 20 | + real corpus markdown with the `captured_*` / `triaged_*` / `inbox_status` |
| 21 | + frontmatter blocks. Many have `tags: []` and empty `captured_note`. |
| 22 | +- **Inbox items have NO SurrealDB row.** `corpus.inbox.add` writes the file |
| 23 | + only. The single-index belief holds for *filed* content — `source.add` |
| 24 | + mints a `source_uuid` (+ per-corpus `source_usages` rows) for domain |
| 25 | + corpora; `organization.corpus.add` / `person.corpus.add` mint |
| 26 | + `content_items` rows for org/person corpora — but the canonical uuid for an |
| 27 | + inbox item is **minted at triage time, by filing through a capability**. |
| 28 | + Never handcraft uuids or DB rows; the capability path also carries the |
| 29 | + `client` tag every canonical write requires. |
| 30 | +- **Disk taxonomy** under `clients/<client>/corpus/`: |
| 31 | + - `funders/<org-slug>/` — corpora for organizations flagged as funders. |
| 32 | + (Canonically these are `organizations` in SurrealDB; *funder* is a role |
| 33 | + modifier, and not every collection-worthy org is one — a non-funder org |
| 34 | + destination means deciding where non-funder org corpora live: see Open |
| 35 | + decisions.) |
| 36 | + - `strategies/<slug>/sources/` — domain corpora; the folder name comes from |
| 37 | + the domain type (`strategy`→`strategies`, `topic`→`topics`, |
| 38 | + `thesis`→`theses`, `category`→`categories`, |
| 39 | + `market-segment`→`market-segments`). reach-edu's domains are strategies. |
| 40 | + - `inbox/` — the queue this skill drains. |
| 41 | + - `_discarded/` — created on first discard; plain visible folder, never |
| 42 | + dot-prefixed, never hard-delete. |
| 43 | + - `AGENTS.md` at the corpus root — read it at session start; it is the |
| 44 | + corpus's own operating guidance. |
| 45 | + |
| 46 | +## The destination model |
| 47 | + |
| 48 | +Each pending item gets **one primary home** plus optional reference copies: |
| 49 | + |
| 50 | +1. **Primary home** — a funder org folder, a person, or a domain |
| 51 | + (thesis/strategy/topic/…). Filing goes **through the capability wire** so |
| 52 | + the canonical index row exists first: `source.add` (domains, resolves |
| 53 | + `(domain_type, domain_slug)` against the live `domain.list` — never |
| 54 | + fabricate a slug, per the [[../inbox-curation/SKILL|inbox-curation]] |
| 55 | + decision tree), `organization.corpus.add` (org/funder), |
| 56 | + `person.corpus.add` (person). |
| 57 | +2. **Reference copies** — once the canonical uuid exists, the markdown is |
| 58 | + cheap to replicate into *any other* folder where agents will later work, |
| 59 | + so deliverable-generation can stay inside one folder without hunting |
| 60 | + stragglers. Every reference copy MUST declare itself in frontmatter: |
| 61 | + |
| 62 | + ```yaml |
| 63 | + reference_of: "<canonical uuid (source_uuid or content_items id)>" |
| 64 | + canonical_path: "corpus/strategies/workforce-development/sources/2026-06-09_….md" |
| 65 | + reference_note: "replicated here because <one line>" |
| 66 | + ``` |
| 67 | +
|
| 68 | + The `reference_of` key is the loop-safety contract: anything scanning a |
| 69 | + folder dedupes by it, and coverage/count surfaces must learn to skip it |
| 70 | + (see Open decisions — double-count risk). |
| 71 | +3. **Sweeping / hard-to-classify content** gets an *abstract* home, not a |
| 72 | + forced org: propose a `topic` (or `category`) domain via `domain.create` |
| 73 | + — always `chat_propose`-grade (creating a corpus is a visible, |
| 74 | + workspace-wide decision), never silent — and file there. Examples from |
| 75 | + the live inbox: cross-funder sector reports, Work-Trend-Index-style |
| 76 | + industry PDFs, regulatory documents. |
| 77 | + |
| 78 | +## The suggestion pass (the scanning half) |
| 79 | + |
| 80 | +For each `inbox_status: pending` file, read the cheap signals — `title`, |
| 81 | +`exact_url` host, `published_at`, `captured_note`, first ~500 chars of body, |
| 82 | +`binary_asset` presence — and match against three rosters loaded once per |
| 83 | +run: the funder/org roster (slugs + known domains), the live `domain.list`, |
| 84 | +and the harvested tag roster. Stamp the result into the file as the |
| 85 | +`triage_suggestion:` block (schema per |
| 86 | +[[../../explorations/Inbox-Sort-by-Agent-Tasks|Inbox-Sort-by-Agent-Tasks]] — |
| 87 | +action, proposed destination, confidence 0–1, one-line rationale, signals |
| 88 | +used). The block is a **proposal**; the operator's sweep decides. |
| 89 | + |
| 90 | +Confidence bands govern the sweep affordance: |
| 91 | + |
| 92 | +| Band | Confidence | Sweep shape | |
| 93 | +|---|---|---| |
| 94 | +| Auto-routable | ≥ 0.90 | Presented as a batch list; one operator yes files the whole batch | |
| 95 | +| Suggested | 0.60–0.89 | Per-item confirm ("looks like X — yes / different / skip") | |
| 96 | +| Uncertain | < 0.60 | Operator decides unaided; agent stays quiet | |
| 97 | + |
| 98 | +Co-pilot phase rule: **no auto-apply, ever, without an operator sweep** — |
| 99 | +even the ≥0.90 band files only after a batch yes. Downgrade this friction |
| 100 | +only after calibration data exists (operator override rate per band). |
| 101 | + |
| 102 | +## Task lanes |
| 103 | + |
| 104 | +Every file maps to exactly one lane (taxonomy from the exploration): |
| 105 | + |
| 106 | +- **TRIAGE** — destination is clear or suggestible. The main lane. |
| 107 | +- **EXTRACT** — PDFs/binaries with empty bodies: run extraction before |
| 108 | + classification, or classify from title+URL alone with a confidence |
| 109 | + penalty and note `body_unextracted: true` in the suggestion. |
| 110 | +- **ENRICH** — stub captures needing more context; agent may fetch linked |
| 111 | + pages, human picks. |
| 112 | +- **DEDUPE** — `exact_url` (or binary sha256) already exists in |
| 113 | + `content_items` / `sources` or in a filed folder: propose merge-or-skip, |
| 114 | + never file a duplicate. |
| 115 | +- **FLAG** — high-signal, operator wants it surfaced but not filed yet; |
| 116 | + stays pending with the flag noted. |
| 117 | +- **DISCARD** — cookie walls, 404 bodies, consent boilerplate, |
| 118 | + `content_length_bytes` tiny: move to `corpus/_discarded/` (archive, no |
| 119 | + delete), `inbox_status: "discarded"`. |
| 120 | +
|
| 121 | +Drain order: auto-routable TRIAGE first, then suggested TRIAGE, EXTRACT, |
| 122 | +DEDUPE, DISCARD, then the uncertain residue. Easy buckets first; the human's |
| 123 | +attention goes to the hard tail. |
| 124 | +
|
| 125 | +## Mechanics of one filing |
| 126 | +
|
| 127 | +1. File through the capability → canonical uuid + client tag exist. |
| 128 | +2. Place the markdown in the primary home; populate the `triaged_*` block |
| 129 | + (`triaged_by: "operator-confirmed:triage-inbox-w-suggestions"` when the |
| 130 | + operator confirmed an agent proposal — provenance matters) and flip |
| 131 | + `inbox_status: "triaged"`. Keep the `captured_*` block untouched forever. |
| 132 | +3. Write any reference copies with their `reference_of` frontmatter. |
| 133 | +4. Tags (see below) go on at filing time, not before. |
| 134 | +5. Never clobber anything a human already filed — filing is additive; |
| 135 | + collisions become DEDUPE decisions. |
| 136 | + |
| 137 | +> **Open mechanics fork (resolve in the first co-pilot run, then record the |
| 138 | +> ruling here):** `source.add` fetches the URL fresh and writes its own file |
| 139 | +> under `sources/` — but the inbox file already holds a fetched body (and |
| 140 | +> sometimes a binary). Either (a) file via capability and treat the *inbox |
| 141 | +> file* as the moved/reference artifact, or (b) let the capability write its |
| 142 | +> file and demote the inbox original to a reference copy. Pick once, apply |
| 143 | +> uniformly. |
| 144 | + |
| 145 | +## Tagging convention |
| 146 | + |
| 147 | +- YAML inline array of quoted strings: `tags: ["Workforce-Development", "State-of-the-Industry", "Future-of-Work"]` |
| 148 | +- Casing is title-like with dashes: each substantive word capitalized, |
| 149 | + **connector/minor words lowercase** ("of", "the", "on", "an", "and"), |
| 150 | + acronyms uppercase (`"HNWI"`, `"AI-in-K-12"`). Not ALLCAPS; not |
| 151 | + every-word Train-Case. |
| 152 | +- **Prefer an existing tag over minting a new one.** Harvest the roster at |
| 153 | + run start (grep `tags:` across the client's corpus + the domain sources' |
| 154 | + tags) and match against it; new tags need a reason a roster tag can't |
| 155 | + serve. |
| 156 | + |
| 157 | +## Batch rhythm & resumability |
| 158 | + |
| 159 | +- Work in batches of ~15–25 files; each batch = scan → stamp suggestions → |
| 160 | + operator sweep → file → log. |
| 161 | +- Keep a run manifest at `clients/<client>/corpus/inbox/_triage-runs/<date>_<n>.md` |
| 162 | + (plain visible folder): one line per file — decision, destination, uuid, |
| 163 | + band, who decided. This is what makes a cleared-context session resumable |
| 164 | + and the calibration data for loosening the bands later. |
| 165 | +- Idempotence: only `inbox_status: "pending"` files are in scope; a re-run |
| 166 | + after an interrupted session picks up exactly where the manifest stopped. |
| 167 | + |
| 168 | +## Never |
| 169 | + |
| 170 | +- Hard-delete anything (discard = archive to `_discarded/`). |
| 171 | +- Fabricate a domain slug, org slug, or uuid — resolve against live rosters. |
| 172 | +- File a canonical write without the `client` tag (the capability wire does |
| 173 | + this for you; another reason not to bypass it). |
| 174 | +- Dot-prefix a folder meant for human review. |
| 175 | +- Force the last uncertain items just to hit 100% — pending is an honest |
| 176 | + state. |
| 177 | + |
| 178 | +## Open decisions (co-pilot phase resolves these; record rulings in place) |
| 179 | + |
| 180 | +- [ ] The mechanics fork above (move-the-inbox-file vs capability-writes- |
| 181 | + fresh + inbox file becomes reference). |
| 182 | +- [ ] **Non-funder organizations**: orgs worth collecting on that aren't |
| 183 | + funders — sibling folder (`corpus/organizations/<slug>/`?) or the |
| 184 | + funders folder with a DB flag distinction only? |
| 185 | +- [ ] **Person-destined content**: `person.corpus.add` is DB-side; persons |
| 186 | + have no on-disk corpus folder today. Where does the markdown live? |
| 187 | +- [ ] **Reference-copy double-counting**: coverage lenses and |
| 188 | + `corpus.list_for_record` walk the filesystem; teach them to skip |
| 189 | + `reference_of:` files before fanning references widely. |
| 190 | +- [ ] When (if ever) the ≥0.90 band graduates from batch-confirm to |
| 191 | + auto-apply-with-undo. |
| 192 | + |
| 193 | +## See also |
| 194 | + |
| 195 | +- [[../../specs/Corpus-Inbox-Capture-and-Triage|Corpus-Inbox-Capture-and-Triage]] — capture spec; this skill is the triage layer it deferred. |
| 196 | +- [[../../explorations/Inbox-Sort-by-Agent-Tasks|Inbox-Sort-by-Agent-Tasks]] — taxonomy, `triage_suggestion:` schema, confidence bands, phased plan. |
| 197 | +- [[../inbox-curation/SKILL|inbox-curation]] — didi's live filing decision tree; this skill is its bulk/backlog sibling and shares its domain-resolution discipline. |
| 198 | +- [[../../plans/Download-PDFs-into-Corpus-Inbox|Download-PDFs-into-Corpus-Inbox]] — why binaries sit beside markdown; feeds the EXTRACT lane. |
| 199 | +- `clients/<client>/corpus/AGENTS.md` — the corpus's own standing guidance; read before every run. |
0 commit comments