# Triage

> Triage items sitting in the canonical Inbox/ (web-clipper, Raindrop, Telegram, manual). THREE modes. Dedicated commands: `/triage-propose` (Mode A — async analysis, stages proposals, Telegram push) and `/triage-review` (Mode B — apply staged proposals). Bare `/triage` runs Mode C — the legacy per-clipping interactive walkthrough, bounded to 2 actionable verdicts/session. If .triage/pending.json e…

- **Type:** Skill
- **Install:** `agentstack add skill-ozlar34-claude-code-skills-triage`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ozlar34](https://agentstack.voostack.com/s/ozlar34)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ozlar34](https://github.com/ozlar34)
- **Source:** https://github.com/ozlar34/claude-code-skills/tree/main/skills/triage
- **Website:** https://github.com/ozlar34

## Install

```sh
agentstack add skill-ozlar34-claude-code-skills-triage
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Triage Skill

Triage items the user dropped into the canonical inbox — web clips, Raindrop exports, manual pastes, or Telegram voice/text captures. Triage is **mechanical, one file at a time** — verdict → promote → flip → archive.

This skill is a verdict-loop closer — verdict → promote → flip → archive. Unlike a pure frontmatter-flip closer, clippings carry a **destructive op** at the end (archive `mv` or `rm`), so the atomicity ordering is **promote → flip → destruct**. The flip is the stamp that says "if my next step fails, the queue can resume here without re-triaging." One verdict — `hold` — is exempt: it deliberately keeps a clipping in `Inbox/` untouched for a future run, performing no promote, flip, or destruct. It is the supported way to say "decide this later" without silently abandoning the file.

---

## Modes & routing

`/triage` has three modes. Parse the skill argument first and branch:

| Argument | Mode | What runs |
|----------|------|-----------|
| `propose` (aliases: `scan`, `batch`, `async`) | **A — Propose** | Fan out one read-only subagent per clipping → stage proposals to `.triage/pending.json` → push a summary to Telegram. **Writes nothing to the vault.** Jump to **Mode A — Propose**. |
| `review` (aliases: `execute`, `apply`, `confirm`) | **B — Review** | Load `.triage/pending.json` → walk proposals (accept-all default) → run the real promote→flip→destruct on confirmed verdicts. Jump to **Mode B — Review**. |
| *(empty)* | **C — Interactive** | The legacy per-clipping walkthrough below, bounded to 2 actionable verdicts. **But first:** if `.triage/pending.json` exists and is non-empty, tell the user a pending proposal queue exists and offer to run Review instead. |

Modes A and B are a **propose-then-execute split**: A is the autonomous analytical pass the user
kicks off whenever (then walks away — his phone gets the action list), B is the fast confirm
pass that actually empties the inbox. C is the original all-in-one interactive flow, untouched.

The split exists because the analytical work (read + fit-scan + enrich) is what's slow and
benefits from per-clipping context isolation, while the *destructive* work (mv/rm/flip) is what
must never happen without the user's confirm. A separates the first from the second; B gates the second.

**Why subagents and not a Workflow:** at typical inbox volume (a handful at a time) plain parallel
subagents give the context-isolation win without the Workflow harness's filesystem dance. If inbox
volume ever grows past ~15 routinely, the clean upgrade is to wrap Mode A's fan-out in a `Workflow`
fanning `agent()` calls with the same schema — the analysis protocol file already is the agent spec.

Shared setup (path resolution below) runs for **all three modes**.

---

## Source of truth: `vault-paths.json`

All vault paths come from `.planning/vault-paths.json` in the project dir. **Never hard-code vault folder names.** If the user has restructured the vault, the JSON is the only place that knows.

Keys consumed:
- `vault_root` — absolute path to the Obsidian vault root
- `inbox` — where unprocessed clippings live (the only place this skill pulls from)
- `resources` — `Resources/` (used only to *compose* resource-note bodies; actual write goes via `obsidian-write`)
- `areas` — `Areas/` (used by `act` + `backlog` to list candidate area notes at runtime)
- `system` — `_System/` (also listed for `act` + `backlog`; the Tool Backlog at `_System/Claude Code/Tool Backlog.md` is surfaced explicitly via `$TOOL_BACKLOG` since it sits below the top-level glob)
- `archive` — `Archive/` (root of cold storage; this skill writes to `/Source//`)

Resolve at session start (before any read or write):

```bash
VAULT_PATHS="$HOME//.planning/vault-paths.json"
VAULT_ROOT=$(jq -er '.vault_root' "$VAULT_PATHS")
INBOX=$(jq -er '.inbox' "$VAULT_PATHS")
RESOURCES=$(jq -er '.resources' "$VAULT_PATHS")
AREAS=$(jq -er '.areas' "$VAULT_PATHS")
SYSTEM=$(jq -er '.system' "$VAULT_PATHS")
ARCHIVE=$(jq -er '.archive' "$VAULT_PATHS")
# `jq -er` (NOT `jq -r`): -e forces non-zero exit on missing/null key.

# The Tool Backlog (the CC-tool sink) lives ONE LEVEL DEEPER than the top-level
# `$SYSTEM/*.md` destination glob reaches — `_System/Claude Code/Tool Backlog.md` —
# so `act`/`backlog` never surface it and the proposer guesses a wrong path. Surface
# it explicitly. We do NOT glob `_System` recursively (that floods the candidate list
# with every `Tool Notes/.md`); the Tool Backlog is the one nested sink worth knowing.
TOOL_BACKLOG="$VAULT_ROOT/$SYSTEM/Claude Code/Tool Backlog.md"

# Staged-proposal queue (Modes A + B). A *working artifact*, NOT vault content —
# lives in the project dir (gitignored), survives a /clear between propose and review.
TRIAGE_STATE="$HOME//.triage"
PENDING="$TRIAGE_STATE/pending.json"
# Plugin/system files in Inbox/ that are never triage subjects — excluded from all ls snapshots
INBOX_EXCLUDE="_Triage Queue.md"
```

If `vault-paths.json` is missing OR any key is empty, **stop and tell the user** — do not guess paths.

---

ALWAYS write via tempfile-rename — protocol: ~//.claude/lib/write-discipline.md

The destructive ops (`mv` of the clipping to archive, `rm` for `ignore`) are already atomic on APFS — no `.tmp` indirection needed.

---

## Session budget — every 2 actionable verdicts, hand off to a fresh session

This skill is bounded per invocation: it stops after **2 clippings receive an actionable verdict** (`act`, `save`, `backlog`, or `ignore`). `hold` does NOT count — it is zero-write and produces no context bloat, so a single session can absorb arbitrarily many `hold`s while still resolving up to 2 actionable items.

At the threshold, print a continuation prompt (Step 8.5) and stop the loop. The remaining inbox items will be picked up by the next `/triage` invocation from a cleared session.

Rationale: long triage runs accumulate verdict-by-verdict context that biases later proposals toward "I've already seen things like this, default to backlog." Bounding the session keeps every clipping seen with a fresh analytical lens and keeps token spend predictable.

**Counter mechanic:** initialise `ACTIONABLE_COUNT=0` at the top of the snapshot loop. Increment it inside Step 6, but ONLY for the four actionable verdicts (`act`, `save`, `backlog`, `ignore`). `hold` is exempt — it skips Steps 4–7 entirely, so the counter never advances on a held clipping. After Step 8, Step 8.5 evaluates the threshold against `ACTIONABLE_COUNT` + remaining-inbox count.

---

## Frontmatter contract (Phase 01 schema)

Every clipping in the inbox carries (or will carry, after you patch it) the Phase 01 frontmatter schema:

```yaml
---
note_type: source            # required; one of the Phase 01 enums
captured_from:       # web-clipper | telegram | manual | raindrop
captured_at: 2026-05-19      # ISO date the clipping was captured
processed: false             # flips to true the moment you commit a verdict (after promote, before mv)
tags: [...]                  # optional, from the clipper or added during triage
---
```

**Atomicity rule (load-bearing):** `processed: true` MUST be written to the clipping AFTER the promote step (area-note bullet / resource-note creation) and BEFORE any `mv` / `rm` operation. If the destructive op fails mid-flight, the next run sees a processed-but-still-in-inbox file and can recover by completing the move, rather than silently re-triaging. The promote step precedes the flip because the promote is itself recoverable (a duplicate bullet is annoying but not destructive), but if it fails AFTER the flip, the clipping would be marked processed without the back-link in place. The `hold` verdict sits outside this ordering entirely — it performs no promote, no flip, and no destructive op, so there is no atomicity concern; the clipping is left exactly as-is.

---

## The 5-verdict matrix

For each clipping, choose exactly one verdict. **Verdicts are mutually exclusive — never combine.**

| Verdict   | Meaning                                                                 | Promote (Step 4)                                                                                                                                                       | Destructive op (Step 6) |
| --------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| `act`     | This clipping triggers a concrete next action right now.                | Append a bullet to a user-chosen area note's section (default `## Notes`); optional TickTick task at Step 7.                                                            | `mv` to archive          |
| `save`    | Durable reference value — worth a clean resource note for future retrieval. Proposer classifies a `save_subtype`: reference, person, place, or long-form. | Forks on `save_subtype` (proposer-supplied; in Mode C derived from the Step 2c proposal): **reference** → `obsidian-write` (`mode=create-new`), Naming Gate + Resource→Area auto-backlink (current path); **person** → direct Write to `People/.md`, create-or-append per `save-routing.md`; **place** → direct Write to `Places/.md`, create-or-append per `save-routing.md`; **long-form** → spawn a `/summarize` sub-agent (minimal mode) that mints the summary note inline; no TickTick task. All four sub-routes keep verdict `save`. | `mv` to archive          |
| `backlog` | Useful-but-not-now — worth keeping for later, but not yet a clean note.   | Append a bullet to a user-chosen area note's `## Backlog` section (auto-create section if missing).                                                                     | `mv` to archive          |
| `ignore`  | Noise, duplicate, stale link, or context the future you won't need.    | None.                                                                                                                                                                  | `rm` (no archive copy)   |
| `hold`    | Decision consciously deferred — keep this clipping in `Inbox/` for a future run.        | None.                                                                                                                                                                  | None — file stays in `Inbox/`, byte-identical, `processed: false` |

**Four of the five verdicts (`act`, `save`, `backlog`, `ignore`) end with the file out of `Inbox/` and `processed: true` written.** `hold` is the deliberate fifth path: the clipping stays in `Inbox/`, byte-identical, `processed: false`, to be re-surfaced in the next run. `hold` is a *conscious* deferral chosen at Step 3 — not an accidental escape hatch. Every clipping must still receive an explicit verdict; what's forbidden is silently abandoning a file mid-loop. If a clipping resists classification, either ask the user for a real verdict or choose `hold` deliberately.

---

## Archive destination shape

For `act`, `save`, and `backlog` verdicts:

```
$VAULT_ROOT/$ARCHIVE/Source//.md
```

- `Source` is a fixed subfolder name (cold storage of triaged source material — distinct from `source::` as a frontmatter field)
- `` is the year from `captured_at` if present, else current year
- Filename is preserved verbatim (no rename) so existing back-links keep resolving
- Create the year folder if it doesn't exist (`mkdir -p`)

Compute the archive path BEFORE any area-note write or resource-note composition, so the back-links can reference the future archive location and resolve once the `mv` completes at Step 6.

---

## Mode C — Interactive (operating procedure, per clipping)

This is the original all-in-one flow, reached by bare `/triage` (no argument). Modes A and B
reuse its building blocks: Mode A's subagents borrow the Step 2a fit-scan logic (read-only, via
the analysis-protocol reference), and Mode B's executor reuses Steps 4–7 verbatim to apply a
confirmed verdict. Keep those steps the single source of truth — don't fork the promote/flip/
destruct logic into the new modes.

For each `*.md` file currently in `$VAULT_ROOT/$INBOX/`:

### Step 1 — Discover

```bash
ls "$VAULT_ROOT/$INBOX/"*.md 2>/dev/null | grep -v "$INBOX_EXCLUDE"
```

If the directory is empty, print "No clippings to triage." and stop. Otherwise, **snapshot this file list once at the start of the run** and iterate the snapshot — don't re-`ls` between clippings. This matters because of `hold`: a `hold` verdict leaves its file in `Inbox/`, so a fresh `ls` mid-run would re-surface a just-held file in an endless loop. Iterating a fixed snapshot guarantees each clipping is presented exactly once per run; a held file simply reappears in the *next* run's snapshot. Process the snapshot by modification time (oldest first).

If the file's frontmatter already shows `processed: true`, this is a recovery case (a prior run flipped but failed to `mv`). Skip to Step 6 — compute the archive path and complete the move. Do not re-promote or re-prompt.

### Step 2a — Full read + system-fit scan

`Read` the **full** clipping body (no truncation — the fit-scan needs the whole thing; truncation is the root cause of "everything looks like backlog").

Resolve the system-fit signals **once per session** and cache them for the snapshot — do not re-`ls` per clipping:

```bash
RESOURCE_BUCKETS=$(ls -1 "$VAULT_ROOT/$RESOURCES" 2>/dev/null)
# sed, NOT `xargs basename`: the vault root may contain an apostrophe,
# which makes `xargs -I{} basename` fail with "unterminated quote" and silently empties the list.
AREA_NOTES=$(ls -1 "$VAULT_ROOT/$AREAS"/*.md "$VAULT_ROOT/$SYSTEM"/*.md "$TOOL_BACKLOG" 2>/dev/null | sed -E 's#.*/##; s#\.md$##')
```

`RESOURCE_BUCKETS` currently resolves to subfolders like `Career, Citizenship, Claude, Coffee, Finance, German B1, Health, Home Office, Travel` — these are the buckets a `save` verdict would route into. `AREA_NOTES` are the candidate destinations for `act` and `backlog`.

Classify the clipping into one of three fit buckets by cross-referencing its body, title, tags, and (if web-clipper / raindrop / manual) URL against the cached signals:

- **clear-fit** — body clearly matches at least one known Resources subfolder OR Area note. **Name the match** in the display (e.g. "topic = Coffee → fits `Resources/Coffee/` and `Areas/Coffee Hub.md`").
- **weak-fit** — body has partial topical overlap but no obvious bucket; needs investigation before a confident verdict.
- **no-fit** — empty, garbage, dead URL, off-topic from every tracked domain. Default verdict will be `ignore`.

Display: title (or first sentence for telegram), `captured_from`, `captured_at`, fit bucket + named match. Branch source-specifically on display:

**`captured_from: telegram`:** Telegram captures are short transcribed thoughts — no title, URL, or author. Frame the fit-scan around the thought itself ("this thought fits `Areas/`" / "this thought is about , no clear bucket" / "noise"). Do not attempt to extract a title or source URL; they won't be present.

**`captured_from: web-clipper | raindrop | manual`:** Frame the fit-scan as an external reference fitting (or not fitting) a destination.

### Step 2b — Optional enrichment (smart default with override)

Call `AskUserQuestion` with two options. The default depends on the fit bucket from Step 2a:

| Fit bucket | Default option | Why |
|------------|----------------|-----|
| clear-fit  | **Skip enrichment** (default) | Bucket is obvious; enrichment rarely changes the verdict. |
| weak-fit   | **Enrich** (default) | This is where enrichment pays off — disambiguate the fit. |
| no-fit     | **Skip enrichment** (default) | Heading for `ignore` anyway; no point researching. |

Both options must always appear so the user can override the smart default. Phrase the question so the default is named, e.g. on a weak-fit clipping:

> "Enrich context bef

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ozlar34](https://github.com/ozlar34)
- **Source:** [ozlar34/claude-code-skills](https://github.com/ozlar34/claude-code-skills)
- **License:** MIT
- **Homepage:** https://github.com/ozlar34

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-ozlar34-claude-code-skills-triage
- Seller: https://agentstack.voostack.com/s/ozlar34
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
