Parity is proven and every call site now invokes the JavaScript engine, so the
Go implementation has no callers. It remains recoverable from history.
The newsletter engine was Go's only consumer here: the Hugo theme is vendored
under themes/ and the site configuration declares no module block, so nothing
else needs a toolchain. Dropping go.mod also disposes of a latent defect, since
it declared a newer Go version than either build environment pinned and only
worked because the toolchain was fetched on demand.
The Go patterns in .gitignore go with it.
Switches every call site to `node scripts/newsletter` and, in the same pass,
fixes three structural problems in the skill layer.
The seven subcommands were spelled out in five files; they are now enumerated
once in docs/newsletter/engine-commands.md, and every other mention is a link
except where a skill inlines the one or two commands it actually invokes. The
shared post mechanics move out of mt-add-url's directory into docs/newsletter/,
so no skill owns another skill's documentation and both runtimes reach it by a
repository-relative path. The Codex adapters become symlinks to their canonical
counterparts, which removes the parallel frontmatter that could drift.
Two skills are renamed for a uniform mt-<verb>-<object> scheme: mt-add-post
becomes mt-add-article, since it adds an article to a post, and mt-webfetch
becomes mt-fetch-url. The other four already conformed. Note that $mt-add-post
and mt-webfetch no longer resolve; no alias directories are left behind, because
two names for one skill is the duplication this change removes.
Trigger wording in the frontmatter descriptions is left alone apart from the
renamed tokens and the corrected fetch-tier summary: those strings are how the
runtimes decide whether to invoke a skill.
Setup documentation now states the npm ci prerequisite. The engine has
dependencies, so unlike its predecessor it does not run on a bare clone.
Captures the Go engine's stdout for 26 deterministic cases, commits those
outputs as fixtures, and asserts the JavaScript engine reproduces them through
the real CLI surface, exit codes included. This is both the migration's proof of
correctness and the regression net the engine never had.
Every fixture was generated from the Go engine rather than written by hand, and
reviewed before commit so no existing bug became a permanent contract. Each
behaviour that a JavaScript-idiomatic rewrite would silently break has a case
that goes red when the naive implementation is reintroduced; verified by
mutating the engine and re-running.
Content-scanning commands are pointed at a fixture tree through the subprocess
working directory. Both engines already resolve content/post from the process
CWD, so neither needed a test-only configuration knob, and the suite is proven
isolated from the real content by renaming it away and re-running.
Cases that depend on live upstreams are opt-in behind NEWSLETTER_NET=1: a
publication's feed window moves, and a suite that goes red on someone else's
outage teaches people to ignore red. The deterministic tier reaches the network
only through .invalid hosts, which never resolve, so it runs offline in about
two seconds.
The golden files are compared byte for byte, so scripts/newsletter is pinned to
LF; the repository otherwise checks out CRLF, which would break every comparison
on a fresh clone.
Translates the seven-subcommand engine to Node ESM, one module per former Go
file, invoked as `node scripts/newsletter <command>` from the repo root. The Go
implementation stays in place for now so parity can be measured against it.
Hand-rolled HTML and XML regexes give way to cheerio, which removes the manual
string surgery in caption extraction and covers RSS and sitemap XML through
xmlMode without a second parser. fetch-via-defuddle gains a local extraction
stage ahead of the defuddle.md proxy; the proxy stays, because fetching from a
third IP is the whole point when this machine's IP is the blocked one. Both
stages now emit the same YAML frontmatter plus body, and an extraction that
looks like a bot challenge counts as a local failure so the proxy still runs.
Behaviour is preserved where it is load-bearing rather than where it is merely
idiomatic: query strings are rebuilt by string surgery so surviving parameters
keep their original order and encoding, duplicate detection stays an index loop
with byte-offset boundary checks so a prefix of a stored URL is not a false
match, empty optional fields are omitted rather than emitted as "", a deep-crawl
miss reports cutoff as null rather than dropping the key, bullet filtering counts
code points, tag counts keep their six-column alignment, and a malformed percent
sequence falls back to the raw substring.
The printer lives in its own module so a command module never imports the
dispatcher: index.js runs main() at module scope, so that cycle would execute
the CLI as a side effect of any import. Missing dependencies report one
actionable line instead of a module-resolution stack trace, and a reader that
closes early exits quietly instead of raising EPIPE.
The root package.json already declares "type": "module", so a .js file here is
ESM and the .mjs suffix only restates what the manifest states. Renames the three
GitHub Actions scripts and follows every reference, including the six JSDoc
import("...") type annotations that would otherwise degrade to any without
throwing.
Replace hand-rolled REST paging, GraphQL cursor loops and header plumbing with
octokit, which also brings request retry and rate-limit backoff. Introduces the
repo's first package.json, lockfile and ESLint flat config; the Hugo site build
itself still needs no Node step.
Output verified against the previous implementation: identical counts and rows
apart from live star drift on four upstream repos.
Resolve owned namespaces from org memberships with the admin role instead of
assuming a fixed owner list, so repos under orgs I own are included.
- starred: owned public non-fork repos with at least one star, by stars then
most recent push
- linked: owned public non-fork repos exposing a homepage or Pages URL, by most
recent push
- contributed: public repos owned by others, by my merged PR count then most
recent push
Private repos are excluded everywhere; forks are excluded from the owned lists
because their homepage belongs to upstream. Split the generator into API access
and rendering modules.
Add a reader-proxy tier after defuddle, stop skipping URLs that only fail a
plain classifier fetch, let a session pin an earlier draft as the target post,
and tighten Substack image candidate handling (dedupe, stable numbering,
reuse a resolved source post across a batch).
Update the referral link to https://agentkit.best/?ref=BWA910UK and expand
the description with current kits, pricing, and the desktop app, noting the
former ClaudeKit name.
The duplicate-check boundary set had '<' but not '>', so a URL stored
as <https://…> (autolink, used by older posts) was never detected as a
duplicate. Inherited from the JS engine; found while validating the Go
port against 2020-era posts.
One Go binary with a subcommand per former script, invoked as
'go run ./scripts/newsletter <command>' from repo root. Stdlib only;
publications config embedded via go:embed (go run recompiles on edit).
Behavior parity verified side-by-side before deleting the JS —
see plans/reports/parity-260818-newsletter-go-migration-report.md.
Permissions: allow 'Bash(go *)', drop 'Bash(node *)' and the redundant
'Write(content)' rule (Edit rules cover all file-editing tools).
- Add Gemini CLI and Antigravity to the list of tools used
- Add dedicated section with deprecation timeline (June 18 cutoff)
- Update tags with Gemini CLI and Antigravity
Extract newsletter scripts to a neutral scripts/newsletter/ engine and add
side-by-side support for all three AI coding tools off one source of truth.
- Move 9 scripts + substack config from .claude/skills/**/scripts to
scripts/newsletter/ (history preserved); fix PROJECT_ROOT depth and
cross-require/config paths; repoint all SKILL.md invocations
- Add canonical AGENTS.md; reduce CLAUDE.md to an @AGENTS.md import
- Add opencode.json (permissions, no MCP); skills auto-discovered in place
- Add codex/prompts/*.md (6 prompts) + copy installers (install.sh/.ps1)
- Add docs/multi-tool-usage.md (setup, per-tool invocation, teardown) + README pointer
- fix checkDuplicate false-negative on Substack cover images (uuid with no size suffix)
- stop cleanUrl from corrupting the query string on unparseable input
- extract shared fetchWithTimeout into url-utils (consistent timer cleanup)
- split find-substack-post html/rss helpers into html-text-utils
- scan content/post for duplicates, matching the other scripts
- correct doc drift in mt-add-image/mt-add-url and drop plan-phase code comment
ByteByteGo images carry no caption/alt and image order cannot be mapped to
titles automatically (sponsor and video items interleave). Extract the source
post's TOC topic titles as candidates and let the user pick the matching label
(caption is used directly when a publication provides one; typing is always
available). Validated against newsletters 101-106: the correct title appears
in the candidate set for all 8 images.
Add the image handler in the meta-dispatch architecture. Given an image
URL it resolves a label and inserts it into the newsletter Bonus -> Images.
For Substack-hosted images, decode the S3 image UUID and find the source
post across configured publications (ByteByteGo by default): RSS feed first,
then a sitemap crawl up to ~3 months back. Label priority is figure caption,
then post title, then a user-typed name. Non-Substack images are best-effort
then ask. Publications are configurable and the skill offers to add a new one
when the user identifies it.
Wire mt-add-url to dispatch the image route to mt-add-image (previously a
fallback). Update the project skills list.
Add mt-add-url meta skill that classifies a URL and auto-dispatches to a
handler skill: articles to mt-add-post, YouTube links to the new
mt-add-video (Bonus -> Videos with a Vietnamese title + short summary).
Unsupported types (image, etc.) prompt the user to add or extend a skill;
image support is deferred to a future mt-add-image handler.
Shared logic (add-url.js router, url-utils.js, find-newsletter-number.js)
and a shared post-mechanics reference live under mt-add-url/. mt-add-post
is reduced to an article-only handler. add-url.js detects watch/youtu.be/
shorts, normalizes to the canonical watch URL, dedups by video id, and
fetches the title via oEmbed. checkDuplicate uses execFileSync (no shell).
bareUrl() dropped the query string, collapsing every youtube.com/watch
URL to the same bare key and flagging all YouTube links as duplicates.
Keep host identity params (v for youtube) so distinct videos are not
treated as duplicates.
Run 25919953710 still failed htmltest on both Hugo legs. Two distinct
issues, both pre-existing latent bugs that v0.3.0 made impossible to
ignore:
1. Several theme templates piped paths with a leading slash through
`relURL`, which in Hugo means "this is already absolute — leave it
alone." The intended subpath prefix (`/tsuki/`) was therefore never
added, producing hrefs like `href="/search/"` and `href="/"`. In
production the GitHub Pages mount masked this (broken links from
the page's perspective still happened to resolve under the repo
path most of the time), but htmltest correctly reported them as
broken. Fix by either:
- using `site.Home.RelPermalink` for the home link, or
- piping a string without a leading slash through `relLangURL`
(so Hugo prepends the baseURL path), or
- stripping the leading slash from data-driven URLs first via
`strings.TrimPrefix "/"` and then `relLangURL`.
Affected: header.html, search-button.html, recent-posts.html,
home/hero.html, nav.html, 404.html, search/list.html (Pagefind UI
asset URLs).
2. htmltest has no URL-rewriting feature (the `URLSwap` config in the
prior `d30f50f` "fix" was never read by htmltest — confirmed by
reading htmltest 0.17.0 source). The only clean way to make
internal-link checking work with a `/repo/`-style baseURL is to
mirror the URL structure on disk. CI now builds with
`hugo --destination public/tsuki`, runs Pagefind/smoke/htmltest
against that path, and uploads `exampleSite/public/tsuki/` as the
Pages artifact. The artifact contents become the served site
under `/tsuki/`, identical to the prior behaviour from the
browser's point of view. `.htmltest.yml` drops the dead URLSwap
block.
Also: `tmp/` (htmltest's runtime cache) added to .gitignore so local
runs don't dirty the working tree.
Local verification: 32 smoke checks green; htmltest reports
`✔✔✔ passed, tested 29 documents` against the subpath build.
Three independent CI failures from run 25916500415:
1. Smoke test [linkToSection aria-label] fail on Hugo 0.146 leg.
The assertion pinned to $post selected via `find ... | head -1`,
which on 0.146 picked `doc-sach-quan-sat/index.html` — a post with
zero h2/h3/h4 headings, so zero heading-anchor markup. Rewrite the
check to scan all built post HTML (same pattern already in use for
render-link rel). Now reports 13 matches on the demo build.
2. htmltest URLSwap silently never matched. The prior
`^/tsuki/: /` config (added in d30f50f) used a regex anchor that
doesn't match htmltest's substring evaluation against minified
`href=/tsuki/...` attributes. Switch to the unanchored
`"/tsuki/": /` form. CI has been failing on this since v0.2.0
despite the "fix" commit; this is the real fix.
3. head.html referenced `/favicon.ico` and `/favicon.svg` but neither
asset existed in exampleSite/static/. Add a minimal SVG favicon,
drop the legacy .ico reference (modern browsers fall back to a
site-rooted /favicon.ico by convention; sites needing one can
override head.html). Switch the SVG link to `relURL` so the
`/tsuki/` baseURL prefix is honoured under GitHub Pages subpath
deploy.
Local validation: all 32 smoke checks green; favicon resolves to
`/tsuki/favicon.svg` and ships in the build artifact.
- Append review footer to 101 newsletter posts
- Remove 13 duplicate sections across 11 posts (link kept in earlier post)
- Strikethrough user's personal commentary about AI tools/models in intros
- README: add a11y badge linking to docs/accessibility.md.
- CHANGELOG: full v0.3.0 entry under Unreleased (Added/Changed/Fixed/
Deferred-to-v0.3.1). Documents per-kind CSS bundles, Tier A
features, Lighthouse-relevant polish, contrast-token visual diff,
tap-target sizing decisions, and Hugo CI matrix.
- docs/accessibility.md: new — WCAG 2.2 AA conformance summary,
Lighthouse measurement instructions, projected baseline (TBD entries
to fill after first production deploy), known limitations.
- docs/customization.md: cover-image override snippet (v0.4.0 native
pipeline promise); document breadcrumbs/prev-next/lang-switcher opt-ins.
- plans/260510-0144-tsuki-v0.3.0/: mark all phases completed; note
narrow-TOC <details> deferral to v0.3.1; record audit-pass scope
additions integrated into Phases 2–6.
- plans/reports/: add researcher-260515-tsuki-vs-stack-papermod-feature
-gap.md (verdict: feature parity on must-haves; Tier A gaps shipped
this round) + code-reviewer-260515-lighthouse-80-baseline-audit.md
(projected ≥80 on all 4 categories; ≥95 a11y achievable with
shipped fixes).
scripts/smoke-tests.sh:
- Per-kind CSS budget (home/post/list/archives/search), each ≤ 4200 B gz
- theme-color light/dark meta tags
- aria-pressed SSR on theme-toggle
- breadcrumbs nav + BreadcrumbList JSON-LD presence on post,
absence on home
- prev/next nav + rel=prev attribute
- linkToSection aria-label on heading anchors
- Speculation Rules absent by default
- llm.txt artifact present
- Pagefind UI preload-swap pattern on /search/
- Giscus preconnect gating (absent when comments disabled)
- Per-kind CSS bundle routing (home loads home.css but not single.css;
post loads single.css but not home.css)
- code-copy.js gating (post loads it, home does not)
.github/workflows/pages.yml:
- Build matrix on Hugo 0.146.0 (theme.toml floor) + 0.154.0 (current).
Smoke + htmltest run on both. Artifact upload + deploy gated on
the current version only.
- Drop the redundant standalone CSS-budget step; smoke-tests.sh now
enforces per-kind budgets.
Headline outcomes:
- Feature parity with Stack/PaperMod on Tier A polish: breadcrumbs
(+BreadcrumbList JSON-LD), prev/next post navigation (rel=prev/next),
language switcher UI (gated on hugo.IsMultilingual), code-copy button
polish, <details> styling.
- Per-page-kind CSS bundles: core loads everywhere; home/single/archive/
search bundles load only where needed. Frees ~1 KB gz from non-post
pages. code-copy.js gated to post pages.
- Lighthouse-relevant network polish: Pagefind UI CSS preload-swap,
conditional preconnect to giscus.app on comment-enabled posts,
hreflang alternates on multilingual sites, <meta name=theme-color>
light/dark variants, aria-pressed SSR-rendered on theme-toggle.
- WCAG AA contrast: --tsuki-fg-subtle darkened to #6b6b6b (light); was
3.54:1 on bg, now 5:1. Pagination disabled uses --tsuki-fg-muted
without opacity compound. Header tap targets bumped to 40×40; pagination
to 44×44.
- i18n: full i18n/en.yml mirror (~50 keys); render-heading aria-label
i18n-driven via linkToSection key; new keys for breadcrumb*, prevPost,
nextPost, copyCode, copiedCode.
- Discovery: /llm.txt output format (llmstxt.org) on home; Speculation
Rules opt-in via params.prefetch.enable.
- <html lang> fallback: site.Language.LanguageCode → Lang → "en"
(was hard-coded "vi").
Per-kind bundle gz sizes on demo: home 3673 / post 4167 / list 2897 /
archives 3363 / search 3179 B (all under 4200 B cap).
Plan: plans/260510-0144-tsuki-v0.3.0/
Separate the general BigModel platform invite (20M free tokens) from
the new GLM Coding Plan referral (5% friend discount, tiered cashback
at 3 and 30 invitees).
htmltest was resolving links like `/tsuki/css/foo.css` against
`exampleSite/public/tsuki/...` (which doesn't exist); actual path
is `exampleSite/public/...`. URLSwap strips the baseURL prefix.
Failing on every CI run since v0.2.0 — deploys not propagating.
The 7-phase v0.2.0 implementation roadmap is shipped (commits
6e17ee6..5d73a13). Move it under plans/archive/ so plans/ root only
shows active work.
Add plans/260509-0947-v0.2.0-prerelease-checklist/ capturing what's
left before the v0.2.0 tag and gallery submission: action SHA pin,
phase-07 plan-todo accuracy fix, CI watch, journal, tag, plus
maintainer decisions (Q1 unsafe Goldmark, Q3 audience) and external
tasks (hugoBasicExample smoke, hugoThemes registry PR).
- Extract duplicated TOC predicate to _partials/toc-enabled.html
(single.html + footer.html were both computing the same 6-line gate).
- Inline _partials/head/og-image.html into head/seo.html — single caller,
the fallback chain is the contract; the partial wasn't earning its
override surface.
- Drop 6 unused i18n keys: postedOn, tags, categories, archive, noResults,
copyright. None referenced by any template (verified via grep). Also
removed an accidental duplicate of the callout title block; vi.yml
reorganized into thematic groups.
Behavior unchanged. Smoke (11 checks) + Hugo build clean. CSS bundle
unchanged at 3962 B gz.