terrylica/cc-skills/plugins/notes-commander/skills/draft-park/SKILL.md
draft-park
Park a draft message/text in macOS Notes for the operator to review and edit, then read it back before acting (e.g. before sending to a real person). Notes is the source of truth (AppleScript CRUD, iCloud-synced, provenance-stamped with the Claude Code session UUID); Stickies is a best-effort view-only desktop mirror. Use whenever you draft something a human should confirm/edit before it is sent or committed — messages, replies, announcements, anything outbound. TRIGGERS - park this draft, park
- Source repository stars
- 62
- Declared platforms
- 1
- Static risk flags
- 1
- Last source update
- 2026-08-24
- Source checked
- 2026-08-25
Decision brief
What it does: where it fits
Renamed from draft-hold on 2026-08-12. Invoke as /notes-commander:draft-park; there is no draft-hold alias. Evolution-log entries dated before the rename keep the old name — that is the accurate historical record, not drift.
Not for
- Tasks that require unconfirmed production actions or broad system permissions.
- Environments where the pinned source and install steps cannot be inspected.
Compatibility matrix
Platform support, with evidence labels
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Declared | Source record | Install path and trigger |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
Inspect first. Install second.
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/terrylica/cc-skills --skill "plugins/notes-commander/skills/draft-park"Inspect the Agent Skill "draft-park" from https://github.com/terrylica/cc-skills/blob/a5f847b22ee5afa35677e446973a903d098cd1d4/plugins/notes-commander/skills/draft-park/SKILL.md at commit a5f847b22ee5afa35677e446973a903d098cd1d4. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
What the source asks the agent to do
- 01
Workflow
1. Park the draft (body on STDIN):
Park the draft (body on STDIN):(Optional) desktop mirror — "$DP" sticky "" pops a view-only Stickies note (needs Accessibility permission). Notes stays authoritative.Read it back before acting — ALWAYS re-read, since the operator may have edited it: - 02
Formatting is handled in code — just write naturally
The engine (formatter in scripts/lib/notes-core.ts, unit-tested in notes-core.test.ts) normalizes your input into Notes HTML, so you never hand-manage line breaks:
Prose reflows. Consecutive non-blank lines join into ONE paragraph that Notes soft-wraps to the reader's screen. Accidental hard-wrapping (text pre-wrapped at 80/100 cols) is corrected automatically — it can no longer b…A blank line = a new paragraph/section. That is the one authored break.List items — lines beginning with -, , +, •, 1., 2), a. etc. each stay on their own line; a wrapped continuation line (indented, no marker) joins back to its item. - 03
Hardening (verified failure modes this engine guards)
Silent-failure detection — new asserts Notes returned a real note id (x-coredata://…); on recent macOS, osascript can exit 0 yet create nothing. If that happens you get a loud ✗ SILENT-FAILURE instead of a phantom "succ…
Silent-failure detection — new asserts Notes returned a real note id (x-coredata://…); on recent macOS, osascript can exit 0 yet create nothing. If that happens you get a loud ✗ SILENT-FAILURE instead of a phantom "succ…Bounded retry — transient AppleEvent errors (-600/-1712/"not running") retry with backoff; permission/syntax errors fail fast.Read-back verify (default ON) — after new, the note is read back and checked for entity leaks and content presence. --no-verify skips (rarely needed). - 04
macOS quirks this skill handles for you
Quote serialization: Notes' AppleScript body getter re-emits every " as the semicolon-less legacy entity " (verified 2026-06-29). We decode with textutil (a real HTML parser) instead of sed, so "/&/< etc. r…
Quote serialization: Notes' AppleScript body getter re-emits every " as the semicolon-less legacy entity " (verified 2026-06-29). We decode with textutil (a real HTML parser) instead of sed, so "/&/< etc. r…UTF-8 decode needs an explicit charset: textutil -format html assumes Latin-1 when the HTML has no charset declaration, mojibaking every non-ASCII character (关于 → 关于; verified 2026-07-02 with a Chinese draft). htmlT…Note name = first body line, TRUNCATED: Notes names a note after its first line, ignoring any title you "set". new therefore prepends the title as a bold first line so get/list/replace can find it by title. Pass the mes… - 05
Getting the session UUID for provenance
The Bash tool already has it: Claude Code exports CLAUDECODESESSIONID (verified 2026-08-05 — matches the /.claude/projects//.jsonl name). So just forward it, and don't go hunting:
The Bash tool already has it: Claude Code exports CLAUDECODESESSIONID (verified 2026-08-05 — matches the /.claude/projects//.jsonl name). So just forward it, and don't go hunting:Only if that variable is empty: the statusline-tools:session-info skill reports the UUID, or read the newest .jsonl under /.claude/projects//. If genuinely unavailable, omit it (the footer drops the token — never write…
Permission review
Static risk signals and limitations
Reads files
The documentation asks the agent to read local files, directories, or repositories.
**2026-07-20 (a) — long titles broke read-back verify + `move-note` (name truncation).** _Trigger_: parking a 66-char draft ("CPC Scanners — Procurement Intelligence & Deliverables (2026-07-20)") returned a false `✗ CONTENT-MISMATCH`, and `Evidence record
Why each signal appears
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 100/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 62 | Source | Repository attention, not individual Skill quality |
| Compatibility | 1 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Provenance and original SKILL.md
- Repository
- terrylica/cc-skills
- Skill path
- plugins/notes-commander/skills/draft-park/SKILL.md
- Commit
- a5f847b22ee5afa35677e446973a903d098cd1d4
- License
- MIT
- Collected
- 2026-08-25
- Default branch
- main
View the original SKILL.md
draft-park — human-in-the-loop drafts via macOS Notes
Renamed from
draft-holdon 2026-08-12. Invoke as/notes-commander:draft-park; there is nodraft-holdalias. Evolution-log entries dated before the rename keep the old name — that is the accurate historical record, not drift.
Self-Evolving skill — if macOS Notes/Stickies behavior drifts from what's below, fix this SKILL.md and the shared engine
scripts/lib/notes-core.ts(+ a case innotes-core.test.ts); see the Post-Execution Reflection at the bottom.
When you compose something a human should confirm or edit before it goes out (a message to a real person, an announcement, a commit body), don't keep it only in chat — park it in macOS Notes so the operator can edit it on any device, then read it back and act on the edited version.
Resolve the entrypoint first — $CLAUDE_PLUGIN_ROOT is NOT a shell variable and expands to empty here (it exists only inside plugin manifests and hook/MCP subprocesses; see the $CLAUDE_PLUGIN_ROOT invariant in the plugin CLAUDE.md). Never glob the version cache either — it retains orphaned versions. Use the resolver:
DP="$(cc-plugin-root notes-commander)/skills/draft-park/draft-park.sh"
draft-park.sh is a thin shim that execs the Bun/TypeScript engine scripts/draft-park.ts, built on the plugin's shared notes-core engine (which also powers notes-inventory/notes-export/notes-organize).
Formatting is handled in code — just write naturally
The engine (formatter in scripts/lib/notes-core.ts, unit-tested in notes-core.test.ts) normalizes your input into Notes HTML, so you never hand-manage line breaks:
- Prose reflows. Consecutive non-blank lines join into ONE paragraph that Notes soft-wraps to the reader's screen. Accidental hard-wrapping (text pre-wrapped at ~80/100 cols) is corrected automatically — it can no longer become a permanent mid-sentence break. Blank lines are the only breaks that matter.
- A blank line = a new paragraph/section. That is the one authored break.
- List items — lines beginning with
-,*,+,•,1.,2),a.etc. each stay on their own line; a wrapped continuation line (indented, no marker) joins back to its item. - Verbatim / columnar / code blocks — wrap them in a
```fence. Every line inside is preserved exactly and rendered monospace with spaces held (via ), so columns and IDs line up in the Notes UI. (Note:get --body-onlyreturns the sendable plain text and collapses inter-column runs to single spaces; if exact alignment must survive to the recipient, send an attachment/screenshot.)
Hardening (verified failure modes this engine guards)
- Silent-failure detection —
newasserts Notes returned a real note id (x-coredata://…); on recent macOS, osascript can exit 0 yet create nothing. If that happens you get a loud✗ SILENT-FAILUREinstead of a phantom "success". - Bounded retry — transient AppleEvent errors (
-600/-1712/"not running") retry with backoff; permission/syntax errors fail fast. - Read-back verify (default ON) — after
new, the note is read back and checked for entity leaks and content presence.--no-verifyskips (rarely needed).
Workflow
-
Park the draft (body on STDIN):
CLAUDE_SESSION_ID="$CLAUDE_CODE_SESSION_ID" "$DP" new "<title>" --project "<repo-or-context>" <<'EOF' Hi <name> — <your drafted message>... EOFCreates/replaces a note in the "Claude Drafts" folder with a provenance footer (session UUID + project + timestamp). Tell the operator: "Draft is in Notes → Claude Drafts → ; edit it there, then tell me to send."
-
(Optional) desktop mirror —
"$DP" sticky "<title>"pops a view-only Stickies note (needs Accessibility permission). Notes stays authoritative. -
Read it back before acting — ALWAYS re-read, since the operator may have edited it:
"$DP" get "<title>" # full note (heading + message + provenance footer) "$DP" get "<title>" --body-only # JUST the sendable message (no heading, no footer)Use
--body-onlyto get exactly the text to send/paste. Add--for whatsappto convert markdown into the channel's own syntax, and--copyto put the result straight on the clipboard so there is no hand-selection step to get wrong:"$DP" get "<title>" --for whatsapp --copy # sendable text → clipboard, warnings on stderr--forand--copyboth imply--body-only. stdout is always exactly the sendable bytes; warnings go to stderr, so piping stays clean. Show the operator the exact current text, get explicit go-ahead, then send/commit. -
"$DP" listenumerates parked drafts.
macOS quirks this skill handles for you
- Quote serialization: Notes' AppleScript
bodygetter re-emits every"as the semicolon-less legacy entity"(verified 2026-06-29). We decode withtextutil(a real HTML parser) instead ofsed, so"/&/<etc. round-trip back to literal characters. Never hand-roll entity decoding here. The read-back verify now DETECTS a drift here automatically (✗ ENTITY-LEAK). A real HTML parser is not sufficient on its own (fixed 2026-07-20): because the stored form is semicolon-LESS, a note holdingWrite-Host "x"; $ycomes back raw asWrite-Host "x" $y, and textutil reads the closing"plus the author's own;as one entity — silently eating the semicolon, so it round-tripped asWrite-Host "x" $y. That corrupts most staged PowerShell/C/Java/JS with no error, which is fatal for a skill whose job is staging text a human will SEND.terminateLegacyEntities()innotes-core.tsnow appends the;to every bare entity before decoding, so a following literal;stays literal. This is safe unconditionally because Notes escapes every&it stores (an author's literal&comes back as&amp;), so a terminated entity in Notes output can only ever be bare-entity + the author's semicolon. - UTF-8 decode needs an explicit charset:
textutil -format htmlassumes Latin-1 when the HTML has no charset declaration, mojibaking every non-ASCII character (关于 →关于; verified 2026-07-02 with a Chinese draft).htmlToTexttherefore prepends<meta charset="utf-8">before piping to textutil — keep that prefix. - Note name = first body line, TRUNCATED: Notes names a note after its first line, ignoring any title you "set".
newtherefore prepends the title as a bold first line soget/list/replace can find it by title. Pass the message body only on STDIN. But Notes truncates a long first line to a cappednameending in an ellipsis…(verified 2026-07-20: a 66-char title stored as…(2026-07-20…), so an exactwhose name is <title>lookup MISSES long-titled notes. The engine handles this:newreads its note back by id (not title) for the verify, andget/sticky/move-noteresolve a title to a note truncation-tolerantly vianoteNameMatchesTitle()(exact, else the stored name is the title truncated with a trailing…). Never rely on exact-name AppleScript matching for a possibly-long title. - Links: the SETTER keeps them, the GETTER strips them (verified 2026-08-05). Write
[label](url)and the engine emits<a href>; Notes stores a genuine link attribute (confirmed by decompressing the note'sZICNOTEDATA.ZDATAprotobuf inNoteStore.sqlite— the href sits in the attribute run while the visible-text run holds only the label). But AppleScript'sbodygetter returns<u>label</u>with no href, for links Notes itself made too. Consequences: (1)get/get --body-onlyshows link TEXT and silently loses the URL — never round-trip a linked note throughgetand re-newit, the links will be gone; (2) read-back verify is link-blind by construction, sorenderInline's unit tests are the only place an href is observable — do not "harden" verify by asserting hrefs, they never appear. Onlyhttp(s)andmailtobecome anchors; any other scheme (javascript:,file:, a bare path) renders as literal text. - Proportional prose, monospace only in fences: prose paragraphs render in Notes' normal proportional font (so a long line reflows). Only
```fenced blocks are wrapped in<tt>(Notes' "Monostyled" face) with spaces held as . The mono face is fixed by Notes — it is not the globalNSFixedPitchFont, which governs TextEdit-style apps, not Notes.
Getting the session UUID for provenance
The Bash tool already has it: Claude Code exports CLAUDE_CODE_SESSION_ID (verified 2026-08-05 — matches the ~/.claude/projects/<slug>/<uuid>.jsonl name). So just forward it, and don't go hunting:
CLAUDE_SESSION_ID="$CLAUDE_CODE_SESSION_ID" "$DP" new "<title>" …
Only if that variable is empty: the statusline-tools:session-info skill reports the UUID, or read the newest *.jsonl under ~/.claude/projects/<project-slug>/. If genuinely unavailable, omit it (the footer drops the token — never write a placeholder).
Rules
- Never send/commit from memory — always
getthe note first; the operator may have changed it. - Never hand-copy the note out of the Notes UI. The provenance footer is selectable text sitting
right underneath the message; on 2026-08-17 it was pasted into a clinic WhatsApp group, disclosing
Parked by Claude Code, a session UUID and a repo path. Use--body-only(or--copy), which now also REFUSES to emit any text still containing the footer. - Write URLs inline, never as
[label](url), in anything you intend to SEND. Notes stores the href but its AppleScript getter strips it, so the URL cannot come back.newrefuses such links with the inline form to use instead;--allow-lossy-linksoverrides for a note meant to be read in Notes rather than sent. - Notes is the source of truth. Stickies cannot be read back (no AppleScript dictionary), so never treat a sticky as the live draft.
- There is no scriptable deep-link to a specific note (
open x-coredata://…fails;applenotes:links are UI-only) — reference drafts by folder + title. - First run prompts once for Automation permission to control Notes.
Evolution log
-
2026-08-17 — three ways a parked draft reached a recipient WRONG, all found in one afternoon after a real clinical message went out damaged. Trigger: a message to a dental clinic's reviewer arrived truncated to 38% of its length, carrying internal provenance, and studded with 20 literal
**. Root causes and fixes:- Silent truncation, and it was injection-shaped.
bodyOnly()broke at the FIRST line matching/^------\s*$/, but the footer it strips is always LAST — so an author's dash rule cut the message there (1,760 of 4,635 chars). The Notes UI showed the whole note, and the sendable text is only ever read by the human pasting it, so nothing looked wrong. The severity is WHOSE text can trigger it: drafts quote clinician comments, patient transcripts and Drive comments verbatim, so a dash rule written outside this machine could choose the cut point. NowfindFooterStart()scans from the END and requires BOTH signals (the rule AND a followingParked by Claude Codeline), so prose dashes are ignored and a quoted footer cannot pull the cut upwards. - URLs cannot survive a markdown link, and the existing verify only caught it by luck. Parking
[the rules portal](https://…)and reading back yieldsthe rules portal— URL absent, not mangled.contentPresent()compares only the first 24 characters of the first line, so it catches a link near the start and passes GREEN on one further in (measured both ways).newnow REFUSES markdown links and prints the inline form to use;--allow-lossy-linksescapes it and relaxes the content check, loudly. - Markdown is not what the channel speaks.
--for whatsappconverts**bold**→*bold*, headings → bold, and flattens[label](url)→label: url, warning on stderr about anything it cannot map (inline code, tables). Fenced blocks are left verbatim.--copywrites the sendable text to the clipboard, removing the hand-copy step that leaked the footer, and says so rather than silently replacing the clipboard. Evidence: 25 new unit tests (64 total, 0 fail), each proved in both directions, plus a mutation check confirming the pre-fix code genuinely exhibits failures 1 and 2 — so the regression tests are not vacuous.
- Silent truncation, and it was injection-shaped.
-
2026-08-12 — renamed
draft-hold→draft-park. Trigger: operator directive — "park" is the verb the skill's own docs and triggers already used ("park it in macOS Notes", "park the message"), while "hold" read as a queue/blocking state. Scope: skill dirskills/draft-hold/→skills/draft-park/, shimdraft-hold.sh→draft-park.sh, enginescripts/draft-hold.ts→scripts/draft-park.ts, call-site variable$DH→$DP, and every live cross-reference (plugin CLAUDE.md/README/plugin.json, marketplace.json, root + plugins CLAUDE.md,scripts/cc-plugin-rootusage example, macos-font-defaults' Notes-mono cross-reference, and the operator's live~/.claudeinstructions). Nodraft-holdalias exists — the slash command is/notes-commander:draft-parkonly. Deliberately NOT rewritten: CHANGELOG.md, the entries below this one, and the itp-hooks skill-plugin-root-guard forensics — those describe events that happened under the old name, and back-dating a rename into them would make the incident record wrong. Behavior: unchanged — same subcommands, same flags, same "Claude Drafts" Notes folder, so previously parked notes are still found byget/list. The one content change: the provenance footer now readsParked by Claude Code(wasHeld by). Nothing parses it —bodyOnly()cuts at the------separator — so notes written under the old wording read back identically. -
2026-08-05 (b) — the skill could not find its own entrypoint (
exit 127). Trigger:/notes-commander:draft-holdinvoked from another repo died on(eval):1: no such file or directory: /skills/draft-hold/draft-hold.sh. Root cause: this SKILL.md told the caller to useDH="$CLAUDE_PLUGIN_ROOT/skills/draft-hold/draft-hold.sh", butCLAUDE_PLUGIN_ROOTis not a shell variable. Claude Code substitutes the exact literal${CLAUDE_PLUGIN_ROOT}(braces REQUIRED — the helper ise.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginPath)) inside plugin manifests, and injects the var into hook/MCP subprocess envs — never into the Bash tool. The bare$…spelling used here is unsubstitutable on every path, so it reached zsh as an unset var, expanded to empty, and produced an absolute-looking/skills/…path — which reads like a missing file, not a missing variable. Two upstream causes made it likely: the repo's ownadvanced-topics.mdandlifecycle-reference.mddocumented the rule exactly backwards ("available in skill loading, NOT in hooks" — it is the reverse), and the recovery path was equally unsound: globbing the version cache and taking the highest semver picked23.4.1, which is marked.orphaned_at(live was23.5.0). Fix: addedscripts/cc-plugin-root(reads~/.claude/plugins/installed_plugins.json, tolerates both registry schemas, jq with a python3 fallback, symlinked into~/.local/bin/); this SKILL.md now resolvesDH="$(cc-plugin-root notes-commander)/skills/draft-hold/draft-hold.sh"; both reference docs corrected; the stale "L3 cache stripsscripts/" claim retired. Also stopped the sibling waste in this same flow:CLAUDE_CODE_SESSION_IDis already exported into the Bash env, so the provenance UUID no longer needs hunting. Evidence: resolver returns the live23.5.0path via both jq and python3 backends and under both registry shapes; negative cases exit 1/2 with actionable stderr; the resolveddraft-hold.shandscripts/draft-hold.tsboth exist. -
2026-08-05 (a) — every URL had to be shown naked, because the formatter escaped anchors. Trigger: staging a weekly report whose 16 PR references made the prose unreadable; the operator asked for
#470-style link text and supplied a screenshot of a working link in Notes, proving Notes supports them. Root cause:escapeHtml()was applied to every prose and list line, so an<a href>could only ever render as literal<a href…. The trap that nearly ended the investigation: a probe note written with<a href>read back as<u>#470</u>, which looks exactly like "Notes stripped the link" — I nearly concluded the setter was broken. The operator's OWN hand-made link read back as<u>link text</u>too, which is what exposed the real shape: the getter is lossy, not the setter. Confirmed by decompressingZICNOTEDATA.ZDATAfromNoteStore.sqlite— the probe note's visible text wasSee #470 and #472.while the hrefs sat in the attribute run. A read-back-only check would have produced a false negative and no links. Fix: added pure, unit-testedrenderInline()promoting[label](url)→<a href>at the three prose/list call sites (fences stay verbatim, so a fenced[x](url)renders literally);http(s)/mailtoallow-list so ajavascript:link can never be minted into a document a human will click. Evidence: 6 new unit tests (39 pass, 0 fail), plus a live round-trip verified against the SQLite protobuf — 16 hrefs stored, zero URLs leaked into visible prose. -
2026-07-27 — a lead-in line silently ate the list under it. Trigger: staging a real reply draft,
解决办法有两个,你倾向哪个?immediately followed by two-bullets (no blank line) came back fromget --body-onlyas ONE run-on line, bullets and all. Hit twice in the same session, in two different notes. Root cause:renderTextBlock()classified each blank-line-delimited paragraph by testingp[0]only — so a paragraph whose first line is prose was rendered wholly as prose, andreflowJoin()folded the following-/1.markers into it. The list was destroyed with no error. This contradicted SKILL.md, which promised list markers "each stay on their own line", and left an undocumented "you must leave a blank line before a list" rule that authors could only learn by being bitten. Fix: split each paragraph at the FIRST line matchingLIST_RE— lead-in lines reflow as prose, everything from the first marker on renders per-item via the new extractedrenderListItems()helper (also removes the duplicated item-grouping loop). All three shapes now work: all-prose, all-list, and lead-in-then-list. Evidence: 3 new unit tests (CJK lead-in + bullets, English lead-in + numbered list, multi-line lead-in that must still reflow before splitting); 33 pass, 0 fail. -
2026-07-20 (b) — unified the truncation-match rule into one home (DRY). The 2026-07-20 (a) fix left the exact-then-truncated matching rule in TWO places:
noteNameMatchesTitle()innotes-core.ts(used by draft-hold) AND a hand-copied AppleScript scan insidenotes.tsOSA_MOVE, guarded only by a "keep the two rules in step" comment. Fix: added purematchNoteIds(index, title)tonotes-core.ts(exact ids first, else truncation-tolerant; returns every match so callers can flag ambiguity).move-notenow indexes the source folder (OSA_NOTE_INDEX_BY_PATH), resolves the title in TS viamatchNoteIds, and moves by id (OSA_MOVE_BY_ID); the name-matching AppleScript is deleted. draft-hold's own resolver + dedup route throughmatchNoteIdstoo. One rule, unit-tested (5 new tests, 30 total), impossible to drift. Invariant recorded in the plugin CLAUDE.md (thematchNoteIdsentry). -
2026-07-20 (a) — long titles broke read-back verify +
move-note(name truncation). Trigger: parking a 66-char draft ("CPC Scanners — Procurement Intelligence & Deliverables (2026-07-20)") returned a false✗ CONTENT-MISMATCH, andmove-note "<that title>"then failed with "note not found in source folder", even though the note existed and its body was correct. Root cause: macOS Notes stores a long first-linenameTRUNCATED with a trailing…, sonew's read-back (body of note <title> of folder) andmove-note'swhose name is <title>both looked up a name that no longer equalled the title. Fix: added the pure, unit-testednoteNameMatchesTitle()+NOTES_NAME_ELLIPSIStonotes-core.ts(exact, else truncated-prefix match);newnow verifies by id (OSA_GET_BY_ID) and dedups older copies via a JS-side folder index;get/stickyresolve title→id truncation-tolerantly;move-note(notes.ts) falls back to a truncated-prefix scan that captures matches by id (a livenotes of srcreference held past the loop throws-1728). Evidence: a 125-char-title probe now creates (clean id, no CONTENT-MISMATCH),gets its body, andmove-notes across folders — all green; 5 new unit tests (25 pass total). Verified live on macOS Notes. -
2026-07-18 (b) — migrated into notes-commander + hardened. The standalone draft-hold plugin was folded into the new
notes-commanderplugin as one of its skills; formatting + process wrappers moved to the sharedscripts/lib/notes-core.tsengine. Added, per a web-researched audit of recent macOS AppleScript failure modes: silent-no-op detection (isNoteIdon create — osascript can exit 0 yet create nothing on macOS 26), bounded retry on transient AppleEvent errors (-600/-1712), and a default-on read-back verify (entityLeaks+contentPresent). All pure helpers unit-tested (16 tests innotes-core.test.ts). -
2026-07-18 (a) — hard-wrapped prose became forced mid-sentence breaks. A long bilingual briefing was passed with each paragraph pre-wrapped at ~100 chars; because the old bash
newmade each input line its own Notes paragraph (all wrapped in<tt>), the reader saw mid-sentence line breaks that did not reflow. First fix (insufficient): a "one line per paragraph" caller contract — but that only works if every caller remembers it. Real fix: reimplemented the engine as Bun/TypeScript, enforcing the formatting in code so the failure is impossible: prose blocks REFLOW (consecutive lines join; blank line = paragraph), list markers stay per-item, and only```fenced blocks are preserved verbatim/monospace (spaces held as ). Verified by unit tests + a live Notes round-trip.
Post-Execution Reflection
After holding or sending a draft, check before closing:
- Did read-back match what Notes shows? — if entities/quotes leaked (e.g.
"), the decode path drifted; the verify step should have caught it — fix thetextutilstep innotes-core.ts, never hand-roll sed. - Did
get/listfind the note by title? — if not, the name==title assumption broke; fix the title-prepend innew. - Did Notes/Stickies change behavior? — update the macOS-quirks section so the next run doesn't rediscover it.
Only update if the issue is real and reproducible — not speculative.
Frequently asked questions
What to verify before installation and use
What does the draft-park source document cover?
Renamed from draft-hold on 2026-08-12. Invoke as /notes-commander:draft-park; there is no draft-hold alias. Evolution-log entries dated before the rename keep the old name — that is the accurate historical record, not drift.
How do I install draft-park?
The source record exposes this install command: npx skills add https://github.com/terrylica/cc-skills --skill "plugins/notes-commander/skills/draft-park". Inspect the command and pinned source before running it.
Which Agent platforms does the source record declare?
The pinned source record declares support for: claude code.
Which permission-related actions were detected?
Static rules flagged read-files in the source; the page lists the matching lines and excerpts.
Alternatives
Compare before choosing
oaustegard/claude-skills
featuring
Generate hierarchical _FEATURES.md files that describe what a codebase DOES from a user/consumer perspective, anchored to source symbols via tree-sitting. Supports large complex codebases through feature-driven decomposition into sub-feature files. Uses a multi-pass synthesis: orientation → detail → overview rewrite. Use when someone says "what does this do", "document features", "feature inventory", "_FEATURES.md", or needs to understand a codebase's purpose before modifying it. Complements tre
apollographql/skills
skill-creator
Guide for creating effective skills for Apollo GraphQL and GraphQL development. Use this skill when: (1) users want to create a new skill, (2) users want to update an existing skill, (3) users ask about skill structure or best practices, (4) users need help writing SKILL.md files.
narrative-io/narrative-skills-marketplace
design-analysis
Translate a fuzzy analytical question into a rigorous investigation plan. Interrogates the ask, grounds the plan in the available data dictionary, applies analytical best practices, and produces a structured brief of query specifications for a downstream query-writing skill. Plans, does not write SQL. Use when: "why did X drop", "is there a relationship between A and B", "who are our highest-value customers", "what's driving the change in Y", "investigate this trend", "design an analysis for", "
vasilyu1983/AI-Agents-public
agents-hooks
Configures Claude Code hooks and Codex hooks.json/notify callbacks. Use when adding guardrails, preflight, audit trails, worktree automation, or budget enforcement.