The README hero (banner-animated.svg, and its static twin banner.svg)
rendered the wordmark and tagline as live <text> in Space Grotesk /
Inter. GitHub serves README images through a sandboxed <img>/camo
context that can't load webfonts, so nearly every visitor saw the
wordmark in Helvetica/Arial fallback — a name-treatment violation on
the most visible surface (issue #292).
Outline both <text> elements to vector <path>s (both fonts are OFL, so
outlining is permitted): the wordmark in Space Grotesk Medium 500 at
-0.02em tracking and the tagline in Inter Medium 500, generated via
HarfBuzz shaping + fontTools so the geometry is identical to a
font-correct render. Only the two text elements changed — the mark,
motif, gradient, and SMIL animate nodes are byte-identical. Fills stay
Ion (#F3F0FF) / Lilac (#B7A8FF). Both banners now carry zero font
dependency and render true letterforms on every consumer.
Documented the outlining + regeneration recipe in assets/brand/README.md.
Co-Authored-By: Claude <bot@wafflenet.io>
The reactive "unable to obtain file audio codec" ffprobe stderr was
unconditionally mapped to NoAudioError, overriding positive audio
evidence from --dump-json. A real HEVC/h265 TikTok video whose local
ffprobe can't introspect the stream was wrongly declared a slideshow.
Gate the reactive slideshow verdict on dump evidence: when the dump
proves audio exists (top-level acodec or any format acodec other than
'none'), a NO_AUDIO_STDERR match now becomes a new terminal
AudioCodecReadError pointing at the ffmpeg-path setting / yt-dlp update
instead of the slideshow message. The reactive NoAudioError is only
trusted when the dump was null/inconclusive.
Co-Authored-By: Claude <bot@wafflenet.io>
Caption transcripts rendered as one wall of text. The caption stream
carries its own structure, no AI needed: `>>` is the broadcast-captioning
convention for a speaker change (each turn becomes a paragraph), creators
declare chapters as `MM:SS Title` description lines (each becomes a ###
heading linked to that timestamp in the video), and cue timing exposes
real pauses (paragraph breaks for marker-less ASR, with a force-break so
no paragraph grows unreadable).
The json3 parser now keeps per-cue timing; strongly structured output
(speaker turns or chapters) skips the AI post-processing pass entirely —
a rewrite would only flatten structure the stream itself declared — while
plain ASR keeps the existing post-processing path.
Verified live: a 70-minute podcast episode formats into 7 linked chapter
headings and 260 dialogue paragraphs in under a second.
Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
Stripping filler is a per-content-type call — right for voice memos,
wrong for interviews, talks, and video transcripts the user wants
verbatim — and an always-on default is exactly the kind of forgotten
setting that silently rewords content. Off by default for new installs
(existing vaults keep their saved value); the toggle now explains when
to use it. Whether this belongs in tidy instead is part of the #465
UX review.
Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
Post-processing rewrites the transcript, so the response needs roughly
as many tokens as the input — but every provider dispatch caps output at
ai.maxTokens (default 2048), silently truncating anything past ~1,500
words, and the cut-off text landed in `processed`, which every consumer
prefers over `raw`. Caption-first transcription (#184) made this common
by unlocking long videos Whisper's upload cap used to keep out.
Guard: estimate the needed budget (chars/4) and keep the raw transcript
with a console diagnostic when it exceeds maxTokens — raw-but-complete
beats clean-but-truncated. Chunked processing for long transcripts is
tracked in #467.
Closes#466
Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
A blocking follow-up decision deserves a first-class surface: the
trim-range picker is now a TimeRangeModal (front and center, can't be
lost to a stray click) replacing both the slider toast and the fallback
text-input toast. Dismissing it resolves 'cancelled' — do nothing —
instead of silently transcribing the whole file.
The trim bar's handles now actually overlay the bar: Obsidian styles
input[type='range'] itself at higher specificity than a bare class, so
every slider rule is prefixed with the attribute selector. The duplicate
range readout above the bar and the redundant "Duration:" label are
gone — the live start/end endpoint labels say it all.
Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
The toast clipped its own labels and buttons: the min-width sat on the
inner .notice-message (the #361 lesson), so the outer .notice never
grew. Size now lands on the .notice via :has(), keyed on the shared
title class so the fallback text-input toast is covered too.
Visual redesign: the slider reads as a video trim bar — vertical pill
in/out handles and a solid gold selection fill (--synapse-gold, the one
brand gesture) on a quiet rail, replacing the overlapping accent-colored
blobs. Eyebrow + two-line-clamped title replace the wrapping "Synapse:"
prefix; endpoint labels get tabular numerals; actions right-align with
the primary button last; handles get grab cursors and a visible
keyboard-focus ring. Verified with headless-Chromium renders of the
notice DOM in dark and light themes.
Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
The "Transcribe media" note flow (VideoModule.transcribeAndInsert) still
called processUrl directly, bypassing the tier router — a captioned
YouTube video was forced through yt-dlp + Whisper and long videos failed
on the provider size limit (413). main.ts now injects the router into
VideoModule so the batch path is caption-first like every other URL
path, with direct extraction kept as a defensive fallback.
Also removes the now-dead VideoModule.transcribeUrl and
transcribeUrlToActiveNote — both had no callers since the router took
over the summarize and modal paths.
Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
YouTube URLs now transcribe on every platform, including mobile, through
a tiered UrlTranscriptionRouter: a zero-dependency caption fetcher
(Innertube ANDROID primary, watch-page fallback — web track URLs are
POT-gated and fetch empty) tries first, and the desktop yt-dlp/ffmpeg
pipeline covers TikTok/Instagram and caption-less videos. The intake
media-URL stub (#112) is wired to the router and now runs the full
pipeline on the transcript-bearing note; failures leave the note
un-stamped so a synced desktop vault finishes what mobile can't. The
opt-in "adopt shared captures" intake setting moves root-level bare-URL
notes (Android share-sheet captures) into the intake folder (#455).
The transcript-level strategy seam deliberately replaces #180's
audio-returning MediaExtractor design — captions never produce audio,
and the Phase 2 server extractor (#181) slots in as a third strategy.
See the 2026-07-14 DECISIONS.md entry.
Verified live against real YouTube (caption fetch end-to-end) plus 40+
new unit tests across the fetcher, router, strategies, intake adoption,
and modal.
Closes#184, closes#112, closes#455
Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
STATUS.md current-focus and counts refreshed; DECISIONS.md gains the
no-unredacted-console gate (#418) and automated-review triage (#454)
entries; ARCHITECTURE.md security layers and audit status extended.
Co-Authored-By: Claude <bot@wafflenet.io>
Document the no-unredacted-console lint gate (root + shared AGENTS.md)
and fix stale file:line references across 9 feature AGENTS.md files.
Co-Authored-By: Claude <bot@wafflenet.io>
Obsidian-compliance audit pass: three notification strings started
lowercase against the repo-wide sentence-case convention; test fixture
updated to mirror the corrected copy.
Co-Authored-By: Claude <bot@wafflenet.io>
The 8 raw-error console sinks listed in #418 were already routed
through redactError() by the 2026-07-02 audit (PR #451: fd56a30,
ec7e4e0). This adds the remaining deliverable: an enforcement guard so
the "every raw-error console sink goes through redactError()" contract
cannot silently regress.
New type-aware ESLint rule `synapse/no-unredacted-console`
(scripts/eslint-rules/no-unredacted-console.mjs), enabled in
eslint.config.mjs for shipped code (tests/mocks excluded, mirroring the
obsidianmd scope): every value reaching a console.* sink must be
statically string-like — checked at the argument top level, inside
template substitutions, and across `+` concatenation. redactError()/
redactSecrets() return string, so sanctioned sites pass with no
function-name allowlist. String()/JSON.stringify() of non-string values
and direct .message/.stack access are flagged as stringification-is-
not-redaction bypasses. The rule fails closed (throws) if type info is
unavailable. Picked up automatically by `npm run lint` in CI.
Co-Authored-By: Claude <bot@wafflenet.io>
Triage the v1.0.11 automated plugin-review dump into fix /
document-as-intended / rebut / untyped-analysis-artifact.
- summarize: replace deprecated querySelectorAll + Array.from().find()
with Obsidian's typed containerEl.findAll(); feature-detect findAll so
the vitest env no-ops cleanly. Add a recursive findAll stub to the
obsidian mock and a test that exercises the scroll-reveal path.
- docs: add docs/automated-review-notes.md, a reviewer-facing companion
declaring desktop-only Node usage (temp-file fs under os.tmpdir with
finally cleanup, execFile-only subprocesses with arg arrays + sanitized
args, the shellEnv() env allowlist, write-only clipboard), the wontfix
rationale for the node-loader require pattern and the :has() toast
selectors, and the false-positive rebuttals (no system-identity reads;
test-only globalThis). References in-code comments rather than
duplicating them.
- README: declare clipboard is write-only and link the reviewer doc.
The no-unsafe-* cascade is an artifact of untyped analysis: npm run lint
(type-aware) exits 0 on main. Full harness reproduction and per-rule
enumeration posted to issue #454.
Refs #454, #389
Co-Authored-By: Claude <bot@wafflenet.io>
- 0.8.0 migration: consumer config trio moves to .waffle/ (waffle.yaml,
waffle.local.yaml, waffle.lock.json); .gitignore paths updated
- 0.7.0 bundle reorg: design bundle dissolved — designer stays as a
project-local agent; architect/security replaced by per-item includes
of engineering-team's lead-engineer/security-engineer; security-audit
skill renamed electron-security-audit (obsidian-dev)
- product-manager is now toolkit-rendered from pm.* config, replacing
the stale local PRD/roadmap variant
- doctor CI workflow re-managed as waffle-doctor.yml via the new
doctor.flags: --allow-missing (previously ejected + hand-edited);
audit compliance agent pinned to plugin-architect
- new label-hook workflow + skill render inert (no trigger labels or
ANTHROPIC_API_KEY secret configured)
Claude-Session: https://claude.ai/code/session_01CCfj4i42BWiaHHWMHQGeKR
Co-Authored-By: Claude <bot@wafflenet.io>
Keeps the redactError coverage enumerations exact after security
pass 2 routed image/preprocess, notifications, and video
settings-section clipboard catches through the scrub.
Co-Authored-By: Claude <bot@wafflenet.io>
Security audit pass 2 (step 6/6): the image-downscale fallback warn
and the two clipboard-copy error catches (notifications, video
settings) logged raw error objects; route them through redactError.
Co-Authored-By: Claude <bot@wafflenet.io>
Docs-agent pass (step 4/6): record the expanded redactSecrets/
redactError consumer lists (main.ts, credential-field, update-checker)
and the title module's barrel re-export of isUntitled.
Co-Authored-By: Claude <bot@wafflenet.io>
Security audit pass 1 (step 2/6): five console.warn/error sites and the
credential Test-button error path logged raw error objects that could
leak API keys embedded in provider error messages. Route them through
the existing redactError/redactSecrets helpers.
Co-Authored-By: Claude <bot@wafflenet.io>
Architecture audit (step 1/6): the title module re-exported isUntitled
from the internal shared/title-detector file instead of the ../shared
barrel, violating the shared-import rule.
Co-Authored-By: Claude <bot@wafflenet.io>
Upgrade the toolkit render from 0.3.0 to 0.6.0. The 0.6.0 migration
renames the consumer dotfiles .wafflestack.* -> .waffle.*; .gitignore
is updated to match (the CLI does not edit it).
The merged .waffle.yaml keeps the git identity block main gained in
PR #448 — the upgrade migrated a stale pre-#448 local copy, so the
block was re-added and the tree re-rendered before committing.
The new wafflestack-doctor CI workflow is ejected from toolkit
management and hand-pinned to v0.6.0 with --allow-missing, because
this repo deliberately gitignores some renders (.agents/, .codex/,
and the .claude skills that embed the local overlay); the stock
render would fail on every CI checkout. Verified against a simulated
tracked-files-only checkout: 24 renders absent and tolerated,
everything else matches the lock.
Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_01FG1dCMAp225RdVmp8GsN2a
Adopt the replaced assets/brand kit across the plugin and repo:
- Regenerate src/brand-icons.ts from the new assets: glyph bodies stay
currentColor, each may carry ONE gold gesture via
var(--synapse-gold, #FFD23F); synapse-actions is now the S-Signal at
glyph weight (brain retired), synapse-main stays impulse-free.
- Relax the brand-icons test color rule: the gold var's canonical
fallback is the only hex a glyph may bake.
- Theme the gold in styles.css (#FFD23F, deepened to #E8B419 on
.theme-light for contrast on white).
- README: hero uses banner-animated.svg (SMIL spike draw-on); sponsor
badges move from the retired violets to Iris/Gold. Same palette note
updated in the donation plan doc.
- Restore the SMIL animations that were stripped from the delivered
banner-animated.svg and accept-flash.svg (reconstructed from the
accept-flash.md spec; scaffolding was intact, animate nodes missing).
- Re-rasterize social-preview.png (1280x640, 679 KB) from the new SVG.
- Sync package-lock version to 1.0.10 (validated with npm@10 ci).
Log the palette/exception decision in DECISIONS.md.
Co-Authored-By: Claude <bot@wafflenet.io>
Upstream (wafflestack PR #1, from the plotto consumer) made bot identity
optional: git.cmd / git.coAuthorTrailer / git.identitySection now default
to the developer's own git config. Synapse opts back in via committed
config that references {{git.botEmail}}/{{git.botName}} through nested
substitution, keeping the literal identity in the gitignored local
overlay exactly as before.
Also adopted from upstream: `tools:` frontmatter (was allowed-tools),
issue-skill label-taxonomy guardrails + native sub-issue linking, new
task-planner agent and clean-up skill, delegate worktree-bug workaround.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5rA4nHBQrjk9cPCHEwFCk
Agent and skill definitions previously hand-maintained (and hand-mirrored
into .codex/ and .agents/) are now rendered from the private
dustinkeeton/agent-toolkit repo (v0.1.0). The committed
.agent-toolkit.yaml carries the project config (roster, module maps,
threat model, tdd tiers, plugin manifest values); account-specific bot
identity lives in the gitignored .agent-toolkit.local.yaml overlay.
Rendered output is byte-identical to the previous hand-maintained files.
Newly tracked: designer + project-manager agents and the
github-project-management skill (rendered clean of account refs, so the
PR #290-era gitignore entries no longer apply). delegate and
git-workflow renders embed the bot identity and stay ignored, as do the
project-local brand-manager/product-manager/brand-guidelines files.
closes#445
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5rA4nHBQrjk9cPCHEwFCk