---
name: vault-clip
description: Clip a web URL into the Obsidian vault as a curator-compatible inbox proposal (article / paper / repo / docs / discussion templates; AI-extracted tldr/tags/key_claims/entities; dedup via vault url grep)
applies_to_agents: []
---

# Skill: vault-clip

Clip a web URL into the Obsidian vault as a curator-compatible `_inbox/`
proposal. v1.4 ships skill-only (Approach A per critic-protocol §4);
researcher auto-invocation is deferred to v1.5+ (WEBCLIP-future-01).

The pipeline: **fetch -> validate -> AI-extract (same-turn JSON) -> route
-> dedup -> write**. Output lands at `<vaultRoot>/Knowledge/_inbox/<slug>.md`
with the locked WEBCLIP-03 13-key frontmatter schema. Curator promotion
(`_inbox/` -> `Knowledge/Context|Patterns/`) is a **separate downstream
step**; this skill never invokes `gmd-vault-curator` (no skill-to-skill
recursion).

## Invocation

- **Direct (human):** `/gmd:vault-clip <url>` slash command (lands in plan
  31-04). Pipes through to this skill end-to-end.
- **Programmatic (agent via Skill tool):** input shape
  `{url: string, force?: boolean, ai_extract?: boolean, clipper_caller?: string}`.
  - `force` (default `false`) — overwrite an existing clip with the same
    canonical URL.
  - `ai_extract` (default `true`) — run the same-turn AI extraction step.
    Set `false` to skip and write a clip with empty `tldr/tags/key_claims/entities`
    fields.
  - `clipper_caller` — string identifying the caller; PII-redacted via
    `stripCredentialShapedTokens` before serialization.
- **Researcher auto-invocation: deferred to v1.5+ (WEBCLIP-future-01).**
  Per `.planning/specs/gmd-critic-protocol.md §4` Approach A lock, v1.4
  ships skill-only. v1.6-A and v1.7-A authors orchestrate explicit
  `/gmd:vault-clip <url>` calls in PLAN.md task lists; researchers do
  NOT auto-clip URLs they encounter.

## Component Gate

The skill respects `components.obsidian.enabled` from the GMD config. The
**first action** of `writeClip()` is `isEnabled('obsidian')`. When
disabled, the skill exits with:

```json
{ "status": "vault_disabled",
  "message": "enable components.obsidian.enabled in ~/.claude/get-magic-done/config.json" }
```

**No file is written.** This is structurally enforced by
`tests/invariants.test.js` Invariant I4 (the `isEnabled` call appears
textually before any `fs.writeFileSync` inside `writeClip`).

## Orchestration

The 7-step pipeline the calling agent walks:

1. **Component-gate check.** If `isEnabled('obsidian') === false`, return
   `{status: 'vault_disabled'}` and stop.
2. **Resolve vaultRoot** via `bin/lib/vault-project.js resolveVaultRoot(cwd)`.
3. **WebFetch the URL.** Use the agent's `WebFetch` tool. On HTTP error or
   persistent timeout, the calling agent passes the failure status into
   `validateFetchResult`, which returns
   `{ok:false, status:'fetch_failed', reason}` — skill exits with that
   envelope. **Single retry on timeout** (the agent re-invokes WebFetch
   once); persistent failure is fatal for this clip.
4. **Validate fetch.** On success, `validateFetchResult(markdown, url, started_at_ms, 200)`
   returns `{ok:true, metadata:{title,author,published}, captured_at, quality}`.
   `quality` is `'shallow'` if markdown length < 500 chars (paywall
   heuristic) else `'full'`.
5. **AI extraction (same-turn).** When `ai_extract:true` (default), the
   calling agent applies the LOCKED prompt template (next section) to the
   fetched markdown, **authoring strict JSON inline within its own turn**.
   Pass the JSON text to `validateExtractionJson`. On `malformed_json` or
   any schema violation, the calling agent re-authors **once**. On
   persistent failure, set `ai_extract: 'failed'` in the args; the file
   IS still written (the vault keeps the source) but with empty extracted
   fields and the failure flag in frontmatter.
6. **Route URL.** `routeUrl(url) -> {template, paywall}` over the locked
   7-pattern table (`arxiv/{abs,pdf}` -> paper, `github.com/<org>/<repo>`
   -> repo, `docs.*` + `*.readthedocs.io` -> docs, HN `/item*` + reddit
   `/r/*/comments/*` -> discussion, `substack` + `medium` -> article,
   bloomberg/ft/wsj -> article + paywall:true, default -> article).
7. **`writeClip(vaultRoot, args)`** handles dedup + body interpolation +
   file write. Returns the final envelope (`{status, path, dedup_match}`).

## AI Extraction Prompt

