Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
linux:
name: Linux
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14

- name: Install locked dependencies
run: bun ci

- name: Check
run: bun run check

macos:
name: macOS
if: ${{ github.event.repository.private == false }}
runs-on: macos-latest
steps:
- name: Check out repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14

- name: Install locked dependencies
run: bun ci

- name: Check
run: bun run check
125 changes: 125 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write
id-token: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
macos:
name: Verify on macOS
if: ${{ github.repository == '0xmrwn/papercuts' && github.event.repository.private == false }}
runs-on: macos-latest
steps:
- name: Check out repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14

- name: Install locked dependencies
run: bun ci

- name: Check
run: bun run check

release:
name: Publish
needs: macos
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14

- name: Set up Node and npm
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24
registry-url: https://registry.npmjs.org
package-manager-cache: false

- name: Install current npm publishing client
run: npm install --global npm@11.18.0

- name: Verify release identity
id: identity
shell: bash
run: |
package_version="$(bun -e 'import metadata from "./package.json" with { type: "json" }; console.log(metadata.version)')"
if [[ "$GITHUB_REF_NAME" != "v${package_version}" ]]; then
echo "Tag ${GITHUB_REF_NAME} does not match package version ${package_version}." >&2
exit 1
fi

git fetch origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "Release tag must point to a commit on main." >&2
exit 1
fi

echo "version=${package_version}" >> "$GITHUB_OUTPUT"

- name: Install locked dependencies
run: bun ci

- name: Check
run: bun run check

- name: Pack npm artifact
id: pack
shell: bash
run: |
mkdir release
tarball="$(npm pack --pack-destination release --json | node -e 'let input = ""; process.stdin.on("data", chunk => input += chunk); process.stdin.on("end", () => console.log(JSON.parse(input)[0].filename));')"
echo "path=release/${tarball}" >> "$GITHUB_OUTPUT"

- name: Verify exact npm artifact
env:
PAPERCUTS_TARBALL: ${{ steps.pack.outputs.path }}
run: bun run scripts/smoke-package.ts

- name: Publish to npm
env:
PACKAGE_VERSION: ${{ steps.identity.outputs.version }}
TARBALL: ${{ steps.pack.outputs.path }}
shell: bash
run: |
local_integrity="sha512-$(openssl dgst -sha512 -binary "$TARBALL" | openssl base64 -A)"
if published_integrity="$(npm view "papercuts@${PACKAGE_VERSION}" dist.integrity --json 2>/dev/null | node -e 'const fs = require("fs"); console.log(JSON.parse(fs.readFileSync(0, "utf8")));')"; then
if [[ "$published_integrity" != "$local_integrity" ]]; then
echo "papercuts@${PACKAGE_VERSION} already exists with different contents." >&2
exit 1
fi
echo "papercuts@${PACKAGE_VERSION} already contains this exact artifact; skipping publish."
else
npm publish "$TARBALL" --access public --provenance
fi

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
TARBALL: ${{ steps.pack.outputs.path }}
shell: bash
run: |
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release upload "$GITHUB_REF_NAME" "$TARBALL#npm package" --clobber
else
gh release create "$GITHUB_REF_NAME" "$TARBALL#npm package" --verify-tag --generate-notes --title "$GITHUB_REF_NAME"
fi
30 changes: 30 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Contributing

Papercuts is a personal tool shared in public, not a staffed project. Issues and pull requests are welcome, but a response or merge is never guaranteed.

For a substantial change, opening an issue first may save both of us from polishing incompatible ideas. Small fixes do not need ceremony.

## Development

Papercuts requires Bun 1.3.14 or newer.

```sh
bun install --frozen-lockfile
bun run check
```

Keep changes inside the existing boundaries:

- Reporting records one concise immutable occurrence and returns to the original task.
- Maintenance groups, disposes, and reconciles tickets; it does not repair underlying systems.
- Papercuts does not own issue-tracker authentication, configuration, or adapters.
- JSON output, schemas, cursor safety, and idempotent maintenance operations are public contracts.
- The npm package is source-only. Do not add generated builds or platform binaries to it.

If you are validating or improving Windows support, say exactly what you ran and on which Windows and Bun versions. Windows is currently untested, not intentionally unsupported.

## Pull requests

Keep the diff narrow, include tests for behavioral changes, and describe any contract or persisted-data impact. Do not commit local SQLite databases, generated binaries, or secrets.

By contributing, you agree that your contribution is licensed under the repository's MIT license.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 0xmrwn

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
113 changes: 92 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,72 @@
# Papercuts

