stikfix 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ No cloud. No accounts. No sign-up. Everything stays on `127.0.0.1` — **your co
28
28
  ## How it feels to use
29
29
 
30
30
  1. Click **Enter Review Mode** on any page.
31
- 2. Drop a **sticky note** — free-floating, or click an element to anchor it. Anchored notes auto-capture the CSS selector, computed styles, `outerHTML`, the bounding box, an auto-highlighted screenshot, and the React component name. No describing required.
31
+ 2. Drop a **sticky note** — free-floating, or click an element to anchor it. Anchored notes auto-capture the CSS selector, computed styles, `outerHTML`, the bounding box, an auto-highlighted screenshot, and the React component name. No describing required. Want to point at something exactly? **Draw an arrow, box, or circle right on a screenshot** and it rides along with the note.
32
32
  3. Tell your agent **"read my notes."** It reads the fresh `.md` files, makes the fixes, and **writes a reply back into each note** — so the pin turns **green ✓** right on the page with a one-line "here's what I changed" (no fix is lost in a chat log). Anything ambiguous turns **amber** with the agent's clarifying question.
33
33
  4. Glance at the pins (or open the **notes panel** to filter/search/jump), drop a few more, repeat. Pins update live as the agent works — no refresh. Your UI gets tighter every loop.
34
34
 
@@ -36,7 +36,7 @@ No cloud. No accounts. No sign-up. Everything stays on `127.0.0.1` — **your co
36
36
 
37
37
  ### Windows: one-click installer (recommended)
38
38
 
