← Corpus / augment-it / agent-skill
augment-it/agent-skills/triage-inbox-w-suggestions/skill
The operator+agent discipline for draining a client's corpus inbox (clients/<client>/corpus/inbox/) — agent scans each pending file, stamps a triage_suggestion, and proposes destinations in confidence-banded batches; the operator sweeps. Proven on the first co-pilot run (2026-07-25: reach-edu 141→4 in six batches); carries the run playbook, six-bucket org taxonomy, pointer/stream/gated routing, and reusable scripts/ helpers. 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.
- Path
- agent-skills/triage-inbox-w-suggestions/SKILL.md
Triage Inbox with Suggestions
Why (operator framing, 2026-07-25)
Triage is not just filing content — it is indexing all the actors and players in the space, peeling the onion toward a MECE knowledge graph (mutually exclusive, collectively exhaustive: every real entity gets exactly one canonical home, and the buckets jointly cover the space). Consequences for triage judgment:
- The actor matters even when the page is thin. A 2KB homepage is weak content, but if it names a real player (TradesFutures, a state agency, an employer network), mint the org row and file it — the entity is the point, the page is just its first evidence.
- Bucket questions are taxonomy questions. When a capture fits no bucket (an operating provider, a nested initiative), that’s a signal the MECE partition is incomplete — surface it as a decision, don’t force-fit.
- Relations complete the graph. Pointer files, streams, and (pending) parent/child edges are how one fact serves multiple entities without breaking mutual exclusivity of the canonical home.
The run playbook (distilled from the first co-pilot run)
Per batch of ~20 pending files: scan → stamp → sweep → execute → log. The per-item decision sequence, in order:
- First-party check. Is this the client’s own content (reach-edu: reach.edu, healthapprenticeship.org)? → first-party home, registered on the client’s own org row. Never propose a new bucket for the client’s own programs.
- Duplicate check. Same
exact_url(or binary sha256) already captured or filed? → DEDUPE (archive the lesser capture; page-vs-PDF of the same artifact are NOT dupes — file both, Work-Trend-Index precedent). - Stream check. Rolling index page on a tracked org (topic hub, blog
index, initiative hub)? → STREAM lane:
organization.streams.add, then archive the capture. - Fetch-health check. 403/451/CAPTCHA/paywall stub? → GATED — unless
the content is worth one ENRICH attempt now (Firecrawl recovers Forbes,
AP, Substack-public;
pdftotextfor local PDFs; web-search to identify an unlabeled paper). A recovered wall-junk basename gets renamed to a real one. - Destination. Org-attributable → the org’s role bucket (search
BEFORE minting; enrich names immediately after any create) with
reference_of:pointers fanned into relevant domainsources/. Topical → domain viasource.add+ canonical merge. Tool homepage → the tools topic. Profile page on an identity-link host → the org it profiles. Operator may rule “article only for now” (Deloitte, WEF) — file content to a domain, defer the org. - Stamp DB-side identity into frontmatter (
content_uuid/source_uuid,org_uuid,org_slug), tags at filing time,triaged_*provenance, manifest row with every uuid.
Calibration from run 1 (bands vs reality): operator overrides were almost never about aboutness — the suggestion engine’s destination reads held up — they were about taxonomy (new buckets minted mid-run: gated, streams, gov-entities, think-tanks, associations-networks, academic-institutions, data-services) and about modeling (parent/child, entity vs content). Auto-band (≥0.90) discards were overridden once, category-wide (discard→gated) — after which the corrected lane never missed. Implication: the ≥0.90 band can graduate to batch-confirm groups confidently; the human’s attention belongs on new-entity and new-bucket proposals.
Reusable helpers (scripts/ beside this SKILL.md)
nats-req.mjs <subject> <json> [timeoutMs]— one-shot capability call over NATS (the whole wire runs through this).stamp-suggestions.mjs <spec.json> <inbox-dir>— inserttriage_suggestion:blocks (skips already-stamped files).merge-canonical.mjs <mapping.json> [client]— inbox-file-is-canonical merge over asource.addstub: grafts registry keys, sets status/fetched, appends the Extracts skeleton, moves binaries.org-move.mjs <mapping.json> [client]— org-bucket filing: frontmatter stamp (content/org uuids, funder_slug/org_slug, triaged_*), binary sibling move,reference_of:pointer files.org-tag.mjs <org-uuid> <client> <Tag...>— has_tag observations with uuid-typed subjects (the direct-write stopgap; see the capability-gaps issue).
The inbox (per [[../../specs/Corpus-Inbox-Capture-and-Triage|Corpus-Inbox-Capture-and-Triage]])
promised “capture first, triage later.” Capture shipped 2026-06-09; later is
now — reach-edu’s inbox sits at 162 pending files and the flat list broke
down at 86 (see [[../../explorations/Inbox-Sort-by-Agent-Tasks|Inbox-Sort-by-Agent-Tasks]],
whose taxonomy and confidence bands this skill operationalizes). The goal of a
triage run: every item leaves inbox/ for a core folder — an existing one,
or a new one we create deliberately. Strive for a full drain; a residue of
genuinely-uncertain items staying pending is acceptable, forcing them is not.
Ground truth (verified against code + disk, 2026-07-25)
- Inbox files live at
clients/<client>/corpus/inbox/<date>_<slug>.md— real corpus markdown with thecaptured_*/triaged_*/inbox_statusfrontmatter blocks. Many havetags: []and emptycaptured_note. - Inbox items have NO SurrealDB row.
corpus.inbox.addwrites the file only. The single-index belief holds for filed content —source.addmints asource_uuid(+ per-corpussource_usagesrows) for domain corpora;organization.corpus.add/person.corpus.addmintcontent_itemsrows for org/person corpora — but the canonical uuid for an inbox item is minted at triage time, by filing through a capability. Never handcraft uuids or DB rows; the capability path also carries theclienttag every canonical write requires. - Disk taxonomy under
clients/<client>/corpus/:funders/<org-slug>/— corpora for organizations flagged as funders. (Canonically these areorganizationsin SurrealDB; funder is a role modifier, and not every collection-worthy org is one — a non-funder org destination means deciding where non-funder org corpora live: see Open decisions.)strategies/<slug>/sources/— domain corpora; the folder name comes from the domain type (strategy→strategies,topic→topics,thesis→theses,category→categories,market-segment→market-segments). reach-edu’s domains are strategies.gov-entities/<slug>/— organizations clearly initiated by government, or government itself (state workforce offices, federal programs, …). Ruled 2026-07-25; same org-row + corpus-add wire as funders.think-tanks/<slug>/— think tanks and research institutions (Brookings, Urban Institute, New America, …). Ruled 2026-07-25 in the same run. The non-funder-org question is resolving into role-named sibling buckets (funders / gov-entities / think-tanks) rather than one genericorganizations/folder; DB org rows stay undifferentiated, disk placement carries the role.associations-networks/<slug>/— membership associations AND networks (renamed fromassociations/2026-07-25 when WEF was identified as a network — the bucket covers both) (BHEF, NAWDP, …). Ruled 2026-07-25 later in the same run; fourth role-named bucket. The org-attributable routing rule below applies to these too (BHEF’s ED559688 report: canonical inassociations/bhef/, pointer in workforce-development).academic-institutions/<slug>/— university research centers and academic units (Project on Workforce at Harvard, …). Ruled 2026-07-25, fifth role bucket; same org-row-first + routing rules. These are also inherently NESTED (Project on Workforce ⊂ HKS ⊂ Harvard) — every filing here feeds the parent/child issue.data-services/<slug>/— nonprofit data utilities (Credential Engine, …). Ruled 2026-07-25, sixth role bucket. Distinct from the grant-prospecting-tools TOPIC: the topic holds tool homepages as content; this bucket holds data-infrastructure orgs as actors. Routing rule (ruled 2026-07-25): think-tank reports/articles are often topical too — when content is attributable to a single think tank, the canonical file goes in the think tank’s folder, and pointer markdown files (reference_of:frontmatter) fan into every relevant domain’ssources/folder (topics/<slug>/sources/,strategies/<slug>/sources/). Pointers are disk-only — nosource.addrow is minted for them (that would double-register the content); the double-count caution in Open decisions applies.inbox/— the queue this skill drains._discarded/— created on first discard; plain visible folder, never dot-prefixed, never hard-delete.AGENTS.mdat the corpus root — read it at session start; it is the corpus’s own operating guidance.
The destination model
Each pending item gets one primary home plus optional reference copies:
-
Primary home — a funder org folder, a person, or a domain (thesis/strategy/topic/…). Filing goes through the capability wire so the canonical index row exists first:
source.add(domains, resolves(domain_type, domain_slug)against the livedomain.list— never fabricate a slug, per the [[../inbox-curation/SKILL|inbox-curation]] decision tree),organization.corpus.add(org/funder),person.corpus.add(person). -
Reference copies — once the canonical uuid exists, the markdown is cheap to replicate into any other folder where agents will later work, so deliverable-generation can stay inside one folder without hunting stragglers. Every reference copy MUST declare itself in frontmatter:
reference_of: "<canonical uuid (source_uuid or content_items id)>" canonical_path: "corpus/strategies/workforce-development/sources/2026-06-09_….md" reference_note: "replicated here because <one line>"The
reference_ofkey is the loop-safety contract: anything scanning a folder dedupes by it, and coverage/count surfaces must learn to skip it (see Open decisions — double-count risk). -
Sweeping / hard-to-classify content gets an abstract home, not a forced org: propose a
topic(orcategory) domain viadomain.create— alwayschat_propose-grade (creating a corpus is a visible, workspace-wide decision), never silent — and file there. Examples from the live inbox: cross-funder sector reports, Work-Trend-Index-style industry PDFs, regulatory documents. -
Identity-link hosts (ruled 2026-07-25): Cause IQ, Charity Navigator, GrantForward, Grant Bay — Crunchbase-like profile databases (roster also effectively includes projects.propublica.org, instrumentl.com, fconline.foundationcenter.org, grantable.co-style prospect pages seen in funder folders). Routing: a profile page on one of these is a profile OF an org → file it to the org it’s about (these should eventually be smartly handled by the UI as org identity links); the tool’s own homepage / content marketing → a topic (first instance:
topics/grant-prospecting-tools/, created this run).
The suggestion pass (the scanning half)
For each inbox_status: pending file, read the cheap signals — title,
exact_url host, published_at, captured_note, first ~500 chars of body,
binary_asset presence — and match against three rosters loaded once per
run: the funder/org roster (slugs + known domains), the live domain.list,
and the harvested tag roster. Stamp the result into the file as the
triage_suggestion: block (schema per
[[../../explorations/Inbox-Sort-by-Agent-Tasks|Inbox-Sort-by-Agent-Tasks]] —
action, proposed destination, confidence 0–1, one-line rationale, signals
used). The block is a proposal; the operator’s sweep decides.
Confidence bands govern the sweep affordance:
| Band | Confidence | Sweep shape |
|---|---|---|
| Auto-routable | ≥ 0.90 | Presented as a batch list; one operator yes files the whole batch |
| Suggested | 0.60–0.89 | Per-item confirm (“looks like X — yes / different / skip”) |
| Uncertain | < 0.60 | Operator decides unaided; agent stays quiet |
Co-pilot phase rule: no auto-apply, ever, without an operator sweep — even the ≥0.90 band files only after a batch yes. Downgrade this friction only after calibration data exists (operator override rate per band).
Task lanes
Every file maps to exactly one lane (taxonomy from the exploration):
- TRIAGE — destination is clear or suggestible. The main lane.
- EXTRACT — PDFs/binaries with empty bodies: run extraction before
classification, or classify from title+URL alone with a confidence
penalty and note
body_unextracted: truein the suggestion. - ENRICH — stub captures needing more context; agent may fetch linked pages, human picks.
- DEDUPE —
exact_url(or binary sha256) already exists incontent_items/sourcesor in a filed folder: propose merge-or-skip, never file a duplicate. - FLAG — high-signal, operator wants it surfaced but not filed yet; stays pending with the flag noted.
- STREAM (ruling, first co-pilot run 2026-07-25) — a capture that is a
rolling index page on a tracked org’s site (a topic hub, blog index,
issues page — Brookings’ /topics/artificial-intelligence/, New America’s
/issues/education-and-work/) is not corpus content and not a discard: it’s
a pulse stream. Propose
organization.streams.add {org_slug, url, kind, name, client}on the org, then archive the capture file to_discarded/with a superseded-by-stream note. The stream keeps pulsing; a one-time capture of it is worthless. Knownkindvocabulary (grows by operator precedent):topic_stream— a topic/issues hub (Brookings AI, New America Education & Work)blog_index— an org’s blog/news index (BHEF Blog)initiative_hub— the hub page of a named org initiative — a program with its own rolling identity inside the org (AEI’s Workforce Futures Initiative onamerican-enterprise-institute, added 2026-07-25). Also the lightweight answer for initiative-shaped pages while the parent-child org model is unresolved: a hub can be a stream on the parent org without minting a child org.
- GATED (ruling, first co-pilot run 2026-07-25) — fetch-blocked
captures (403 “Access Denied”, CAPTCHA/“Just a moment” walls, paywall
stubs) are NOT discards: the URL is still wanted, only the fetch failed.
Move to
inbox/gated/(plain visible folder),inbox_status: "gated". They stay parked for a later re-fetch attempt via a different fetcher; a gated item whose URL later lands successfully becomes purgeable. - DISCARD — genuinely worthless captures: 404 bodies, consent
boilerplate, empty nav/index pages,
content_length_bytestiny with nothing behind it. Move tocorpus/_discarded/(archive, no delete),inbox_status: "discarded". Fetch-blocked pages go to GATED, not here.
Drain order: auto-routable TRIAGE first, then suggested TRIAGE, EXTRACT, DEDUPE, DISCARD, then the uncertain residue. Easy buckets first; the human’s attention goes to the hard tail.
Mechanics of one filing
- File through the capability → canonical uuid + client tag exist.
- Place the markdown in the primary home; populate the
triaged_*block (triaged_by: "operator-confirmed:triage-inbox-w-suggestions"when the operator confirmed an agent proposal — provenance matters) and flipinbox_status: "triaged". Keep thecaptured_*block untouched forever. - Write any reference copies with their
reference_offrontmatter. - Tags (see below) go on at filing time, not before.
- Never clobber anything a human already filed — filing is additive; collisions become DEDUPE decisions.
Mechanics fork RESOLVED (first co-pilot run, 2026-07-25): the inbox file is canonical — option (a). Call the capability first so the uuid + client tag exist (
source.addwrites a metadata-only stub and returnssource_uuid+corpus_path), then merge: graft the stub’s registry keys (source_uuid,url,normalized_url, bib fields,domains) into the inbox file’s frontmatter, setstatus: "fetched"/content_pulled: true, append the# Extractsskeleton, write the result over the stub atcorpus_path, delete the inbox original. PDF siblings move too, renamed to the destination basename (and updatebinary_asset.filename) — a binary is never left behind in the inbox. ⚠️ Registry gotcha: the DBsources/source_usagesrows still saymetadata-onlyafter this — never runsource.fetchon a merged source (it would refetch and clobber the canonical body). Open follow-up: a registry-status patch or a source.add variant accepting a provided body.Funder/org filings (same run): SurrealDB is the source of truth. A new org folder REQUIRES an org row first — but search before minting:
resolver.search {q, client}/organization.detailfirst, because the operator may have created the org in the UI in parallel (a duplicatebhefrow got minted this way and had to be consolidated). Thenresolver.apply {action: "create", record: {name, slug_hint, url}, client}(stampsclient_access), thenorganization.corpus.add {org_slug, url, client}mints thecontent_itemsentry, then move the inbox file into the org’s folder (keep its inbox basename), stampingcontent_uuid+org_uuidand swappingfunder_slug: "inbox"→ the real slug (or null for non-funder buckets, withorg_slugcarrying the identity). Legacy funder folders may be rowless (jff was) — mint the row on first filing.Slug naming (ruled 2026-07-25): long-form full-name slugs —
business-higher-education-forum, notbhef;national-association-of-workforce-development-professionals, notnawdp. The acronym/short form goes inaliasesandconventional_name(settable viaresolver.update_org, which also does slug renames — though conventional_name has no edit affordance in the app yet). Disk folder always matches the DB slug. Legacy short slugs (jff) rename lazily, folder + row together.The org naming model (ratified on stanford-pacs, 2026-07-25). Typed name fields answer rendering (“what do I print here”);
aliases[]answers resolution (“what strings should find this entity”), like Obsidian frontmatter aliases:
slug— derived, never authored: long-form kebab of the conventional name; identity key; matches the disk folder.conventional_name— what humans call it (“Stanford PACS”, “TWC”, “CRS”); prose and chips.complete_name— the full formal name (“Stanford Center on Philanthropy and Civil Society”); documents and disambiguation.- (
legal_name— only if it ever diverges AND a surface needs it; until then it’s just an alias.)aliases[]— greedy, additive, untyped: acronyms, smushed forms, hyphen variants, former names, misspellings — every form seen in the wild. Costs nothing; the resolver matches on it; slug renames auto-preserve the old slug here. When any create flow (UI or agent) mints an org from one string, enrich immediately withresolver.update_org— one string can’t fill a three-field model.
Tagging convention
- YAML inline array of quoted strings:
tags: ["Workforce-Development", "State-of-the-Industry", "Future-of-Work"] - Casing is title-like with dashes: each substantive word capitalized,
connector/minor words lowercase (“of”, “the”, “on”, “an”, “and”),
acronyms uppercase (
"HNWI","AI-in-K-12"). Not ALLCAPS; not every-word Train-Case. - Prefer an existing tag over minting a new one. Harvest the roster at
run start (grep
tags:across the client’s corpus + the domain sources’ tags) and match against it; new tags need a reason a roster tag can’t serve.
Batch rhythm & resumability
- Work in batches of ~15–25 files; each batch = scan → stamp suggestions → operator sweep → file → log.
- Keep a run manifest at
clients/<client>/corpus/inbox/_triage-runs/<date>_<n>.md(plain visible folder): one line per file — decision, destination, uuid, band, who decided. This is what makes a cleared-context session resumable and the calibration data for loosening the bands later. - Idempotence: only
inbox_status: "pending"files are in scope; a re-run after an interrupted session picks up exactly where the manifest stopped.
Never
- Hard-delete anything (discard = archive to
_discarded/). - Fabricate a domain slug, org slug, or uuid — resolve against live rosters.
- File a canonical write without the
clienttag (the capability wire does this for you; another reason not to bypass it). - Dot-prefix a folder meant for human review.
- Force the last uncertain items just to hit 100% — pending is an honest state.
Open decisions (co-pilot phase resolves these; record rulings in place)
-
The mechanics fork— RESOLVED: inbox file is canonical; see the ruling block in “Mechanics of one filing”. -
Discard vs fetch-blocked— RESOLVED: GATED lane added; blocked fetches park ininbox/gated/, discards are for genuinely worthless content only. - Government entities (ruled 2026-07-25): entities clearly initiated
by government, or government itself, get
corpus/gov-entities/<slug>/— clients (reach-edu certainly) want to track all kinds of government-related entities. Same filing wire as funders (org row viaresolver.apply action:create, thenorganization.corpus.add, then move the file); disk placement carries the gov distinction, DB-side flagging is a follow-up. First filing: accelerate-ms. - Non-funder, non-government organizations: orgs worth collecting on
that are neither funders nor gov-initiated (membership forums like
BHEF, professional associations like NAWDP, employer networks like
Human Potential Network) — sibling folder
(
corpus/organizations/<slug>/?) or something else? Still open; captures accumulate pending in the inbox. - Parent-child organizations — MODEL LANDED (2026-07-27): org↔org
relations are live (
organization.relate/.relations/.unrelate/.relation.update— parent/child/peer + kind + free-text description, edges in theaffiliationstable) along with org tags (organization.tag.add/.remove— Initiative, Program, Fund…). Per [[../../plans/Org-Relations-Parent-Child-Peer-Plus-Org-Tags|the plan]]; history in [[../../issues/Parent-Child-Nested-Organizations-Not-Modeled|Parent-Child-Nested-Organizations-Not-Modeled]]. Aboutness routing (step 5b): when a destination org has relations or the page names an initiative/fund/program of a parent, ask which entity the content is about before filing — parent content on the parent, initiative content on the child,reference_of:pointer across the seam when both want it. An initiative that is a real actor but has no row yet → mint the child, relate it (rel: parent,kind: initiative_of), tag itInitiative, and file there. The parked upmobility/urban-institute captures are the pilot filing (issue’s worklist section A). - Person-destined content (ruled 2026-07-27): individual funders
keep normal
funders/<slug>/folders (the funders corpus is the funder-to-strategy mapping substrate); DB identity is a persons row affiliated to their org(s); folder files carryperson_uuid+person_name+entity_kind: "person"frontmatter. Also ruled: the DB slug is the source of truth for folder names — rename folders, not rows (welded parent/child slugs excepted, parked on that issue). - Reference-copy double-counting: coverage lenses and
corpus.list_for_recordwalk the filesystem; teach them to skipreference_of:files before fanning references widely. - When (if ever) the ≥0.90 band graduates from batch-confirm to auto-apply-with-undo.
Agent-chat availability (since 2026-07-25)
A condensed operational form of this skill ships in didi’s chat as the
ACTIVE_SKILLS slab in services/workspace/src/chat.ts (the Slab-3 spot
reserved since v0.0.1), teaching the triage verbs
(organization.corpus.add, resolver.search, resolver.apply,
resolver.update_org, organization.streams.add — and, since 2026-07-27,
the relations/tags verbs organization.relations, organization.relate,
organization.tag.add with the step-5b aboutness routing) and the decision
sequence above. Honest limit: chat-didi can register, resolve, mint, and
stream — the disk half of a filing (canonical merge, pointer files,
binary moves) still runs session-side via the scripts above, until a
corpus.triage.apply capability exists. This SKILL.md remains the source
of truth; the slab is its condensation — update both together.
See also
- [[../../specs/Corpus-Inbox-Capture-and-Triage|Corpus-Inbox-Capture-and-Triage]] — capture spec; this skill is the triage layer it deferred.
- [[../../explorations/Inbox-Sort-by-Agent-Tasks|Inbox-Sort-by-Agent-Tasks]] — taxonomy,
triage_suggestion:schema, confidence bands, phased plan. - [[../inbox-curation/SKILL|inbox-curation]] — didi’s live filing decision tree; this skill is its bulk/backlog sibling and shares its domain-resolution discipline.
- [[../../plans/Download-PDFs-into-Corpus-Inbox|Download-PDFs-into-Corpus-Inbox]] — why binaries sit beside markdown; feeds the EXTRACT lane.
clients/<client>/corpus/AGENTS.md— the corpus’s own standing guidance; read before every run.