Papercuts is a local agent-facing CLI for recording avoidable, operator-controlled friction during work and organizing it later without interrupting the original task.
A tiny local memory for the friction agents are otherwise too competent to complain about.

The ordinary-agent integration is one line:
I built Papercuts because an agent can lose a few seconds or a few minutes to a misleading command, recover, finish the task, and leave no trace that the next agent will hit the same thing. Each incident is small. The repetition and compound effect are not. The recovery looks like success, so the bad interface survives.

> If avoidable friction in something we control makes the correct path discoverable only through failed attempts, or forces an unnecessary workaround, handoff, or weaker verification, record one concise, secret-free occurrence with `papercuts report "<attempt; friction; recovery/impact>"`, then continue.
Papercuts records that moment without turning the original task into a retrospective. Reporting is one write-only command. Grouping, dismissing, and ticketing happen later, in a separate maintenance pass.

Reporting is write-only. Clustering, dispositions, and ticket reconciliation belong to the separately authored [maintenance skill](./skills/papercuts-maintenance/SKILL.md). Repair is outside both workflows.
> **Add this to `AGENTS.md`:** If avoidable friction in something we control makes the correct path discoverable only through failed attempts, or forces an unnecessary workaround, handoff, or weaker verification, record one concise, secret-free occurrence with `papercuts report "<attempt; friction; recovery/impact>"`, then continue.

