pi-papercuts
v0.4.0
Published
Agent complaint box for Pi/OMP: file friction into an append-only .papercuts.jsonl, then keep working.
Maintainers
Readme
pi-papercuts
Agent files one-line friction notes into .papercuts.jsonl and keeps working. Review the backlog later.
Port of treygoff24/papercuts (MIT) for Pi and OMP. Pure Node · Node 22+.
pi install npm:pi-papercuts
omp install npm:pi-papercuts
# from a checkout:
pi install ./packages/pi-papercuts
omp install ./packages/pi-papercutsUse
papercuts({ action: "add", text: "what broke + what would have prevented it", tags: ["tooling"], severity: "major" })
papercuts({ action: "list" })
papercuts({ action: "list", format: "md" })
papercuts({ action: "resolve", ids: ["pc_9f2c"], note: "fixed" })
papercuts({ action: "doctor" })
papercuts({ action: "schema" })
papercuts({ action: "prune" })| Action | What it does |
|--------|----------------|
| add | File a cut (text required). Optional tags, severity. Evidence is free-note or cmd/exit/stderr, not both. Alias log → add. |
| list | Open cuts by default. Filters: status, agent, tag, severity, limit, format (json | md). |
| resolve | Mark ids resolved (pc_ + ≥4 hex). Append-only. |
| prune | Archive resolved cuts; keep the working log lean. |
| doctor | Validate the log. |
| schema | Machine contract for agents. |
Severity: minor (default), major, blocker. New IDs hash a JSON tuple of timestamp, agent, text, severity and the sorted tag array. They retain the pc_ + 12-hex format; old logged IDs remain readable/resolvable but are not regenerated by the new encoding. Distinct tag boundaries and lone surrogates are preserved. Hash collisions are still possible with 48-bit IDs.
Dedupe applies to an identical ID in the working log, including the timestamp. Evidence is not part of identity. A later timestamp is a new cut, and pruning forgets working-log dedupe for archived cuts. Duplicate receipts return the first stored record, including its original evidence and tag order, never unsaved replacement fields. Unknown legacy severity values sort after the three known severity levels without dropping their records.
Interactive hosts show a compact themed call/result row -- expand for full text, tags, and log path.
Pi 0.99 programmatic callers receive the same versioned envelope in
structuredContent (validated by outputSchema); UI details and model-facing
text stay intact. Rejected usage, busy and I/O operations set native isError,
so nested execution does not report them as successful writes. The combined
tool includes mutations and does not claim to be read-only. Markdown lists retain
this programmatic envelope too. The required action and action-specific field
types are checked before storage; known null placeholders are absent, but unknown
keys are rejected even when null. TUI previews strip terminal controls and do not
split valid surrogate pairs.
Layout
index.js binds the host signature and error boundary. lib/contract.js owns wire
schemas/envelopes, lib/params.js narrows action-specific arguments, lib/actions.js
implements validated actions, and lib/render.js owns compact/expanded TUI output.
lib/worker-client.js manages lazy background execution and graceful shutdown;
lib/worker.js runs the unchanged synchronous actions serially off the UI thread.
lib/store.js remains the sole persistence boundary, including canonical-path locks,
append durability, tear healing, and archive-before-rewrite pruning.
Rendering
Renderers reuse their own previous Text component when freshly formatted text
is unchanged, preserving Pi's width/layout cache without caching theme colors.
Pi's self-rendering API also preserves the surrounding Box cache: padding,
pending/success/error backgrounds and click expansion match the default shell.
Each transcript row retains at most two frames, one per expansion state, so
collapsing and reopening a result preserves both layouts. If the call renderer
fails, its detached frame cannot swallow the host-visible error result. Failed
result rendering clears the old receipt before the host shows its fallback.
Changed arguments, nested result values, expansion, theme invalidation and resize
remain visible. Common-width resolve batches build one ordered prefix index per
call; longer legacy IDs still participate in ambiguity checks. The temporary
index retains only requested-prefix hits rather than unrelated cuts. Mixed-width
requests retain prefix scanning.
There is no persistent log cache: cooperating writes keep the full snapshot-to-
append lock, reads still observe current file contents, and acknowledgements still
require fsync. Each extension registration owns one lazy worker, so independent
sessions cannot shut down each other's execution. schema and input validation
stay local. The first storage call pays worker startup latency without blocking
on its I/O. Results are copied back to the host, so unbounded result payloads and
cold layout of very large expanded lists can still block a frame.
The worker is unreferenced when idle and drains accepted work on session_shutdown.
Already-aborted calls are rejected before dispatch; after dispatch, cancellation
waits for the transaction's receipt rather than interrupting a durable write.
Worker failures reject pending calls without replay: a write may have committed
before a lost receipt, so inspect the log before retrying. Later calls can restart
the worker. Per-request PAPERCUTS_FILE, PAPERCUTS_AGENT and PAPERCUTS_NOW
overrides are snapshotted before dispatch, along with the execution directory.
Storage
fileparamPAPERCUTS_FILE- nearest
.git→<root>/.papercuts.jsonl(exact basename only;foo.papercuts.jsonlis not the repo log) - else
~/.papercuts/log.jsonl
echo .papercuts.jsonl >> .gitignoreRelative file and PAPERCUTS_FILE overrides resolve against the active execution
directory, not the host process directory. Reads reject non-regular files without
waiting for a FIFO writer. Missing files remain empty backlogs. Pruning retains
the working log's basic permission bits even under a tighter umask; ownership,
ACLs, and extended attributes are not preserved by that replacement.
Optional: PAPERCUTS_AGENT, PAPERCUTS_NOW (tests).
With pi-deferred-context-engine, pin papercuts if you want it always active.
Tests
npm test --prefix packages/pi-papercutsHermetic Node 22 tests under tests/. Local bench tests/performance.mjs is not shipped.
No-claim boundaries
- Does not auto-detect failures -- the agent must call it (habit /
AGENTS.md). - Not a secret store. Size caps only; resolve does not erase cut text.
- Outside git, log goes under home unless
PAPERCUTS_FILEis set. - Log paths must be normal files, without hard-link aliases. Existing symlink targets are resolved before locking; dangling log symlinks are refused.
- All writers/pruners must run this version's locking protocol. Older versions or external appenders are not coordinated. Stop them before pruning with this version.
- Canonical log/archive paths ending in
.lock(case-insensitive) are reserved and refused, including symlinks into that namespace. This prevents event data from being deleted as lock cleanup. - Writes acquire exclusive
<canonical-log>.lockfiles; add/resolve hold the lock from snapshot through append, and prune also locks its archive. Contention returns a retryablebusyerror without acknowledging a write. Locks contain a PID/time and are not stolen automatically: after a crash, confirm the writer has stopped before manually removing a stale lock. - Prune archives before replacing the working log. A crash can leave duplicate archive events or a temp file; duplicate events are harmless to
fold. File contents are flushed, but directory durability after power loss and network filesystems are not guaranteed. - Appends separate an unterminated tail from the new event; malformed old lines remain on disk until explicit pruning.
- Invalid UTF-8 (strict per-record decode, not replacement characters), invalid/missing IDs, malformed tag arrays, and wrong-typed
ts/agentor cuttext/severityfields are torn. These text fields must be strings when present; sparse legacy records, unknown severity strings, and opaque extra metadata remain supported.doctorflags torn records; list/resolve skip them without rewriting those lines. Explicitpruneremoves them. - Text and tag byte caps keep the original UTF-16 slice, including lone surrogates. A Buffer round-trip would replace those code units and change content-addressed IDs.
- Open-cut list order is severity, then RFC3339 timestamp (not lexical), then remaining text. Truncated list leftovers are
total - shown.length.
More: residual risks.
License
MIT · AdityaVG13/pi-stack
