{"_id":"@akaule/spec-review","name":"@akaule/spec-review","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@akaule/spec-review","version":"0.1.0","description":"Local, repo-agnostic tool to review markdown specs with anchored comments that an agent can apply and resolve.","type":"module","bin":{"spec-review":"bin/spec-review.js"},"engines":{"node":">=20"},"publishConfig":{"access":"public"},"scripts":{"start":"node bin/spec-review.js","test":"node --test"},"keywords":["spec","review","markdown","comments","cli"],"repository":{"type":"git","url":"git+https://github.com/akakaule/spec-review.git"},"homepage":"https://github.com/akakaule/spec-review#readme","bugs":{"url":"https://github.com/akakaule/spec-review/issues"},"author":{"name":"Alvin Kaule","url":"akakaule"},"license":"MIT","dependencies":{"markdown-it":"^14.1.0"},"gitHead":"e29c706fc88cc8033988fc5f77ed0f3d8810f606","_id":"@akaule/spec-review@0.1.0","_nodeVersion":"26.1.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-/+6qwyxp2h0aUEXxCHwQpSH4MtAj/1jE6zK4LTuejsetA6B/SAVTecLTg6b+JlPBB/DRRSIX/Hc/a4oogz/SFg==","shasum":"50c6edb15e2db60b04ca7a20d1b19851e9d8c890","tarball":"https://registry.npmjs.org/@akaule/spec-review/-/spec-review-0.1.0.tgz","fileCount":16,"unpackedSize":75248,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCID6OaMS0cz0VomQNvHtbIfumbDDmul1Tg5aBeHj3TllOAiEAwhctKO8L2t0lkAfreHeQ625c1ZZNoYUgt29qxf1Uv5Y="}]},"_npmUser":{"name":"akaule","email":"alvin.kaule@gmail.com"},"directories":{},"maintainers":[{"name":"akaule","email":"alvin.kaule@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/spec-review_0.1.0_1780296954482_0.1774650710540946"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-01T06:55:54.205Z","0.1.0":"2026-06-01T06:55:54.593Z","modified":"2026-06-01T06:55:54.855Z"},"maintainers":[{"name":"akaule","email":"alvin.kaule@gmail.com"}],"description":"Local, repo-agnostic tool to review markdown specs with anchored comments that an agent can apply and resolve.","homepage":"https://github.com/akakaule/spec-review#readme","keywords":["spec","review","markdown","comments","cli"],"repository":{"type":"git","url":"git+https://github.com/akakaule/spec-review.git"},"author":{"name":"Alvin Kaule","url":"akakaule"},"bugs":{"url":"https://github.com/akakaule/spec-review/issues"},"license":"MIT","readme":"# spec-review\n\nA small, **repo-agnostic** spec-review loop with two halves that ship together:\n\n1. **The review tool (web)** — a localhost HTML page that renders a markdown spec, lets a\n   reviewer attach comments to highlighted passages, and persists them to a stable JSON\n   sidecar next to the spec. Distributed on **npm**: run `npx @akaule/spec-review docs/specs` in any repo.\n2. **The `apply-review` agent skill** — a [Claude Code](https://docs.claude.com/en/docs/claude-code)\n   skill that reads those sidecars, edits the spec to address each `open` comment, and marks it\n   `resolved`/`wontfix`. Distributed as a **Claude Code plugin** (`.claude-plugin/` + `skills/`):\n   install once and run `/apply-review` in any repo. See [skills/apply-review/SKILL.md](skills/apply-review/SKILL.md).\n\nBoth work against **any folder in any git repo** with zero per-project configuration.\n\n> Implements [spec 019 — Spec Review Tool](docs/specs/019-spec-review-tool/spec.md) (tool) and\n> [spec 020 — Apply Review](docs/specs/020-apply-review/spec.md) (skill).\n\n## Install the agent skill (Claude Code plugin)\n\n```\n/plugin marketplace add akakaule/spec-review\n/plugin install spec-review@spec-review\n```\n\nThen `/apply-review [path]` applies open review comments to your specs. The skill calls\n`npx @akaule/spec-review` only if you ask it to open the web UI — the two halves compose but have no\nhard dependency on each other.\n\n## Quick start\n\n```bash\n# from any repo\nnpx @akaule/spec-review docs/specs\n\n# or globally\nnpm i -g @akaule/spec-review\nspec-review docs/specs\n```\n\nThe CLI starts a server on `127.0.0.1`, prints a URL containing a per-run token, and\nopens your browser. The sidebar lists every discovered spec; click one to read it,\nselect a passage to comment, and your feedback is saved next to the spec.\n\n## CLI\n\n```\nspec-review [path] [options]\n```\n\n| Argument / flag | Default | Meaning |\n| --- | --- | --- |\n| `path` | `./docs/specs` if it exists, else `.` | Folder to scan. |\n| `--glob <pattern>` | `**/spec.md` | Which markdown files are specs. |\n| `--port <n>` | an open port | Port to listen on. |\n| `--no-open` | (off) | Don't auto-launch the browser. |\n| `--read-only` | (off) | Render specs but disallow writing comments. |\n| `-h`, `--help` | | Show help. |\n\n## How it works\n\n- **Rendering.** Markdown is rendered server-side to **sanitized** HTML with\n  `markdown-it` (`html:false`, so embedded `<script>`/raw HTML is escaped, never\n  executed). Headings get stable slugs; top-level blocks carry `data-source-*`\n  line attributes used to map a browser selection back to source text.\n- **Anchored comments.** A comment stores the selected `quote`, the nearest\n  `heading`, bounded `prefix`/`suffix` context, and an advisory `offsetHint`. On\n  every reopen the tool **re-anchors** by matching the quote and disambiguating\n  with prefix/suffix/offset. The result is recorded as `anchorState`:\n  `anchored` | `orphaned` | `ambiguous` — a non-anchored comment is surfaced\n  separately and **never** attached to an unrelated passage.\n- **Live reload.** File changes to a spec or its sidecar push an update to the UI\n  (Server-Sent Events).\n\n## Security model\n\n\"localhost-only\" is a **reachability** control, not the write-authorization control:\nany page in your browser can reach `http://127.0.0.1:<port>`. So writes require **all** of:\n\n1. **Per-run token** — minted on startup, embedded in the served URL, sent as\n   `X-Spec-Review-Token` (or `Authorization: Bearer`). Never persisted.\n2. **Origin/Referer** matches the served origin.\n3. **Host** is `127.0.0.1:<port>` / `localhost:<port>` (closes DNS-rebinding).\n4. **Content-Type** is `application/json` (blocks simple cross-site form posts).\n\nReads also validate Host and require the token. No permissive CORS headers are set.\n\n## The sidecar (`<name>.review.json`)\n\nEach reviewed markdown file gets a sidecar next to it, named by replacing the `.md`\nextension with `.review.json` (`spec.md` → `spec.review.json`). This keeps sidecars\nunique when a directory holds several reviewed files under a broad `--glob`.\n\n```jsonc\n{\n  \"version\": 1,            // schema version (evolves additively)\n  \"rev\": 7,                // monotonic data revision (optimistic concurrency)\n  \"spec\": \"spec.md\",       // the markdown file this sidecar pairs with\n  \"comments\": [\n    {\n      \"id\": \"c_ab12cd\",                 // immutable, never changes\n      \"anchor\": {\n        \"heading\": \"FR-033\",\n        \"quote\": \"MUST dead-letter via IMessageContext.DeadLetter\",\n        \"prefix\": \"…up to 32 chars before…\",\n        \"suffix\": \"…up to 32 chars after…\",\n        \"offsetHint\": 1240\n      },\n      \"body\": \"This is ambiguous — does it also abandon?\",\n      \"author\": \"alvin\",\n      \"status\": \"open\",                 // open | resolved | wontfix\n      \"anchorState\": \"anchored\",        // anchored | orphaned | ambiguous (tool-maintained)\n      \"createdAt\": \"2026-05-29T10:00:00Z\",\n      \"updatedAt\": \"2026-05-29T10:00:00Z\",\n      \"thread\": [ { \"author\": \"alvin\", \"body\": \"…\", \"createdAt\": \"…\" } ],\n      \"resolution\": null                // { by, note, at } when resolved/wontfix\n    }\n  ]\n}\n```\n\nThe file is **atomically** written (temp + rename) with stable key ordering, so it\ndiffs cleanly and is git-mergeable. **Commit it** for shared, reviewable-in-PR\nfeedback, or gitignore it for ephemeral local review — the default is committable.\n\n## Agent-apply contract\n\nA separate agent run (out of band) closes the loop. This is implemented by the\n[`apply-review` skill](skills/apply-review/SKILL.md) (spec 020) — invoke it with\n`/apply-review [path]`. Given `spec.md` + its `spec.review.json`, the agent MUST:\n\n1. Read all comments with `status: \"open\"`.\n2. For each, **recompute** the anchor against the *current* `spec.md` via `anchor.quote`,\n   disambiguated by `prefix`/`suffix`/`offsetHint`. `anchorState` is advisory only — the live\n   recompute decides editability.\n3. Edit `spec.md` to address the comment.\n4. Set `status` to `\"resolved\"` (or `\"wontfix\"`) and populate `resolution: { by, note, at }`.\n5. Leave `id`, `anchor`, `anchorState`, `body`, `author`, `createdAt`, and `thread`\n   unchanged. `anchorState` is **tool-maintained** — the agent reads it but never writes it.\n\nIf the anchor is not uniquely locatable (the `quote` is missing, moved, or now duplicated), the\nagent MUST NOT edit a guessed passage — it leaves the comment `open` and reports it for the human\nto re-place. If the anchor is located but the correct edit is undeterminable, it marks the comment\n`wontfix` with a reason. **It never writes `thread`.**\n\nIf the agent writes the sidecar directly on disk, it MUST preserve all fields it does\nnot own and **increment `rev`** so a running tool's concurrency check stays coherent.\n\n### Worked example\n\nBefore — `spec.review.json`:\n\n```json\n{ \"version\": 1, \"rev\": 3, \"spec\": \"spec.md\", \"comments\": [\n  { \"id\": \"c_ab12cd\", \"anchor\": { \"heading\": \"FR-033\", \"quote\": \"MUST dead-letter the message\", \"prefix\": \"The tool \", \"suffix\": \" safely.\", \"offsetHint\": 1240 },\n    \"body\": \"Does it also abandon?\", \"author\": \"alvin\", \"status\": \"open\", \"anchorState\": \"anchored\",\n    \"createdAt\": \"2026-05-29T10:00:00Z\", \"updatedAt\": \"2026-05-29T10:00:00Z\", \"thread\": [], \"resolution\": null } ] }\n```\n\nThe agent finds `MUST dead-letter the message` in `spec.md`, rewrites it to\n`MUST dead-letter the message (without abandoning it)`, then writes the sidecar with\n`rev` bumped and the comment resolved:\n\n```json\n{ \"version\": 1, \"rev\": 4, \"spec\": \"spec.md\", \"comments\": [\n  { \"id\": \"c_ab12cd\", \"anchor\": { \"heading\": \"FR-033\", \"quote\": \"MUST dead-letter the message\", \"prefix\": \"The tool \", \"suffix\": \" safely.\", \"offsetHint\": 1240 },\n    \"body\": \"Does it also abandon?\", \"author\": \"alvin\", \"status\": \"resolved\", \"anchorState\": \"anchored\",\n    \"createdAt\": \"2026-05-29T10:00:00Z\", \"updatedAt\": \"2026-05-29T11:00:00Z\", \"thread\": [],\n    \"resolution\": { \"by\": \"agent\", \"note\": \"Clarified: dead-letters without abandoning.\", \"at\": \"2026-05-29T11:00:00Z\" } } ] }\n```\n\n## Development\n\n```bash\nnpm install      # one dependency: markdown-it\nnpm test         # node:test suite (unit + server integration)\nnpm start -- .   # run against this repo\n```\n\n## Known limitations (v1)\n\n- **Sub-block inline anchoring.** Selecting *part* of a rendered sentence works when\n  the selected text matches the source verbatim. Heavily formatted inline spans\n  (e.g. selecting across `**bold**`) may not map back to source and are reported as\n  unable to anchor — select plainer prose in that case.\n- Single reviewer per running instance; no multi-user/real-time collaboration.\n- No VCS-host (GitHub/GitLab) PR-comment integration.\n","readmeFilename":"README.md","_rev":"1-00e0d7ea97014408b545f0d2a9f50754"}