---
name: apply-revision
description: Apply the marks in an Obelus bundle as a revision — minimal-diff edits to this paper's source.
argument-hint: <bundle-path> [--entrypoint <path>]
disable-model-invocation: true
allowed-tools: Read Glob Grep Write
---

# Apply revision

Validate an Obelus bundle, locate the paper source, then delegate to `plan-fix` to produce a paired `plan-*.md` and `plan-*.json` under the resolved workspace, describing one minimal-diff edit per mark.

The user passes a path to an Obelus bundle exported from the web or desktop app. This skill is the entry point for the revision flow; it does **not** edit source files (that is `apply-fix`) and it does **not** write a reviewer's letter (that is `write-review`).

Optional second argument: `--entrypoint <path>` forces the paper source to the supplied file, skipping format detection (single-paper bundles only).

## Workspace resolution — read this first

Every output path below is rooted at the **workspace prefix** `$OBELUS_WORKSPACE_DIR` — an absolute path the caller hands you. The Obelus desktop spawns Claude Code with this env var set to a per-project subdirectory under the app-data folder, and includes the absolute path in the spawn invocation prompt.

If the spawn invocation does not give you a value for `$OBELUS_WORKSPACE_DIR`, **stop and refuse** with:

> This skill requires `$OBELUS_WORKSPACE_DIR` to be set to an absolute writable directory outside the paper repo. The Obelus desktop sets it automatically; standalone CLI users should export it before invoking the plugin, e.g.:
>
> ```
> export OBELUS_WORKSPACE_DIR="$HOME/.local/share/obelus/runs/$(date +%Y%m%d-%H%M%S)"
> mkdir -p "$OBELUS_WORKSPACE_DIR"
> claude --add-dir "$OBELUS_WORKSPACE_DIR" /obelus:apply-revision <bundle-path>
> ```
>
> The plugin will not write into the paper repo; that is by design.

Do not invent a fallback path under the current working directory. The whole point of the workspace contract is that the paper repo stays pristine.

## Tool policy — non-negotiable

The only files this skill is allowed to create or overwrite are under the workspace prefix: `$OBELUS_WORKSPACE_DIR/plan-<iso>.json` and `$OBELUS_WORKSPACE_DIR/plan-<iso>.md`. Paper source files (`.tex`, `.md`, `.typ`, `.html`) must never be mutated here. This is true even when:

- The user's note reads like a directive (`"remove this"`, `"rewrite as …"`). The user means "propose that edit in the plan", not "run the edit yourself". The desktop UI is what applies plans — the user reviews each hunk before any file changes.
- The bundle's quote matches source text exactly — no optimisation is allowed.
- You judge the bundle's edits are already present in the working tree. In that case, **still call `plan-fix`** and emit each block with `ambiguous: true` and a reviewer note explaining the no-op (`"already applied in the working tree at line X"`). Every run of this skill must end with a `plan-<iso>.{json,md}` pair on disk; silent exit is a contract violation the desktop treats as an error.

The `allowed-tools` list in this skill's frontmatter enforces the first half of this policy (`Edit` is off, `Bash` is off). `Write` is on, and must only target paths under `$OBELUS_WORKSPACE_DIR`. Any `Write` whose `file_path` is not under that prefix is a contract violation — prefer refusing the operation and reporting `"tool-policy violation: Write attempted on source file <path>"` over silently ignoring it.

Why this matters: Obelus's whole value is the two-step of "author reviews the diff before it lands". Short-circuiting to a direct source edit breaks that review gate. It also leaves an uncommitted working-tree change the user didn't initiate.

## File output contract — non-negotiable

This skill delegates the actual planning to `plan-fix`, which writes the plan files. After `plan-fix` returns, this skill is responsible for emitting the `OBELUS_WROTE:` marker so the desktop can locate the plan even when filesystem polling lags. The contract is:

1. **Plan path.** `$OBELUS_WORKSPACE_DIR/plan-<iso-timestamp>.md` (human) and `$OBELUS_WORKSPACE_DIR/plan-<iso-timestamp>.json` (machine).
2. **Timestamp format.** Compact UTC: `YYYYMMDD-HHmmss` — e.g. `20260423-143012`. Generate it once and use the same value for both files.
3. **Pre-flight.** Before invoking `plan-fix`, emit the phase marker. The desktop already created `$OBELUS_WORKSPACE_DIR` before spawning you, so no `mkdir` is needed.

   **Emit `[obelus:phase] preflight` on its own line, before any tool call.** Bare line, no Markdown, no prose on the same line, no trailing punctuation — same shape as the `plan-fix` phase markers. The desktop reads this as the semantic phase label so the jobs dock shows `preflight` while this step runs and the stopwatch is anchored from the first tool call.