## Installation
The original idea for collecting papercuts belongs to [Steve Ruiz](https://x.com/steveruizok), who shared his implementation and rationale in [this post](https://x.com/steveruizok/status/2075303919664734295).

> This is a tool I built for myself and decided to share as-is. There is no roadmap, support SLA, or promise that I will maintain it on anyone else's schedule—or at all. Issues, discussions, and pull requests are welcome; that is an invitation, not a service contract. Papercuts is tested on macOS and Linux. Windows is untested, not deliberately blocked.

## Install

Papercuts requires [Bun](https://bun.com/) 1.3.14 or newer. It is distributed as source, not as a platform-specific binary.

Run it without keeping it:

```sh
bun install --frozen-lockfile
bun link
bunx papercuts --version
```

Or install it globally:

```sh
bun install --global papercuts
papercuts --version
```

`bun link` registers the `papercuts` executable from this checkout in Bun's global bin directory.
Every GitHub release also carries the exact npm package tarball:

```sh
bun install --global https://github.com/0xmrwn/papercuts/releases/download/v0.2.0/papercuts-0.2.0.tgz
```

You can install a tagged checkout instead:

```sh
bun install --global github:0xmrwn/papercuts#v0.2.0
```

## Report a papercut

The `AGENTS.md` instruction above is the ordinary integration. That last word—continue—matters. Reporting is deliberately boring and write-only: record the papercut, then get back to work.

You can also try it directly:

```sh
papercuts report "A hidden working directory made the documented test command fail; using the nested path worked."
```

## Commands
Commands return JSON by default. Inputs and outputs are discoverable without prose archaeology:

```sh
papercuts report --schema --format json
papercuts inbox --schema --format json
```

## Maintenance is separate

Papercuts does not quietly turn every annoyance into a ticket. The ordinary workflow only records immutable occurrences. A maintainer—human or agent—later decides whether occurrences belong together, should be dismissed or deferred, or justify an external ticket.

The optional maintenance skill lives in [`skills/papercuts-maintenance`](https://github.com/0xmrwn/papercuts/tree/main/skills/papercuts-maintenance). Point your agent's existing skill installer at that directory, or copy it into the skill location that agent already uses. Installing the npm package includes the skill files but does not install or configure them behind your back.

The skill never activates implicitly: a user request or scheduled-task prompt must explicitly invoke `$papercuts-maintenance`. It uses whichever authenticated issue tracker the user and agent have already agreed to use. Papercuts does not own tracker authentication, adapters, or routing configuration.

## Command reference

```text
papercuts report <note>
Expand All @@ -35,26 +83,49 @@ papercuts occurrence defer --id <id>... --reason <reason>
papercuts consumer commit --consumer <id> --cursor <cursor>
```

All commands are non-interactive and default to JSON. Command inputs and outputs are discoverable with `papercuts <command> --schema --format json`.
Run `papercuts <command> --help` for command-level usage. Incur currently advertises some integration surfaces in root help that Papercuts intentionally refuses; MCP and generated-skill commands are not part of the supported interface.

Inbox and cluster-list responses are bounded to 50 items per stream by default and 200 maximum. Follow `page.nextOffset` until it is `null` before mutating maintenance state. A consumer cursor advances only after every occurrence through the target cursor has been clustered or explicitly dismissed or deferred.

Inbox and cluster-list responses are bounded to 50 items per stream by default and 200 maximum. Follow `page.nextOffset` until it is `null`; maintenance reads every page before mutating so offset pagination stays stable. The inbox target cursor remains the occurrence high-water mark and is committed only after all occurrences through it are handled.
Cluster membership cannot be removed after any consumer has committed past an occurrence. Use cluster merge for same-fix corrections and finish membership changes before committing.

Cluster membership cannot be removed after any consumer has committed past the occurrence. This preserves durable cursors; use cluster merge for same-fix corrections and finish membership changes before committing.
## Local data and privacy

## Local development
The CLI has no service and makes no network calls. It stores its SQLite database locally at:

```text
$XDG_DATA_HOME/papercuts/papercuts.sqlite3
~/.local/share/papercuts/papercuts.sqlite3 (when XDG_DATA_HOME is unset)
```

`PAPERCUTS_HOME` overrides the data directory. Papercuts creates a missing dedicated directory with mode `0700`, preserves the permissions of an existing directory, and creates the database with mode `0600`.

Each occurrence includes the note, timestamp, compact repository name, and best-effort runtime attribution: harness, model, reasoning effort, and session ID. Recognized harnesses are `codex`, `claude-code`, `claude-code-desktop`, and `claude-cowork`. Attribution comes from process markers and bounded reads of matching local agent state. Missing or malformed state leaves fields `null` and never blocks reporting. Transcript content and transcript paths are not stored.

These environment variables override local discovery:

```text
PAPERCUTS_HOME
PAPERCUTS_REPOSITORY
PAPERCUTS_HARNESS
PAPERCUTS_MODEL
PAPERCUTS_REASONING_EFFORT
PAPERCUTS_SESSION_ID
```

## Development

```sh
bun install
bun run papercuts -- report "A hidden cwd forced a corrected test path."
git clone https://github.com/0xmrwn/papercuts.git
cd papercuts
bun install --frozen-lockfile
bun run check
```

`PAPERCUTS_HOME` overrides the data directory. Papercuts creates a missing dedicated directory with mode `0700` but preserves the permissions of an existing directory. `PAPERCUTS_REPOSITORY` overrides the compact repository name captured with a report; otherwise Papercuts uses the current Git root's basename. The SQLite database uses mode `0600`, and its WAL sidecars stay beside it.

Each new occurrence also captures nullable `harness`, `model`, `reasoningEffort`, and `sessionId` fields. Papercuts recognizes Codex, Claude Code, Claude Code inside Claude Desktop, and Claude Desktop's separate Cowork mode as `codex`, `claude-code`, `claude-code-desktop`, and `claude-cowork`. It uses process markers and bounded reads of matching local session state; missing, malformed, or changing harness state leaves fields null and never blocks reporting. Cowork effort remains null unless its process or session state exposes a concrete level, and Claude child sessions deliberately leave model and effort null rather than borrowing parent values. Papercuts stores no transcript content or path.
`bun run check` runs the contract tests, builds and exercises the compiled binary, then packs, installs, and exercises the npm artifact in an isolated environment.

`PAPERCUTS_HARNESS`, `PAPERCUTS_MODEL`, `PAPERCUTS_REASONING_EFFORT`, and `PAPERCUTS_SESSION_ID` override their individual detected fields. They are an operator escape hatch for an unsupported or changing harness, not additional reporting arguments. The ordinary-agent integration line therefore remains unchanged.
The implementation is intentionally small: Bun, Incur, `bun:sqlite`, and no ORM. See [CONTEXT.md](https://github.com/0xmrwn/papercuts/blob/main/CONTEXT.md) for the precise domain language and [CONTRIBUTING.md](https://github.com/0xmrwn/papercuts/blob/main/CONTRIBUTING.md) before changing a contract.

The production stack is Bun, Incur, and `bun:sqlite`, with no ORM. Incur is used only for parsing, validation, schemas, JSON output, and packaging. Its MCP, generated-skill, and unused integration entry points are refused before framework dispatch, although Incur still displays them in root help.
## License

See [CONTEXT.md](./CONTEXT.md) for canonical domain language and the [maintenance skill](./skills/papercuts-maintenance/SKILL.md) for the organization and ticketing workflow.
[MIT](https://github.com/0xmrwn/papercuts/blob/main/LICENSE) © 2026 0xmrwn.
3 changes: 0 additions & 3 deletions docs/adr/0001-local-cli-with-separated-maintenance-skill.md

This file was deleted.

5 changes: 0 additions & 5 deletions docs/adr/0002-cursors-track-occurrence-handling.md

This file was deleted.

Loading