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 +25 -8
- package/dist/host/stikfix-init.cjs +76 -29
- package/package.json +3 -2
- package/skill/SKILL.md +361 -0
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
|
|
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
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
6
|
-
var
|
|
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,
|
|
471
|
-
var CONFIG_PATH2 = (0,
|
|
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,
|
|
486
|
-
const name = (0,
|
|
487
|
-
const notesDir = (0,
|
|
488
|
-
const hostEntryPath = (0,
|
|
489
|
-
(0,
|
|
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,
|
|
492
|
-
const hostBinPath = (0,
|
|
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,
|
|
529
|
+
const projectRoot = (0, import_node_path4.resolve)((0, import_node_path4.join)(__dirname, "..", ".."));
|
|
500
530
|
const firefoxOutputCandidates = [
|
|
501
|
-
(0,
|
|
502
|
-
(0,
|
|
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,
|
|
505
|
-
const browserOutputDir = isFirefox ? firefoxOutputCandidates.find((d) => (0,
|
|
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,
|
|
508
|
-
(0,
|
|
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,
|
|
512
|
-
(0,
|
|
513
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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.
|
|
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.)
|