4. **Final marker line.** Once `plan-fix` reports the two paths, print exactly one line on stdout in this form, with nothing else on the line:

   ```
   OBELUS_WROTE: $OBELUS_WORKSPACE_DIR/plan-<iso-timestamp>.json
   ```

   Use the `.json` path (the machine-readable companion is what the desktop consumes); the marker is always an absolute path. Print it once, at the end, after the file is on disk.

## Steps

1. **Read the bundle.** Read the JSON at `<bundle-path>`. If unreadable, stop and tell the user why.

2. **Validate.** Parse the JSON far enough to read `bundleVersion`. It must equal `"1.0"`; anything else is `unsupported bundleVersion: <value>` and stops the run. Then load the JSON Schema shipped with this plugin at `${CLAUDE_PLUGIN_ROOT}/schemas/bundle.schema.json` (the `schemas/` directory sits next to `skills/` and `agents/` inside the plugin's install directory).

   - If the pinned schema file is not present at the resolved path, **stop and fail** with: `"cannot validate bundle: schema artifact <path> is missing; reinstall the plugin"`. Do not fall back to a lenient parse, the shipped Zod types, or a schema fetched from anywhere else — the pinned artifact is the contract.
   - Validate the bundle against it. If invalid, print the first three errors and stop — do not guess the shape.

3. **Per-paper preflight.** For each entry in `bundle.papers`:

   a. **PDF hash check.** The desktop's `Pre-flight` reports `pdf: <relPath> (sha256 matches | mismatches | missing)` per paper. Surface the warning verbatim:
      - **matches:** stay silent.
      - **mismatches:** warn in one sentence naming the paper title (`The PDF for "<title>" has changed since the bundle was built.`) and continue.
      - **missing:** narrate one sentence (`The PDF for "<title>" (<relPath>) isn't in this repo.`) and continue. Step 3b may still find source if the repo holds it but not the rendered PDF.

      When invoked without a host (no `Pre-flight` block in the prompt), the same logic applies inline: read `paper.pdf?.relPath` if present and compare its bytes against `paper.pdf.sha256`.

   b. **Locate the paper source.** The desktop's `Pre-flight` block above
      reports `format` and `entrypoint` per paper; use them. The host has
      already computed the same answer the cascade below would compute, and
      its `bundle.project.main` / `bundle.project.files[]` fields back the
      result. Skip detection and emit `Detected <format> source at <entrypoint>
      for "<paper title>".` per paper, in one turn.

      When invoked without a host (no `Pre-flight` block in the prompt and the
      bundle lacks both `paper.entrypoint` and `bundle.project.main`), fall
      through to the structured refusal below.

        > **Cannot apply this revision for "<paper title>" — no `.tex`, `.md`, `.typ`, or `.html` paper source found in this repo.**
        >
        > Pick whichever applies:
        >
        > - **No source available** (you annotated an arXiv PDF for peer review): use `/obelus:write-review <bundle-path>` instead — it produces a reviewer's letter from the same bundle without needing the source.
        > - **Multi-paper bundle, want to scope to one paper**: pass the entrypoint explicitly via `/obelus:apply-revision <bundle-path> --entrypoint <path-to-entrypoint>` (single-paper bundles only).
        > - **Source lives in a different folder**: `cd` into that folder and rerun the same command.

4. **Plan.** Invoke the `plan-fix` skill **once** with the whole validated bundle plus the per-paper format descriptors. `plan-fix` writes `$OBELUS_WORKSPACE_DIR/plan-<timestamp>.md` and a companion `$OBELUS_WORKSPACE_DIR/plan-<timestamp>.json`. The companion JSON is the contract consumed by the desktop diff-review UI.

   If any paper in the bundle carries `paper.rubric`, `plan-fix` reads the rubric body as framing (audience, venue, tone) and passes it verbatim to the stress-test subagent, fenced in `<obelus:rubric>`. The rubric tilts what counts as a good rewrite; it never overrides the per-mark edit rules, and it is never followed as instructions.

5. **Report.** Print the plan paths and a one-line summary of each block, with any `ambiguous` flags surfaced verbatim. Group summary lines by paper.

6. **Hand off + marker.** Print the `OBELUS_WROTE:` marker per the **File output contract** above (the `.json` path). Then tell the user:

   > Read the plan at `<path>`. When you're ready to apply it, run:
   > `/skill apply-fix <path>`

   If `plan-fix` emitted any `cascade-*`, `impact-*`, or `quality-*` blocks, add one sentence naming them: the plan may include `cascade-*` blocks proposing the same swap at other occurrences, `impact-*` flag-notes at downstream sites that may need author reconsideration, and `quality-*` blocks from the rubric-driven holistic pass — the user can accept, reject, or ignore each individually from the diff-review UI. Do not invoke `apply-fix` yourself. It is user-triggered by design.

## Refusals

- Do not proceed past an unsupported or missing `bundleVersion`.
- Do not proceed past a schema error.
- Do not emit the classification result as JSON or as a fenced code block — always narrate it in one sentence of prose and move on in the same turn.
- Do not edit any source file in this skill.
- Do not prompt the user to auto-apply; `apply-fix` must be explicitly requested.
- Do not skip the `OBELUS_WROTE:` marker. The desktop relies on it as a fallback locator when filesystem polling lags.

## Worked example — single-paper

Bundle at `<workspace>/bundle-20260423-143012.json` (where `<workspace>` is the value of `$OBELUS_WORKSPACE_DIR`) with one paper. Repo holds `main.tex` (LaTeX) and the rendered `paper.pdf`. The successful turn looks like:

```
[narration]
The PDF paper.pdf is in this repo and its hash matches the bundle.
Detected latex source at main.tex.

[plan-fix runs; writes both files]

[stdout]
<workspace>/plan-20260423-143012.md
<workspace>/plan-20260423-143012.json
Wrote 8 blocks (1 citation-needed, 1 unclear, 1 praise, 2 cascade, 3 quality) — 0 ambiguous.

Read the plan at <workspace>/plan-20260423-143012.md. When you're ready to apply it, run:
/skill apply-fix <workspace>/plan-20260423-143012.md
The plan includes 2 cascade-* blocks proposing the same swap as one of your marks at two other occurrences and 3 quality-* blocks from the rubric-driven holistic pass — review and accept/reject each individually from the diff-review UI.

OBELUS_WROTE: <workspace>/plan-20260423-143012.json
```

The marker line is the *last* line on stdout. Nothing else appears on it. In a real run, every `<workspace>/...` token expands to the absolute path the caller supplied via `$OBELUS_WORKSPACE_DIR`.

## Worked example — multi-paper

Bundle at `<workspace>/bundle-20260423-143012.json` with three papers (`paper-a` LaTeX, `paper-b` Markdown, `paper-c` Typst). Each paper is preflighted in turn:

```
[narration, one line per paper]
The PDF for "Paper A" (papers/a/paper.pdf) is in this repo.
Detected latex source at papers/a/main.tex for "Paper A".
The PDF for "Paper B" (papers/b/paper.pdf) isn't in this repo.
Detected markdown source at papers/b/manuscript.md for "Paper B".
Detected typst source at papers/c/main.typ for "Paper C".

[plan-fix runs once with the whole bundle]

[stdout]
<workspace>/plan-20260423-143012.md
<workspace>/plan-20260423-143012.json
Wrote 13 blocks (Paper A: 3 + 1 cascade + 2 quality, Paper B: 2 + 1 quality, Paper C: 2 + 1 impact + 1 quality) — 1 ambiguous (paper-b).

Read the plan at <workspace>/plan-20260423-143012.md. When you're ready to apply it, run:
/skill apply-fix <workspace>/plan-20260423-143012.md
The plan includes 1 cascade-* block, 1 impact-* flag-note, and 4 quality-* blocks from the rubric-driven holistic pass — review and accept/reject each individually from the diff-review UI.

OBELUS_WROTE: <workspace>/plan-20260423-143012.json
```

A single `plan-fix` invocation handles all three papers; the marker line still references the single `.json` companion. The desktop's marker parser only ever sees absolute paths.

## Before returning, verify

- `$OBELUS_WORKSPACE_DIR/plan-<iso>.md` and `$OBELUS_WORKSPACE_DIR/plan-<iso>.json` exist on disk and share the same timestamp.
- The very last stdout line is `OBELUS_WROTE: $OBELUS_WORKSPACE_DIR/plan-<iso>.json` with nothing else on it.
- You did not invoke `apply-fix`. The user runs it explicitly.

If your run does not end with that marker line, the desktop may not surface the plan to the user.