**Channel decision: SAME-TURN (option a) — LOCKED.** The calling agent
owns this step within its own turn (free, contextual, low-latency for
v1.4's direct-user invocation profile). The skill does NOT spawn a
delegated haiku subagent. The lib substrate (`lib/clip.js`) deliberately
exposes a clean JSON-author / JSON-validate boundary so a future v1.5+
refactor to the delegated channel only changes WHERE the JSON is
authored (this prompt-text site), not the schema or validation logic.

The calling agent applies this prompt verbatim to the cleaned WebFetch
markdown:

```
You are extracting structured fields from a web page captured via vault-clip.

INPUT: cleaned-markdown body of the page below.

OUTPUT (strict JSON, no prose, no code fences):
{
  "tldr": "<one sentence ≤240 chars summarizing the main claim>",
  "tags": ["3-5", "kebab-case", "tags"],
  "key_claims": ["2-4 load-bearing claims as short bullets"],
  "entities": {
    "people": [],
    "orgs": [],
    "tickers": [],
    "tools": []
  }
}

CONSTRAINTS:
- tldr ≤ 240 chars total
- tags: 3-5 items, kebab-case, lowercase, no whitespace
- key_claims: 2-4 items, each ≤ 200 chars
- entities: object with 0-4 array fields; each field optional; empty arrays allowed
- Output EXACTLY one JSON object, no surrounding text

PAGE BODY:
<source markdown here>
```

Pass the resulting JSON text to `validateExtractionJson(json_text)`. The
validator returns the FIRST violation reason in fixed order:
`malformed_json`, `schema_violation`, `tldr_too_long`, `tags_count_invalid`,
`key_claims_count_invalid`.

## Output Envelope

The skill returns a structured envelope:

```json
{
  "status": "ok | fetch_failed | already_clipped | ai_extract_failed | vault_disabled",
  "path": "Knowledge/_inbox/<slug>.md or null",
  "dedup_match": "<existing-path or null>",
  "timings": { "fetch_ms": 0, "extract_ms": 0, "write_ms": 0, "total_ms": 0 }
}
```

Built via `buildClipEnvelope(result, timings)` for shape consistency.

## Error Modes

| status               | when                                                         | file written? |
| -------------------- | ------------------------------------------------------------ | ------------- |
| `ok`                 | happy path — fetch + extract + dedup-miss + write            | yes           |
| `fetch_failed`       | HTTP 4xx/5xx, persistent timeout, or empty body              | no            |
| `already_clipped`    | dedup hit on `url:` frontmatter; `--force` not set           | no            |
| `ai_extract_failed`  | AI extraction malformed twice; calling agent set `ai_extract:'failed'` | yes (no extracted fields, flag set) |
| `vault_disabled`     | `components.obsidian.enabled` is `false`                     | no            |

## Curator Compatibility

The frontmatter authored at `Knowledge/_inbox/<slug>.md` is a valid input
for `gmd-vault-curator`'s `_inbox/` -> `Knowledge/{Context,Patterns}/`
promotion path. Specifically: it parses cleanly via
`bin/lib/vault-frontmatter.js parseFrontmatter` (the same parser the
curator uses).

**Promotion is a separate downstream step:** run `gmd-vault-curator
--promote` later to lift the file from `_inbox/` to its final home. **This
skill does NOT call `gmd-vault-curator` or `gmd-vault-maintain`** — that's
a critical-invariant violation (no skill-to-skill recursion). Test
Invariant I1 in `tests/invariants.test.js` enforces it structurally.

The 13-key WEBCLIP-03 schema deliberately **excludes** `type:` and
`summary:` fields — those are added at curator-promotion time to satisfy
qualityCheck Rules 1+3. The clipped file lands in `_inbox/` as a raw web
source for human or curator review.

## PII Discipline

Both `clipper_caller` and `tldr` fields run through
`stripCredentialShapedTokens` (regex `/[A-Za-z0-9_-]{20,}/g` -> `<redacted>`)
before frontmatter serialization AND body substitution. This mirrors the
TELM-06 invariant (`bin/lib/vault-telemetry.js` HIGH_ENTROPY_RE) — defense
in depth, since clipped pages may contain pasted API keys or tokens.

The Phase 32 audit (`audit-webclip-pipeline.sh`) asserts this end-to-end.

## Lib Surface

The load-bearing primitives the calling agent invokes (typically via
`Bash node -e "..."` from the slash command in plan 31-04):

- `validateFetchResult(markdown, url, started_at_ms, fetch_status)` —
  fetch envelope validator + metadata extraction.
- `validateExtractionJson(json_text)` — extraction schema validator.
- `writeClip(vaultRoot, args)` — orchestration centerpiece (gate, dedup,
  slug, frontmatter, body interpolation, paywall callout, file write).
- `buildClipEnvelope(result, timings)` — output envelope assembly.

These are pure-JS (zero npm deps, only Node built-ins + skill-local lib +
`bin/lib/{config,vault-frontmatter}.js`). Importing surface stays inside
this skill plus the two shared `bin/lib/` utilities.

## Tests

```bash
node skills/vault-clip/tests/url-route.test.js       # 20 cases
node skills/vault-clip/tests/slug.test.js            # 15 cases
node skills/vault-clip/tests/frontmatter.test.js     # 12 cases
node skills/vault-clip/tests/template-parity.test.js # 5 cases
node skills/vault-clip/tests/clip.test.js            # 22 cases (incl. LOAD-BEARING curator round-trip + component-gate + PII)
node skills/vault-clip/tests/invariants.test.js      # 4 architectural invariants
```

## Deferred Features

Out of v1.4 scope. Do NOT implement under this phase. Track each as a
distinct future-feature ticket if/when demand surfaces:

- **WEBCLIP-future-01: Researcher auto-invocation of `vault-clip`.**
  Hooks in `gmd-phase-researcher` and `gmd-project-researcher` that
  detect URLs in research prompts and auto-clip them. Locked OUT per
  critic-protocol §4 Approach A. Defer to v1.5+ if v1.6-A's manual
  orchestration proves friction-heavy.
- **Cross-project URL-history sharing.** Skill is per-vault; no global
  history.
- **Browser-extension parity / migration tooling.** The skill is
  agent-driven; we don't ship an extension.
- **Paywall-bypass mechanisms.** "Paywall-aware" means the template knows
  the source is paywalled (`paywall: true` flag + `> [!warning]` callout);
  we don't bypass paywalls (ToS territory).
- **Multi-language extraction.** English-only in v1.4.
- **Image / PDF / video ingestion.** Markdown-only in v1.4. Defer if
  demand surfaces.