39
- 1. Download **`stikfix-setup-1.6.0.exe`** from the [latest release](https://github.com/omernesh/stikfix/releases/latest).
39
+ 1. Download the latest **`stikfix-setup-<version>.exe`** from the [latest release](https://github.com/omernesh/stikfix/releases/latest).
40
40
  2. Run it. It needs **administrator rights** — it writes a browser policy so the extension can install itself.
41
41
  3. Pick a setup type:
42
42
  - **Complete** — installs the host, force-installs the extension into every Chromium browser it finds (Chrome/Edge/Brave), sets the host to run on Windows login, and adds a desktop shortcut. Fully automatic — no terminal.
@@ -49,6 +49,17 @@ The installer ships **`stikfix-host.exe`**, a self-contained binary — no Node.
49
49
 
50
50
  For advanced use or troubleshooting, the same binary takes subcommands directly: `stikfix-host.exe serve --root <dir>`, `stikfix-host.exe doctor` (prints the health checklist), `stikfix-host.exe register` / `stikfix-host.exe uninstall`.
51
51
 
52
+ #### Staying up to date
53
+
54
+ stikfix keeps itself current with **no reinstalling by hand**:
55
+
56
+ - **Extension** — auto-updates silently. The installer registers a self-hosted update manifest via enterprise policy, so Chrome/Edge/Brave pull new versions on their own, exactly like a Web Store extension.
57
+ - **Host** — checks GitHub for a newer release on startup and every ~6 hours. When one is available, the **system-tray icon** shows an *update available* balloon and an **Update Stikfix (vX.Y.Z)** item in its right-click menu. Click it and stikfix downloads the new installer, **verifies its SHA-256**, and runs it (one Windows permission prompt) to replace and restart the host — your notes folder and settings are preserved.
58
+
59
+ The **system-tray icon** is also your at-a-glance host status: green when the host is running, and its menu lets you open the notes folder, stop the host, or apply a pending update.
60
+
61
+ > Already on an older build? Install the latest `stikfix-setup-<version>.exe` once; from then on, every future version offers itself from the tray with a single click.
62
+
52
63
  ### Developer path (macOS, Linux, or from source on Windows)
53
64
 
54
65
  Everything the Windows installer automates above, you can also do by hand — this is the path for macOS/Linux, or for building and loading the extension from source.
@@ -95,12 +106,14 @@ Open your app, click **Enter Review Mode**, and start dropping notes. The first
95
106
  - **Free notes** — a draggable post-it you can drop anywhere on the page. Captures URL, title, timestamp, viewport, and a screenshot.
96
107
  - **Element notes** — click any element and the note auto-captures a robust unique CSS selector ([`@medv/finder`](https://github.com/antonmedv/finder)), curated computed styles, truncated `outerHTML`, bounding box, `data-*`, accessibility role/label, the best-effort **React component name**, and an **auto element-highlight screenshot** showing exactly which element you meant.
97
108
  - **Region / marquee capture** — the camera tool dims the page, gives you a crosshair, and lets you drag a rectangle. stikfix hides its own UI, captures, and crops the screenshot DPR-correctly. Stack multiple per note; each is a deletable thumbnail.
109
+ - **Annotation drawing** — mark up exactly what you mean instead of describing it. The pencil tool freezes the current view as a screenshot and drops you into a drawing canvas with **arrow, line, rectangle, circle, and freehand pen** tools, a color palette, and S/M/L thickness. Your drawing is flattened onto the screenshot and attached to the note, so your agent sees the annotated image — pixel-for-pixel.
98
110
  - **Persistent on-page pins** — notes stay visible as pins on the page across reloads. View, edit, and delete them in place — backed by host-side CRUD over the localhost relay. Overlapping pins fan out automatically so dense pages stay readable.
99
111
  - **Two-way status, on the page** — pins reflect the loop: **unread** (yellow), **flagged** (amber — the agent needs you to clarify, with its question on hover), **resolved** (green ✓ — fixed, with the agent's reply on hover). Resolved notes stay visible so you can verify the fix; archived (`read`) notes disappear.
100
112
  - **Notes panel** — a chip-toggled list of every note: counts by status, filter chips, text search, and click-to-jump that scrolls right to the pin. Flip **All pages** to browse every note across the project, not just the current page.
101
113
  - **Live updates** — while Review Mode is on, pins and the panel refresh on their own (~4s, only when the tab is visible) as the agent writes replies and resolves notes. No manual reload.
102
114
  - **Per-origin project routing** — the first note on a new site opens an OS folder picker; after that, every tab routes to the right project's `notes/` folder automatically. No per-note picking.
103
115
  - **Cross-browser host** — one `npx stikfix init` registers Chrome and Edge in a single pass. On Windows, the one-click installer does this (and the browser-side install) for you.
116
+ - **Self-updating** — the browser extension auto-updates via enterprise policy; the Windows host checks GitHub and offers a **SHA-256-verified, one-click update** from its system-tray icon (which also shows live running/stopped status). See [Staying up to date](#staying-up-to-date).
104
117
  - **The `review-notes` AI skill** — the portable agent half of the loop (see below).
105
118
 
106
119
  ## How notes reach your AI
@@ -109,14 +122,18 @@ Every note becomes a markdown file in your project's `notes/` folder, named `<se
109
122
 
110
123
  The **review-notes** skill is the agent half of the loop. It works through your unread notes in serial order, applies each fix, then writes a short **`reply`** into the note and marks it **`resolved`** (the pin turns green ✓ on the page). Re-running is always idempotent — resolved, flagged, and archived notes are skipped.
111
124
 
112
- **Claude Code (project-local):**
125
+ **Claude Code:** installation is now **automatic** — both the Windows installer's "Install the review-notes skill for Claude Code" task and `npx stikfix init` drop the skill into the user-level `~/.claude/skills/review-notes/SKILL.md`, so it's available in *every* Claude Code project with no manual copy. Pass `npx stikfix init --no-skill` to skip it; `npx stikfix uninstall` removes it.
113
126
 
114
- ```bash
115
- mkdir -p .claude/skills/review-notes
116
- cp /path/to/stikfix/skill/SKILL.md .claude/skills/review-notes/SKILL.md
117
- ```
127
+ Fallbacks (both optional):
128
+
129
+ - **Project-local install** — to pin the skill to one repo instead of user-wide:
130
+
131
+ ```bash
132
+ mkdir -p .claude/skills/review-notes
133
+ cp /path/to/stikfix/skill/SKILL.md .claude/skills/review-notes/SKILL.md
134
+ ```
118
135
 
119
- **Any other folder-reading agent (Cursor, Codex, etc.):** point it at [`skill/SKILL.md`](skill/SKILL.md) — it's plain markdown, no Claude-specific bits. For example: *"follow the instructions in skill/SKILL.md."*
136
+ - **Any other folder-reading agent (Cursor, Codex, etc.):** point it at [`skill/SKILL.md`](skill/SKILL.md) — it's plain markdown, no Claude-specific bits. For example: *"follow the instructions in skill/SKILL.md."*
120
137
 
121
138
  **Just say the word.** Any of these kicks it off:
122
139
 
@@ -2,8 +2,8 @@
2
2
  "use strict";
3
3
 
4
4
  // bin/stikfix.ts
5
- var import_node_fs2 = require("node:fs");
6
- var import_node_path3 = require("node:path");
5
+ var import_node_fs3 = require("node:fs");
6
+ var import_node_path4 = require("node:path");
7
7
  var import_node_os2 = require("node:os");
8
8
  var import_node_util = require("node:util");
9
9
 
@@ -415,6 +415,35 @@ function teardownHost(opts = {}) {
415
415
  var import_node_crypto2 = require("node:crypto");
416
416
  var STABLE_EXTENSION_ID = "ccdfmbhdcafhmnnnfjpbhgebfkfgjgca";
417
417
 
418
+ // bin/skill-install.ts
419
+ var import_node_fs2 = require("node:fs");
420
+ var import_node_path3 = require("node:path");
421
+ function installReviewNotesSkill(homeDir, skillContent) {
422
+ const dir = (0, import_node_path3.join)(homeDir, ".claude", "skills", "review-notes");
423
+ const path = (0, import_node_path3.join)(dir, "SKILL.md");
424
+ try {
425
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
426
+ (0, import_node_fs2.writeFileSync)(path, skillContent, { encoding: "utf8" });
427
+ return { ok: true, path };
428
+ } catch (err) {
429
+ return { ok: false, path, error: err instanceof Error ? err.message : String(err) };
430
+ }
431
+ }
432
+ function removeReviewNotesSkill(homeDir) {
433
+ const path = (0, import_node_path3.join)(homeDir, ".claude", "skills", "review-notes", "SKILL.md");
434
+ try {
435
+ if ((0, import_node_fs2.existsSync)(path)) {
436
+ (0, import_node_fs2.rmSync)(path, { force: true });
437
+ return { removed: true, path };
438
+ }
439
+ } catch {
440
+ }
441
+ return { removed: false, path };
442
+ }
443
+
444
+ // skill/SKILL.md
445
+ var SKILL_default = '# review-notes\r\n\r\nProcess unread stikfix review notes in serial order \u2014 read each note, apply the\r\nrequested code fix, then update the note\'s status so the extension can surface the\r\nresult on the page. This replaces the screenshot-paste-describe ping-pong of UI\r\nreview with a durable, file-based, iterative loop: a developer drops notes on a live\r\npage, saves them to disk via the stikfix host, then tells you to "read my notes" \u2014\r\nyou fix the code and write a structured reply back into the note, idempotently,\r\nwithout any manual copy-paste.\r\n\r\n## When to use\r\n\r\n- The user says "read my notes", "process review notes", "fix sticky notes", "run\r\n review-notes", or "what notes do I have"\r\n- There is a `notes/` directory in the project (or an explicit path was given) that\r\n may contain unread `.md` note files written by the stikfix extension\r\n\r\n## When NOT to use\r\n\r\n- The user wants to view notes without acting on them (read-only inspection)\r\n- The user has not set up the stikfix host and extension (no `notes/` directory)\r\n- You are operating outside the project repository where notes live\r\n\r\n## Preconditions\r\n\r\n- Notes directory: `./notes` relative to the project root, unless the user supplies an\r\n explicit path argument (e.g. "read my notes in /path/to/project/notes")\r\n- You must be able to read, write, and rename files inside that directory\r\n- The `yaml` library (`yaml@2.9.0`) is available in the project if you need to\r\n programmatically parse or reserialize frontmatter; alternatively, parse the YAML\r\n block manually as described in the steps below\r\n\r\n## Git-sync mode (optional)\r\n\r\nstikfix has an opt-in **git-sync mode**: the owner can enable a "Sync notes to\r\ngit" toggle (per project, off by default) that makes the host automatically\r\n`git add`/`git commit`/`git push` each captured note (pathspec-limited to\r\n`notes/`, so it never touches the owner\'s code changes). This lets notes\r\ncaptured on one computer show up \u2014 via `git pull` \u2014 on another computer where\r\nyou run this skill.\r\n\r\n- **Before Step 1 (discovering unread notes):** if the project is a git\r\n repository, run `git pull` (or `git pull --rebase`) so you see any notes\r\n pushed from other machines before you list `notes/`. If the pull fails, or\r\n the project is not a git repo, just continue with whatever notes are on\r\n disk locally \u2014 never block the run on this.\r\n- **After you resolve or flag a note:** the host only auto-commits *new* note\r\n captures \u2014 your own edits to a note\'s frontmatter (setting\r\n `status: resolved`, adding `reply:`, etc.) are plain disk writes the host\r\n does not know about and will not commit or push. So, only when the project\r\n is a git repo AND git-sync is in use, after writing your frontmatter\r\n update(s) also stage, commit, and push just the notes directory:\r\n\r\n ```\r\n git add -- notes/ && git commit -m "stikfix: resolve NNNN" && git push\r\n ```\r\n\r\n Treat this as best-effort: if the push fails (no network, no remote,\r\n auth issue), leave the change committed locally and mention it in your\r\n summary rather than retrying repeatedly. In pure local mode (no git-sync)\r\n do nothing git-related \u2014 the file writes described in the rest of this\r\n document are already the whole job.\r\n\r\n## Note frontmatter schema\r\n\r\nEvery note file starts with a YAML frontmatter block. The fields written by the\r\nextension at creation time are:\r\n\r\n```\r\n---\r\nid: 1\r\ncreated: "2026-01-01T12:00:00.000Z"\r\nmode: element # or: free\r\nurl: "https://example.com/page"\r\ntitle: "Page Title"\r\nviewport:\r\n width: 1280\r\n height: 800\r\n dpr: 1\r\nselector: "#submit-btn" # element mode only\r\nreact_component: "SubmitButton" # element mode, optional\r\nrect:\r\n x: 100\r\n y: 200\r\n width: 80\r\n height: 30\r\nscreenshots:\r\n - "0001-20260101-120000+1.png"\r\nstatus: unread\r\n---\r\n```\r\n\r\nFree-mode notes omit `selector`, `react_component`, and `rect`, and instead include\r\n`note_position: {x, y}`.\r\n\r\nThe AI review loop may add or update the following fields. **Preserve every existing\r\nfield; only add or update `status`, `reply`, and `fixed_in`.**\r\n\r\n| Field | Type | Who sets it | Meaning |\r\n|-------|------|-------------|---------|\r\n| `status` | string | AI | Lifecycle state \u2014 one of `unread`, `flagged`, `resolved`, `read` (see below) |\r\n| `reply` | string | AI | One-line message shown to the developer in the extension UI on hover |\r\n| `fixed_in` | string | AI | Optional commit hash or PR ref where the fix landed (set when `status: resolved`) |\r\n\r\n### Status values and their meaning\r\n\r\n| `status` | Set by | Visual in extension | File renamed? | Meaning |\r\n|----------|--------|---------------------|---------------|---------|\r\n| `unread` | Extension (at creation) | Default pin colour | No | Not yet processed |\r\n| `flagged` | AI | Amber pin; `reply` text on hover | No | Ambiguous \u2014 needs developer clarification |\r\n| `resolved` | AI | Green pin with \u2713; `reply` text on hover | No | AI fixed the issue; developer should verify on page |\r\n| `read` | AI (archive step) | Hidden \u2014 no pin shown | Yes \u2192 `*.read.md` | Archived / dismissed; developer has acknowledged |\r\n\r\n## Steps\r\n\r\n1. **Discover unread notes**\r\n\r\n List all files in the notes directory. Keep only filenames that end in `.md` AND\r\n do NOT end in `.read.md` \u2014 both patterns end with `.md`, so the exclusion must be\r\n explicit. In pseudocode:\r\n\r\n ```\r\n unread = files.filter(f => f.endsWith(\'.md\') && !f.endsWith(\'.read.md\'))\r\n unread.sort() // ascending lexicographic\r\n ```\r\n\r\n The filename format is `<serial>-<YYYYMMDD-HHmmss>.md` where `<serial>` is a\r\n 4-digit zero-padded integer (e.g. `0001`, `0042`). Because every serial is\r\n zero-padded to exactly 4 digits, ascending lexicographic sort equals serial order\r\n \u2014 process notes from lowest serial to highest.\r\n\r\n If there are no unread notes, report "no unread notes" and stop. Do not process\r\n `*.read.md` files \u2014 they are already done.\r\n\r\n2. **Process each note in serial order**\r\n\r\n For each unread note filename (lowest serial first):\r\n\r\n a. **Read the file and parse frontmatter**\r\n\r\n First, short-circuit on already-handled notes. After parsing the frontmatter,\r\n check the `status` field BEFORE doing any work:\r\n\r\n - `status: read` \u2192 skip this note (it is archived; normally it is also renamed\r\n to `*.read.md` and excluded in Step 1, but skip defensively if you encounter\r\n it).\r\n - `status: flagged` \u2192 skip this note. A flagged note was already judged\r\n ambiguous on a prior run and is awaiting human clarification. Do NOT\r\n re-process it and do NOT append another `> flagged:` blockquote \u2014 re-flagging\r\n duplicates the reason line on every run. A flagged note is intentionally NOT\r\n renamed (it stays a `.md` file so it remains visible), so it WILL reappear in\r\n the Step 1 listing; the `status: flagged` check here is what makes the re-run\r\n idempotent for it.\r\n - `status: resolved` \u2192 skip this note. The fix has been applied on a prior run;\r\n the note is visible in the extension so the developer can verify it on the\r\n page. Do NOT re-process or re-rename it.\r\n\r\n Only continue to the remaining sub-steps (b\u2013e) when `status` is `unread`. This\r\n mirrors the `classifyNote` helper (`lib/review-notes.ts`), which returns\r\n `\'read\'`, `\'flagged\'`, or `\'resolved\'` as skip outcomes and only\r\n `\'fixable\'`/`\'text-only\'` as actionable.\r\n\r\n Parse the frontmatter by extracting the text between the first `---` pair and\r\n passing it to a YAML parser.\r\n\r\n Derive the note\'s serial from `filename.slice(0, 4)` \u2014 NEVER from the `id`\r\n frontmatter field. The `id` field is stored as an integer and loses its leading\r\n zeros (the integer `1` is not the string `"0001"`). The filename prefix always\r\n carries the authoritative zero-padded serial.\r\n\r\n The text after the closing `---\\n` is the note body. The first non-blank line\r\n of the body is the developer\'s instruction \u2014 the comment they typed into the\r\n stikfix note UI. Read it as the actionable request.\r\n\r\n b. **Read element context (element mode only)**\r\n\r\n If `mode` is `element`, the body contains a `## Element context` section\r\n immediately after the developer comment. Use this section to locate the\r\n relevant code. It looks like:\r\n\r\n ```markdown\r\n ## Element context\r\n\r\n - **Selector:** `#submit-btn`\r\n - **React component:** `SubmitButton`\r\n - **Tag / role:** `button` / `button`\r\n - **Text:** Submit\r\n - **Rect:** x=100 y=200 w=80 h=30\r\n\r\n ### Computed styles (curated)\r\n | prop | value |\r\n |------|-------|\r\n | display | inline-flex |\r\n | color | rgb(255,255,255) |\r\n\r\n ### outerHTML (truncated)\r\n ```html\r\n <button id="submit-btn" class="btn-primary">Submit</button>\r\n ```\r\n ```\r\n\r\n The selector, React component name, tag, text, computed styles, and outerHTML\r\n together give you enough context to locate the exact element in the codebase\r\n without guessing. Use `grep`, a file search, or your knowledge of the project\r\n structure to find the relevant source file(s).\r\n\r\n c. **Open screenshots (if available and present on disk)**\r\n\r\n The frontmatter `screenshots` field is an array of filenames. These filenames\r\n are relative to the **notes directory**, not the project root. Resolve each\r\n screenshot path by joining it with the notes directory path:\r\n\r\n ```\r\n screenshotAbsPath = join(notesDir, screenshotFilename)\r\n ```\r\n\r\n If `screenshots` is non-empty AND the referenced PNG files exist on disk, open\r\n them with vision when the text description or element context alone is\r\n insufficient to understand the issue.\r\n\r\n If a referenced screenshot file does not exist on disk, emit exactly one line:\r\n\r\n ```\r\n WARN: <filename> not found \u2014 proceeding text-only\r\n ```\r\n\r\n Then continue processing the note as a text-only note. A missing screenshot is\r\n NOT ambiguous \u2014 the developer instruction and element context remain actionable.\r\n Do not flag the note or skip it because of a missing screenshot.\r\n\r\n d. **Apply the fix**\r\n\r\n Using all available context \u2014 the developer instruction, element context\r\n (selector, computed styles, outerHTML), and any screenshots \u2014 apply the\r\n requested fix to the relevant source file(s) in the project.\r\n\r\n If the instruction is genuinely ambiguous even after reading the element\r\n context and any screenshots \u2014 for example, "make this better" with no\r\n actionable target and no element context \u2014 do NOT attempt a fix. Instead:\r\n\r\n - Update the frontmatter: set `status` from `unread` to `flagged`, and add a\r\n `reply:` field containing a short, one-line clarification question directed at\r\n the developer (e.g. `reply: "Which colour should the button be \u2014 brand blue or\r\n red?"`)\r\n - Optionally also append a blockquote line to the end of the note body for\r\n human readability:\r\n ```\r\n > flagged: <brief reason why the instruction is ambiguous>\r\n ```\r\n (The canonical machine-readable signal is the `reply:` frontmatter field; the\r\n blockquote is supplementary.)\r\n - Leave the filename unchanged (do NOT rename to `.read.md` or any other name)\r\n - Skip to the next note\r\n\r\n The extension renders flagged pins in amber and shows the `reply` text on hover,\r\n so the developer sees exactly what question to answer.\r\n\r\n For all other cases (clear instruction, missing-screenshot text-only, element\r\n mode with context), proceed with the fix and continue to step (e).\r\n\r\n e. **Mark the note as resolved \u2014 ONLY after a successful fix**\r\n\r\n The frontmatter update is the LAST action, performed only after the fix has been\r\n successfully applied. This ordering is the core reliability guarantee: if the fix\r\n fails or is interrupted, the note stays `unread` and will be retried on the next\r\n run.\r\n\r\n **Update the frontmatter:** Parse the YAML block, set `status: resolved`, add a\r\n `reply:` field with a one-line description of what was done, and optionally add\r\n `fixed_in:` with the commit hash or PR ref. Preserve every other existing field.\r\n\r\n ```\r\n rawFm = content between the first --- pair\r\n fm = yaml.parse(rawFm)\r\n fm[\'status\'] = \'resolved\'\r\n fm[\'reply\'] = \'Renamed Submit \u2192 Save Changes in Header.tsx\' // what you did\r\n fm[\'fixed_in\'] = \'abc1234\' // optional: commit hash or PR ref\r\n newFmBlock = \'---\\n\' + yaml.stringify(fm) + \'---\\n\'\r\n newContent = newFmBlock + bodyAfterFrontmatter\r\n writeFile(mdPath, newContent)\r\n ```\r\n\r\n The project\'s `yaml` library (`yaml@2.9.0`, import `{ parse, stringify } from\r\n \'yaml\'`) handles colons and quotes in URL values correctly \u2014 do not hand-roll\r\n YAML serialization.\r\n\r\n **Do NOT rename the file.** A resolved note stays as `<serial>-<timestamp>.md`\r\n so it remains visible in the extension as a green pin with a \u2713. The developer\r\n must be able to see the fix confirmation on the page before dismissing the note.\r\n\r\n **Archive / dismiss (separate, later step):** Only when the developer has\r\n acknowledged a resolved note (or explicitly wants to dismiss a note) should the\r\n note be archived. Archiving means:\r\n\r\n - Set `status: read` in the frontmatter\r\n - Rename the file: `readPath = mdPath.replace(/\\.md$/, \'.read.md\')`\r\n\r\n This two-step approach (status:read + rename to *.read.md) is belt-and-suspenders:\r\n the rename is the primary skip signal on the next run (excluded by the glob in\r\n Step 1); the `status: read` frontmatter field lets other tools (like\r\n `listAnnotations` in the host) still list the note while knowing it is archived.\r\n The archive step is NOT done automatically after fixing \u2014 it is a separate,\r\n developer-triggered action.\r\n\r\n3. **Report a terse summary**\r\n\r\n After processing all notes, report:\r\n\r\n ```\r\n Processed N notes: M resolved (fix applied), K flagged (needs clarification), J skipped (resolved/read).\r\n ```\r\n\r\n If any notes were resolved, list their filenames and the `reply` text (what was\r\n done) so the developer knows what to verify on the page.\r\n\r\n If any notes were flagged, list their filenames and the `reply` question so the\r\n developer knows what to clarify.\r\n\r\n## After the pass\r\n\r\nOn the next run:\r\n\r\n- `resolved` notes: still present as `.md` files (not renamed), skipped by the\r\n `status: resolved` check in Step 2a. They remain visible on the page as green pins\r\n until the developer archives them.\r\n- `flagged` notes: skipped by the `status: flagged` check in Step 2a. They remain\r\n visible on the page as amber pins until the developer answers the question and the\r\n note is re-opened for processing.\r\n- `read` notes: excluded by the Step 1 glob (renamed to `*.read.md`) and skipped\r\n defensively by the `status: read` check if encountered anyway.\r\n\r\nA re-run on a fully-processed directory does no fixing work and reports either "no\r\nunread notes" (all `unread` notes have been resolved or flagged) or that only\r\npreviously-flagged notes remain. This is the idempotency guarantee \u2014 re-running never\r\nre-applies a fix, never re-resolves an already-resolved note, and never appends a\r\nduplicate `> flagged:` blockquote.\r\n\r\n## Forbidden patterns (do not do these)\r\n\r\n- **Glob `*.md` without excluding `*.read.md`** \u2014 `.read.md` files end in `.md`; a\r\n naive glob will reprocess already-archived notes\r\n- **Derive serial from the `id` frontmatter field** \u2014 the integer `id` loses zero-\r\n padding; always use `filename.slice(0, 4)`\r\n- **Set `status: read` on a successful fix** \u2014 a successful fix must set\r\n `status: resolved` (not `read`). `read` is the archive state, set only after the\r\n developer has acknowledged the resolved note. Getting this wrong hides the green pin\r\n before the developer can verify the fix.\r\n- **Rename the file after a fix** \u2014 resolved notes are NOT renamed. Renaming to\r\n `*.read.md` is the archive step, not the fix-completion step.\r\n- **Rename before fix** \u2014 for the archive step (if triggered), write the updated\r\n status first; rename is always the last action\r\n- **Treat a missing screenshot as ambiguous** \u2014 warn once and proceed text-only;\r\n only an unclear developer instruction triggers the flagged path\r\n- **Overwrite existing frontmatter fields** \u2014 always parse the YAML block, update\r\n only `status`, `reply`, and `fixed_in`, and reserialize. Never clobber `id`,\r\n `created`, `url`, `selector`, or any other field the extension wrote.\r\n- **Make network or host calls to the stikfix host** \u2014 this skill is disk-only with\r\n respect to stikfix itself; do not call the stikfix host HTTP API, do not POST to\r\n any endpoint. (The local `git` CLI \u2014 `pull`/`add`/`commit`/`push` \u2014 is explicitly\r\n permitted, and expected in git-sync mode; see "Git-sync mode (optional)" above.\r\n That is a plain local git operation, not a call to the stikfix host.)\r\n';
446
+
418
447
  // bin/stikfix.ts
419
448
  var { values, positionals } = (0, import_node_util.parseArgs)({
420
449
  allowPositionals: true,
@@ -424,7 +453,8 @@ var { values, positionals } = (0, import_node_util.parseArgs)({
424
453
  port: { type: "string" },
425
454
  browser: { type: "string" },
426
455
  startup: { type: "boolean" },
427
- "no-startup": { type: "boolean" }
456
+ "no-startup": { type: "boolean" },
457
+ "no-skill": { type: "boolean" }
428
458
  },
429
459
  strict: false
430
460
  });
@@ -467,8 +497,8 @@ function resolveStartupChoice(forceOn, forceOff, isTTY) {
467
497
  }
468
498
  return false;
469
499
  }
470
- var CONFIG_DIR2 = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".config", "stikfix");
471
- var CONFIG_PATH2 = (0, import_node_path3.join)(CONFIG_DIR2, "config.json");
500
+ var CONFIG_DIR2 = (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config", "stikfix");
501
+ var CONFIG_PATH2 = (0, import_node_path4.join)(CONFIG_DIR2, "config.json");
472
502
  if (subcommand === "init") {
473
503
  const rawRoot = values["root"];
474
504
  const rawExtId = values["extension-id"];
@@ -482,38 +512,38 @@ if (subcommand === "init") {
482
512
  const isFirefox = browser === "firefox";
483
513
  const extensionId = rawExtId && typeof rawExtId === "string" ? rawExtId : isFirefox ? DEFAULT_GECKO_ID : STABLE_EXTENSION_ID;
484
514
  const port = rawPort ? parseInt(rawPort, 10) : void 0;
485
- const root = (0, import_node_path3.resolve)(rootArg);
486
- const name = (0, import_node_path3.basename)(root);
487
- const notesDir = (0, import_node_path3.join)(root, "notes");
488
- const hostEntryPath = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "src", "index.js"));
489
- (0, import_node_fs2.mkdirSync)(CONFIG_DIR2, { recursive: true });
515
+ const root = (0, import_node_path4.resolve)(rootArg);
516
+ const name = (0, import_node_path4.basename)(root);
517
+ const notesDir = (0, import_node_path4.join)(root, "notes");
518
+ const hostEntryPath = (0, import_node_path4.resolve)((0, import_node_path4.join)(__dirname, "src", "index.js"));
519
+ (0, import_node_fs3.mkdirSync)(CONFIG_DIR2, { recursive: true });
490
520
  const config = { root, name, notesDir, hostEntry: hostEntryPath, nodePath: process.execPath };
491
- (0, import_node_fs2.writeFileSync)(CONFIG_PATH2, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
492
- const hostBinPath = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "stikfix-native.cjs"));
521
+ (0, import_node_fs3.writeFileSync)(CONFIG_PATH2, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
522
+ const hostBinPath = (0, import_node_path4.resolve)((0, import_node_path4.join)(__dirname, "stikfix-native.cjs"));
493
523
  try {
494
524
  registerNativeHost({ extensionId, hostBinPath, browser });
495
525
  } catch (err) {
496
526
  console.error("stikfix init: failed to register native host:", String(err));
497
527
  process.exit(1);
498
528
  }
499
- const projectRoot = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "..", ".."));
529
+ const projectRoot = (0, import_node_path4.resolve)((0, import_node_path4.join)(__dirname, "..", ".."));
500
530
  const firefoxOutputCandidates = [
501
- (0, import_node_path3.join)(projectRoot, ".output", "firefox-mv2"),
502
- (0, import_node_path3.join)(projectRoot, ".output", "firefox-mv3")
531
+ (0, import_node_path4.join)(projectRoot, ".output", "firefox-mv2"),
532
+ (0, import_node_path4.join)(projectRoot, ".output", "firefox-mv3")
503
533
  ];
504
- const chromeOutputDir = (0, import_node_path3.join)(projectRoot, ".output", "chrome-mv3");
505
- const browserOutputDir = isFirefox ? firefoxOutputCandidates.find((d) => (0, import_node_fs2.existsSync)(d)) ?? firefoxOutputCandidates[1] : chromeOutputDir;
534
+ const chromeOutputDir = (0, import_node_path4.join)(projectRoot, ".output", "chrome-mv3");
535
+ const browserOutputDir = isFirefox ? firefoxOutputCandidates.find((d) => (0, import_node_fs3.existsSync)(d)) ?? firefoxOutputCandidates[1] : chromeOutputDir;
506
536
  const icoCandidates = [
507
- (0, import_node_path3.join)(projectRoot, "public", "icon", "stikfix.ico"),
508
- (0, import_node_path3.join)(browserOutputDir, "icon", "stikfix.ico")
537
+ (0, import_node_path4.join)(projectRoot, "public", "icon", "stikfix.ico"),
538
+ (0, import_node_path4.join)(browserOutputDir, "icon", "stikfix.ico")
509
539
  ];
510
540
  const pngCandidates = [
511
- (0, import_node_path3.join)(browserOutputDir, "icon", "128.png"),
512
- (0, import_node_path3.join)(projectRoot, "public", "icon", "128.png"),
513
- (0, import_node_path3.join)(projectRoot, "assets", "icon", "128.png")
541
+ (0, import_node_path4.join)(browserOutputDir, "icon", "128.png"),
542
+ (0, import_node_path4.join)(projectRoot, "public", "icon", "128.png"),
543
+ (0, import_node_path4.join)(projectRoot, "assets", "icon", "128.png")
514
544
  ];
515
545
  const iconCandidates = process.platform === "win32" ? [...icoCandidates, ...pngCandidates] : pngCandidates;
516
- const iconPath = iconCandidates.find((p) => (0, import_node_fs2.existsSync)(p));
546
+ const iconPath = iconCandidates.find((p) => (0, import_node_fs3.existsSync)(p));
517
547
  const launcherResult = createLauncherFiles({
518
548
  hostEntryPath,
519
549
  root,
@@ -568,18 +598,18 @@ if (subcommand === "init") {
568
598
  if (isFirefox) {
569
599
  console.log(" 1. Load the extension (temporary add-on):");
570
600
  console.log(" about:debugging#/runtime/this-firefox \u2192 Load Temporary Add-on\u2026");
571
- console.log(" Pick: " + (0, import_node_path3.resolve)((0, import_node_path3.join)(browserOutputDir, "manifest.json")));
601
+ console.log(" Pick: " + (0, import_node_path4.resolve)((0, import_node_path4.join)(browserOutputDir, "manifest.json")));
572
602
  } else {
573
603
  console.log(" 1. Load the extension (unpacked):");
574
604
  console.log(" chrome://extensions \u2192 Developer mode ON \u2192 Load unpacked");
575
- console.log(" Folder: " + (0, import_node_path3.resolve)(browserOutputDir));
605
+ console.log(" Folder: " + (0, import_node_path4.resolve)(browserOutputDir));
576
606
  }
577
607
  console.log("");
578
608
  console.log(" 2. Start the backend \u2014 double-click the desktop launcher:");
579
609
  if (process.platform === "win32") {
580
610
  const lnkPath = launcherResult.written.find((p) => p.endsWith(".lnk")) ?? "";
581
611
  const batchPath = launcherResult.written.find((p) => p.endsWith(".bat")) ?? "";
582
- if (lnkPath && (0, import_node_fs2.existsSync)(lnkPath)) {
612
+ if (lnkPath && (0, import_node_fs3.existsSync)(lnkPath)) {
583
613
  console.log(' Desktop shortcut: "Stikfix Host" (icon on your Desktop)');
584
614
  } else if (batchPath) {
585
615
  console.log(" Batch file: " + batchPath);
@@ -600,6 +630,17 @@ if (subcommand === "init") {
600
630
  console.log("");
601
631
  console.log(" Extension ID: " + extensionId);
602
632
  console.log("");
633
+ if (values["no-skill"] === true) {
634
+ console.log(" Skill: skipped \u2014 review-notes skill not installed (--no-skill).");
635
+ } else {
636
+ const skillResult = installReviewNotesSkill((0, import_node_os2.homedir)(), SKILL_default);
637
+ if (skillResult.ok) {
638
+ console.log(" \u2713 Installed the review-notes skill for Claude Code \u2192 " + skillResult.path);
639
+ } else {
640
+ console.error("stikfix init: could not install review-notes skill: " + skillResult.error + " (copy skill/SKILL.md manually)");
641
+ }
642
+ }
643
+ console.log("");
603
644
  console.log("To keep the host up-to-date:");
604
645
  console.log(" npx --yes stikfix@latest init --root " + root);
605
646
  } else if (subcommand === "uninstall") {
@@ -611,15 +652,21 @@ if (subcommand === "init") {
611
652
  if (teardown.startupError) {
612
653
  console.error("stikfix uninstall: error removing startup entry:", teardown.startupError);
613
654
  }
655
+ const skillRemoval = removeReviewNotesSkill((0, import_node_os2.homedir)());
614
656
  console.log("stikfix: native host unregistered.");
615
657
  console.log(" manifest removed");
616
658
  console.log(" launcher files removed");
617
659
  console.log(" startup entry removed");
618
660
  console.log(" config removed");
661
+ console.log(
662
+ skillRemoval.removed ? " review-notes skill removed" : " review-notes skill not present (nothing to remove)"
663
+ );
619
664
  } else {
620
- console.error("Usage: npx stikfix <init|uninstall> [--root <dir>] [--browser <chrome|firefox>] [--extension-id <id>] [--port <port>]");
665
+ console.error("Usage: npx stikfix <init|uninstall> [--root <dir>] [--browser <chrome|firefox>] [--extension-id <id>] [--port <port>] [--no-skill]");
666
+ console.error("");
667
+ console.error(" init Register the native host, write config, and install the review-notes skill");
668
+ console.error(" uninstall Remove the native host manifest, launchers, config, and review-notes skill");
621
669
  console.error("");
622
- console.error(" init Register the native host and write config");
623
- console.error(" uninstall Remove the native host manifest, launchers, and config");
670
+ console.error(" --no-skill (init) Skip installing the review-notes Claude Code skill");
624
671
  process.exit(1);
625
672
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stikfix",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Pin sticky notes on any page — your AI reads them.",
5
5
  "license": "MIT",
6
6
  "author": "Omer Nesher <omernesher@gmail.com>",
@@ -36,6 +36,7 @@
36
36
  "dist/host/stikfix-init.cjs",
37
37
  "dist/host/stikfix-native.cjs",
38
38
  "public/icon",
39
+ "skill/SKILL.md",
39
40
  "README.md",
40
41
  "LICENSE"
41
42
  ],
@@ -44,7 +45,7 @@
44
45
  },
45
46
  "scripts": {
46
47
  "dev": "wxt",
47
- "build:host-bin": "npx esbuild bin/stikfix.ts --platform=node --format=cjs --bundle \"--external:node:*\" --outfile=dist/host/stikfix-init.cjs && npx esbuild host/src/native-host.ts --platform=node --format=cjs --bundle \"--external:node:*\" --outfile=dist/host/stikfix-native.cjs",
48
+ "build:host-bin": "npx esbuild bin/stikfix.ts --platform=node --format=cjs --bundle \"--external:node:*\" \"--loader:.md=text\" --outfile=dist/host/stikfix-init.cjs && npx esbuild host/src/native-host.ts --platform=node --format=cjs --bundle \"--external:node:*\" --outfile=dist/host/stikfix-native.cjs",
48
49
  "build": "wxt build && tsc -p tsconfig.host.json && npm run build:host-bin",
49
50
  "build:firefox": "wxt build -b firefox && tsc -p tsconfig.host.json && npm run build:host-bin",
50
51
  "host": "node dist/host/src/index.js",
package/skill/SKILL.md ADDED
@@ -0,0 +1,361 @@
1
+ # review-notes
2
+
3
+ Process unread stikfix review notes in serial order — read each note, apply the
4
+ requested code fix, then update the note's status so the extension can surface the
5
+ result on the page. This replaces the screenshot-paste-describe ping-pong of UI
6
+ review with a durable, file-based, iterative loop: a developer drops notes on a live
7
+ page, saves them to disk via the stikfix host, then tells you to "read my notes" —
8
+ you fix the code and write a structured reply back into the note, idempotently,
9
+ without any manual copy-paste.
10
+
11
+ ## When to use
12
+
13
+ - The user says "read my notes", "process review notes", "fix sticky notes", "run
14
+ review-notes", or "what notes do I have"
15
+ - There is a `notes/` directory in the project (or an explicit path was given) that
16
+ may contain unread `.md` note files written by the stikfix extension
17
+
18
+ ## When NOT to use
19
+
20
+ - The user wants to view notes without acting on them (read-only inspection)
21
+ - The user has not set up the stikfix host and extension (no `notes/` directory)
22
+ - You are operating outside the project repository where notes live
23
+
24
+ ## Preconditions
25
+
26
+ - Notes directory: `./notes` relative to the project root, unless the user supplies an
27
+ explicit path argument (e.g. "read my notes in /path/to/project/notes")
28
+ - You must be able to read, write, and rename files inside that directory
29
+ - The `yaml` library (`yaml@2.9.0`) is available in the project if you need to
30
+ programmatically parse or reserialize frontmatter; alternatively, parse the YAML
31
+ block manually as described in the steps below
32
+
33
+ ## Git-sync mode (optional)
34
+
35
+ stikfix has an opt-in **git-sync mode**: the owner can enable a "Sync notes to
36
+ git" toggle (per project, off by default) that makes the host automatically
37
+ `git add`/`git commit`/`git push` each captured note (pathspec-limited to
38
+ `notes/`, so it never touches the owner's code changes). This lets notes
39
+ captured on one computer show up — via `git pull` — on another computer where
40
+ you run this skill.
41
+
42
+ - **Before Step 1 (discovering unread notes):** if the project is a git
43
+ repository, run `git pull` (or `git pull --rebase`) so you see any notes
44
+ pushed from other machines before you list `notes/`. If the pull fails, or
45
+ the project is not a git repo, just continue with whatever notes are on
46
+ disk locally — never block the run on this.
47
+ - **After you resolve or flag a note:** the host only auto-commits *new* note
48
+ captures — your own edits to a note's frontmatter (setting
49
+ `status: resolved`, adding `reply:`, etc.) are plain disk writes the host
50
+ does not know about and will not commit or push. So, only when the project
51
+ is a git repo AND git-sync is in use, after writing your frontmatter
52
+ update(s) also stage, commit, and push just the notes directory:
53
+
54
+ ```
55
+ git add -- notes/ && git commit -m "stikfix: resolve NNNN" && git push
56
+ ```
57
+
58
+ Treat this as best-effort: if the push fails (no network, no remote,
59
+ auth issue), leave the change committed locally and mention it in your
60
+ summary rather than retrying repeatedly. In pure local mode (no git-sync)
61
+ do nothing git-related — the file writes described in the rest of this
62
+ document are already the whole job.
63
+
64
+ ## Note frontmatter schema
65
+
66
+ Every note file starts with a YAML frontmatter block. The fields written by the
67
+ extension at creation time are:
68
+
69
+ ```
70
+ ---
71
+ id: 1
72
+ created: "2026-01-01T12:00:00.000Z"
73
+ mode: element # or: free
74
+ url: "https://example.com/page"
75
+ title: "Page Title"
76
+ viewport:
77
+ width: 1280
78
+ height: 800
79
+ dpr: 1
80
+ selector: "#submit-btn" # element mode only
81
+ react_component: "SubmitButton" # element mode, optional
82
+ rect:
83
+ x: 100
84
+ y: 200
85
+ width: 80
86
+ height: 30
87
+ screenshots:
88
+ - "0001-20260101-120000+1.png"
89
+ status: unread
90
+ ---
91
+ ```
92
+
93
+ Free-mode notes omit `selector`, `react_component`, and `rect`, and instead include
94
+ `note_position: {x, y}`.
95
+
96
+ The AI review loop may add or update the following fields. **Preserve every existing
97
+ field; only add or update `status`, `reply`, and `fixed_in`.**
98
+
99
+ | Field | Type | Who sets it | Meaning |
100
+ |-------|------|-------------|---------|
101
+ | `status` | string | AI | Lifecycle state — one of `unread`, `flagged`, `resolved`, `read` (see below) |
102
+ | `reply` | string | AI | One-line message shown to the developer in the extension UI on hover |
103
+ | `fixed_in` | string | AI | Optional commit hash or PR ref where the fix landed (set when `status: resolved`) |
104
+
105
+ ### Status values and their meaning
106
+
107
+ | `status` | Set by | Visual in extension | File renamed? | Meaning |
108
+ |----------|--------|---------------------|---------------|---------|
109
+ | `unread` | Extension (at creation) | Default pin colour | No | Not yet processed |
110
+ | `flagged` | AI | Amber pin; `reply` text on hover | No | Ambiguous — needs developer clarification |
111
+ | `resolved` | AI | Green pin with ✓; `reply` text on hover | No | AI fixed the issue; developer should verify on page |
112
+ | `read` | AI (archive step) | Hidden — no pin shown | Yes → `*.read.md` | Archived / dismissed; developer has acknowledged |
113
+
114
+ ## Steps
115
+
116
+ 1. **Discover unread notes**
117
+
118
+ List all files in the notes directory. Keep only filenames that end in `.md` AND
119
+ do NOT end in `.read.md` — both patterns end with `.md`, so the exclusion must be
120
+ explicit. In pseudocode:
121
+
122
+ ```
123
+ unread = files.filter(f => f.endsWith('.md') && !f.endsWith('.read.md'))
124
+ unread.sort() // ascending lexicographic
125
+ ```
126
+
127
+ The filename format is `<serial>-<YYYYMMDD-HHmmss>.md` where `<serial>` is a
128
+ 4-digit zero-padded integer (e.g. `0001`, `0042`). Because every serial is
129
+ zero-padded to exactly 4 digits, ascending lexicographic sort equals serial order
130
+ — process notes from lowest serial to highest.
131
+
132
+ If there are no unread notes, report "no unread notes" and stop. Do not process
133
+ `*.read.md` files — they are already done.
134
+
135
+ 2. **Process each note in serial order**
136
+
137
+ For each unread note filename (lowest serial first):
138
+
139
+ a. **Read the file and parse frontmatter**
140
+
141
+ First, short-circuit on already-handled notes. After parsing the frontmatter,
142
+ check the `status` field BEFORE doing any work:
143
+
144
+ - `status: read` → skip this note (it is archived; normally it is also renamed
145
+ to `*.read.md` and excluded in Step 1, but skip defensively if you encounter
146
+ it).
147
+ - `status: flagged` → skip this note. A flagged note was already judged
148
+ ambiguous on a prior run and is awaiting human clarification. Do NOT
149
+ re-process it and do NOT append another `> flagged:` blockquote — re-flagging
150
+ duplicates the reason line on every run. A flagged note is intentionally NOT
151
+ renamed (it stays a `.md` file so it remains visible), so it WILL reappear in
152
+ the Step 1 listing; the `status: flagged` check here is what makes the re-run
153
+ idempotent for it.
154
+ - `status: resolved` → skip this note. The fix has been applied on a prior run;
155
+ the note is visible in the extension so the developer can verify it on the
156
+ page. Do NOT re-process or re-rename it.
157
+
158
+ Only continue to the remaining sub-steps (b–e) when `status` is `unread`. This
159
+ mirrors the `classifyNote` helper (`lib/review-notes.ts`), which returns
160
+ `'read'`, `'flagged'`, or `'resolved'` as skip outcomes and only
161
+ `'fixable'`/`'text-only'` as actionable.
162
+
163
+ Parse the frontmatter by extracting the text between the first `---` pair and
164
+ passing it to a YAML parser.
165
+
166
+ Derive the note's serial from `filename.slice(0, 4)` — NEVER from the `id`
167
+ frontmatter field. The `id` field is stored as an integer and loses its leading
168
+ zeros (the integer `1` is not the string `"0001"`). The filename prefix always
169
+ carries the authoritative zero-padded serial.
170
+
171
+ The text after the closing `---\n` is the note body. The first non-blank line
172
+ of the body is the developer's instruction — the comment they typed into the
173
+ stikfix note UI. Read it as the actionable request.
174
+
175
+ b. **Read element context (element mode only)**
176
+
177
+ If `mode` is `element`, the body contains a `## Element context` section
178
+ immediately after the developer comment. Use this section to locate the
179
+ relevant code. It looks like:
180
+
181
+ ```markdown
182
+ ## Element context
183
+
184
+ - **Selector:** `#submit-btn`
185
+ - **React component:** `SubmitButton`
186
+ - **Tag / role:** `button` / `button`
187
+ - **Text:** Submit
188
+ - **Rect:** x=100 y=200 w=80 h=30
189
+
190
+ ### Computed styles (curated)
191
+ | prop | value |
192
+ |------|-------|
193
+ | display | inline-flex |
194
+ | color | rgb(255,255,255) |
195
+
196
+ ### outerHTML (truncated)
197
+ ```html
198
+ <button id="submit-btn" class="btn-primary">Submit</button>
199
+ ```
200
+ ```
201
+
202
+ The selector, React component name, tag, text, computed styles, and outerHTML
203
+ together give you enough context to locate the exact element in the codebase
204
+ without guessing. Use `grep`, a file search, or your knowledge of the project
205
+ structure to find the relevant source file(s).
206
+
207
+ c. **Open screenshots (if available and present on disk)**
208
+
209
+ The frontmatter `screenshots` field is an array of filenames. These filenames
210
+ are relative to the **notes directory**, not the project root. Resolve each
211
+ screenshot path by joining it with the notes directory path:
212
+
213
+ ```
214
+ screenshotAbsPath = join(notesDir, screenshotFilename)
215
+ ```
216
+
217
+ If `screenshots` is non-empty AND the referenced PNG files exist on disk, open
218
+ them with vision when the text description or element context alone is
219
+ insufficient to understand the issue.
220
+
221
+ If a referenced screenshot file does not exist on disk, emit exactly one line:
222
+
223
+ ```
224
+ WARN: <filename> not found — proceeding text-only
225
+ ```
226
+
227
+ Then continue processing the note as a text-only note. A missing screenshot is
228
+ NOT ambiguous — the developer instruction and element context remain actionable.
229
+ Do not flag the note or skip it because of a missing screenshot.
230
+
231
+ d. **Apply the fix**
232
+
233
+ Using all available context — the developer instruction, element context
234
+ (selector, computed styles, outerHTML), and any screenshots — apply the
235
+ requested fix to the relevant source file(s) in the project.
236
+
237
+ If the instruction is genuinely ambiguous even after reading the element
238
+ context and any screenshots — for example, "make this better" with no
239
+ actionable target and no element context — do NOT attempt a fix. Instead:
240
+
241
+ - Update the frontmatter: set `status` from `unread` to `flagged`, and add a
242
+ `reply:` field containing a short, one-line clarification question directed at
243
+ the developer (e.g. `reply: "Which colour should the button be — brand blue or
244
+ red?"`)
245
+ - Optionally also append a blockquote line to the end of the note body for
246
+ human readability:
247
+ ```
248
+ > flagged: <brief reason why the instruction is ambiguous>
249
+ ```
250
+ (The canonical machine-readable signal is the `reply:` frontmatter field; the
251
+ blockquote is supplementary.)
252
+ - Leave the filename unchanged (do NOT rename to `.read.md` or any other name)
253
+ - Skip to the next note
254
+
255
+ The extension renders flagged pins in amber and shows the `reply` text on hover,
256
+ so the developer sees exactly what question to answer.
257
+
258
+ For all other cases (clear instruction, missing-screenshot text-only, element
259
+ mode with context), proceed with the fix and continue to step (e).
260
+
261
+ e. **Mark the note as resolved — ONLY after a successful fix**
262
+
263
+ The frontmatter update is the LAST action, performed only after the fix has been
264
+ successfully applied. This ordering is the core reliability guarantee: if the fix
265
+ fails or is interrupted, the note stays `unread` and will be retried on the next
266
+ run.
267
+
268
+ **Update the frontmatter:** Parse the YAML block, set `status: resolved`, add a
269
+ `reply:` field with a one-line description of what was done, and optionally add
270
+ `fixed_in:` with the commit hash or PR ref. Preserve every other existing field.
271
+
272
+ ```
273
+ rawFm = content between the first --- pair
274
+ fm = yaml.parse(rawFm)
275
+ fm['status'] = 'resolved'
276
+ fm['reply'] = 'Renamed Submit → Save Changes in Header.tsx' // what you did
277
+ fm['fixed_in'] = 'abc1234' // optional: commit hash or PR ref
278
+ newFmBlock = '---\n' + yaml.stringify(fm) + '---\n'
279
+ newContent = newFmBlock + bodyAfterFrontmatter
280
+ writeFile(mdPath, newContent)
281
+ ```
282
+
283
+ The project's `yaml` library (`yaml@2.9.0`, import `{ parse, stringify } from
284
+ 'yaml'`) handles colons and quotes in URL values correctly — do not hand-roll
285
+ YAML serialization.
286
+
287
+ **Do NOT rename the file.** A resolved note stays as `<serial>-<timestamp>.md`
288
+ so it remains visible in the extension as a green pin with a ✓. The developer
289
+ must be able to see the fix confirmation on the page before dismissing the note.
290
+
291
+ **Archive / dismiss (separate, later step):** Only when the developer has
292
+ acknowledged a resolved note (or explicitly wants to dismiss a note) should the
293
+ note be archived. Archiving means:
294
+
295
+ - Set `status: read` in the frontmatter
296
+ - Rename the file: `readPath = mdPath.replace(/\.md$/, '.read.md')`
297
+
298
+ This two-step approach (status:read + rename to *.read.md) is belt-and-suspenders:
299
+ the rename is the primary skip signal on the next run (excluded by the glob in
300
+ Step 1); the `status: read` frontmatter field lets other tools (like
301
+ `listAnnotations` in the host) still list the note while knowing it is archived.
302
+ The archive step is NOT done automatically after fixing — it is a separate,
303
+ developer-triggered action.
304
+
305
+ 3. **Report a terse summary**
306
+
307
+ After processing all notes, report:
308
+
309
+ ```
310
+ Processed N notes: M resolved (fix applied), K flagged (needs clarification), J skipped (resolved/read).
311
+ ```
312
+
313
+ If any notes were resolved, list their filenames and the `reply` text (what was
314
+ done) so the developer knows what to verify on the page.
315
+
316
+ If any notes were flagged, list their filenames and the `reply` question so the
317
+ developer knows what to clarify.
318
+
319
+ ## After the pass
320
+
321
+ On the next run:
322
+
323
+ - `resolved` notes: still present as `.md` files (not renamed), skipped by the
324
+ `status: resolved` check in Step 2a. They remain visible on the page as green pins
325
+ until the developer archives them.
326
+ - `flagged` notes: skipped by the `status: flagged` check in Step 2a. They remain
327
+ visible on the page as amber pins until the developer answers the question and the
328
+ note is re-opened for processing.
329
+ - `read` notes: excluded by the Step 1 glob (renamed to `*.read.md`) and skipped
330
+ defensively by the `status: read` check if encountered anyway.
331
+
332
+ A re-run on a fully-processed directory does no fixing work and reports either "no
333
+ unread notes" (all `unread` notes have been resolved or flagged) or that only
334
+ previously-flagged notes remain. This is the idempotency guarantee — re-running never
335
+ re-applies a fix, never re-resolves an already-resolved note, and never appends a
336
+ duplicate `> flagged:` blockquote.
337
+
338
+ ## Forbidden patterns (do not do these)
339
+
340
+ - **Glob `*.md` without excluding `*.read.md`** — `.read.md` files end in `.md`; a
341
+ naive glob will reprocess already-archived notes
342
+ - **Derive serial from the `id` frontmatter field** — the integer `id` loses zero-
343
+ padding; always use `filename.slice(0, 4)`
344
+ - **Set `status: read` on a successful fix** — a successful fix must set
345
+ `status: resolved` (not `read`). `read` is the archive state, set only after the
346
+ developer has acknowledged the resolved note. Getting this wrong hides the green pin
347
+ before the developer can verify the fix.
348
+ - **Rename the file after a fix** — resolved notes are NOT renamed. Renaming to
349
+ `*.read.md` is the archive step, not the fix-completion step.
350
+ - **Rename before fix** — for the archive step (if triggered), write the updated
351
+ status first; rename is always the last action
352
+ - **Treat a missing screenshot as ambiguous** — warn once and proceed text-only;
353
+ only an unclear developer instruction triggers the flagged path
354
+ - **Overwrite existing frontmatter fields** — always parse the YAML block, update
355
+ only `status`, `reply`, and `fixed_in`, and reserialize. Never clobber `id`,
356
+ `created`, `url`, `selector`, or any other field the extension wrote.
357
+ - **Make network or host calls to the stikfix host** — this skill is disk-only with
358
+ respect to stikfix itself; do not call the stikfix host HTTP API, do not POST to
359
+ any endpoint. (The local `git` CLI — `pull`/`add`/`commit`/`push` — is explicitly
360
+ permitted, and expected in git-sync mode; see "Git-sync mode (optional)" above.
361
+ That is a plain local git operation, not a call to the stikfix host.)