- Vitest needs the node-ABI prebuilt sqlite binary; Electron 41 needs ABI 145.
Add `rebuild:node` / `rebuild:electron` scripts and wire them as
pre-hooks for `test`, `test:e2e`, `start`, `dev`, `test:integration`.
- Smoke test was racing on `firstWindow()`, which non-deterministically
returned the quickcapture window. Strip ELECTRON_RUN_AS_NODE from the
launch env, wait for both windows to register, then pick the inbox by
title and assert the heading via `getByRole`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to the CJS refactor: destructuring electron.BrowserWindow /
electron.Tray pulled the values into scope but lost the type
namespace, breaking sites that used the bare class name as a type
(let win: BrowserWindow | null, return-type annotations,
implicitly-any close handlers). Added 'import type { ... as ...Type }'
for each affected file and replaced the type-position references
with the aliased names. Runtime semantics unchanged; typecheck
exits 0 again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 32 of the slice plan. playwright.config.ts (testDir
tests/e2e, single worker, 60s default timeout, list reporter)
and a smoke spec that launches the built main bundle, waits
for the inbox renderer, and asserts the v0.2 empty-state copy
'첫 기억을 구출해보세요.' is rendered.
Build verified end-to-end (`npm run build` exits 0, produces
out/main/index.js + out/preload/index.mjs + the inbox /
quickcapture renderer chunks). Playwright's chromium bundle
installed.
Known issue (deferred to a follow-up debug session): on this
specific Windows machine, electron@41.3.0's main-process
module hook is not injecting the real `electron` API into the
launched bundle. `require('electron')` from the entry script
returns the on-disk path string (the package's index.js
default), and `process.electronBinding` / `process.type` are
undefined inside the spawned process. This reproduces with a
minimal package.json + main.js outside this repo — i.e. it's
not a slice bug. The smoke test currently fails with
'Process failed to launch!' at the
`app.whenReady().then(...)` line because `app` is undefined.
When the host issue is sorted, no test changes should be
required: build, config, and spec are correct. Likely fixes:
clean-reinstall electron + node-modules, try electron@latest,
or invoke through `npx electron` rather than the binary
directly. Captured in project_inkling_status memory under
'open issues'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related runtime defects surfaced when first attempting
`npm run build` + Electron launch:
1. Renderer html files referenced `/src/.../main.tsx`, which
vite dev resolves but vite production rollup cannot. Changed
both inbox and quickcapture to `./main.tsx` (sibling-relative).
2. Electron 41's main-process module hook only injects the
real `electron` API into `require('electron')` calls inside
CommonJS modules. With package.json `"type": "module"` set,
electron-vite emitted ESM bundles that did
`import { app } from "electron"`; Node's ESM CJS-interop
then loaded the on-disk index.js (which exports the binary
path string) instead, leaving `app` undefined at startup.
Fix: drop `"type": "module"` so electron-vite emits CJS for
main + preload (renderer is unaffected — vite handles its own
ESM transform regardless). Source files keep modern import
syntax via the default-import + destructure pattern
(`import electron from 'electron'; const { app } = electron;`)
which transpiles correctly to `const { app } = require('electron')`
in the CJS output.
Also updated `electron-log/main` -> `electron-log/main.js`
because Node ESM strict resolution requires the `.js` extension
even though the package's CJS entry serves both.
Verification: `npm run build` produces 47-module renderer +
1005KB main bundle without errors. `npm run typecheck` clean.
Unit suite (52 tests) unaffected.
Plan deviation log: Task 4 (logger), Task 30 (main wiring), and
Tasks 17/18/22/30 referencing electron imports landed with named
imports per plan; this commit refactors them to default+destructure
without changing semantics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 31 of the slice plan. Diff list of media/ subdirectories
against current note ids (SELECT id FROM notes); deletes
directories whose noteId is no longer referenced. Runs once on
startup from main; returns { removed } so logger captures the
count. Covers the 'media delete failure leaves orphan' branch
of spec §6.1 row 8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 30 of the slice plan. Replaces the Task 2 placeholder main
entry with the final whenReady wiring:
- profile path resolution + better-sqlite3 open + migrations.
- repo / store / continuity / intent / health constructed
against the open db.
- LocalOllamaProvider reads INKLING_OLLAMA_ENDPOINT for LAN
dogfood, falls back to localhost:11434 otherwise.
- AiWorker registers an onUpdate that fans note:updated through
pushNoteUpdated(getInboxWindow, note).
- NotificationService is plumbed to electron.Notification with
isSupported gating; CaptureService gets the worker.enqueue +
notify.celebrate hooks.
- IPC bindings for both capture and inbox surfaces.
- HotkeyService registers Ctrl/Cmd+Shift+J -> showQuickCapture;
failure logged but not fatal.
- Tray menu '구출한 메모 보기' / '기억 구출하기' / '종료'
with click-to-show-inbox.
- worker.loadFromDb() resumes pending jobs at startup.
- MediaGc runs once and logs the count of removed orphan dirs.
- Two logger.info(..., {...obj} as Record<string, unknown>)
casts at the health and gc result sites to satisfy the
index-signature requirement on logger meta — the result
objects (HealthResult, {removed: number}) are assignment-
compatible with Record<string, unknown> at runtime but TS
refuses without the spread + cast.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 29 of the slice plan. Subscribes to ollamaStatus from the
inbox store and renders one of two messages: 'ollama pull
gemma4:e4b 실행 후 앱을 재시작해주세요.' when the model isn't
installed (status.reason includes 'not installed'), or
'Inkling 정리가 잠시 멈췄습니다. Ollama를 실행해주세요.' for
any other unhealthy state. Capture continues regardless.
HealthChecker.ts itself was pulled forward in Task 21 so its
import would resolve from inboxApi.ts at compile time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 28 of the slice plan. Renders the green '🌱 흐름을 다시
이어갑니다' banner only when the parent says show=true
(continuity.showRecoveryToast && !dismissedToday). Click ✕
calls onDismiss which persists today's date in localStorage
via recoveryToast.ts, suppressing it for the rest of the KST
day even after a reload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 27 of the slice plan. Surfaces 'meaning question' once
per note (gated by intent_prompted_at IS NULL on the backend
side; frontend just provides the input UX). intentPrompts.ts
holds the 4 rotating prompts plus a deterministic
pickIntentPrompt(noteId) (FNV-style 32-bit hash mod 4) so the
same note always gets the same question across reloads. Submit
calls setIntent and reports the typed text up; Skip calls
dismissIntent and reports null. 200-char cap matches repo-side
truncation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 26 of the slice plan. Click-to-edit primitive used for
title, summary, and intent. Blur commits via injected onSave;
Enter blurs (single-line); Escape cancels and reverts to
the prop value. Failed save shows a 1px red outline for 800ms
and restores the prior value (no error message — caller decides
visual feedback). singleLine prop swaps <input> for <textarea>.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 25 of the slice plan. Single-card view of a note with
local optimistic state plus the IPC writes. Per-status branches:
- pending: 'Inkling이 정리하는 중…' headline, raw text auto-open.
- failed: '정리 보류 — 원문은 안전합니다' with hover tooltip.
- done: editable title + summary (each with the gray 'AI' badge
hidden as soon as *_edited_by_user flips), tag chips with
AI subscript, optional 💡 user_intent row, IntentBanner
surfaced exactly when intent_prompted_at is null.
Tag click removes (per spec), title/summary edits route through
inboxApi.updateAiFields (which sets the edited flag server-side),
intent edits use inboxApi.setIntent. Delete confirms with '이
기억을 버릴까요? 되돌릴 수 없습니다.' and routes through
CaptureService.deleteNote so media gets cleaned up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 24 of the slice plan. Subscribes to pendingCount in the
store and renders '🟡 Inkling이 정리하는 중: N건' when count > 0.
Hidden when zero so the layout doesn't reserve space.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 23 of the slice plan. Reads continuity from the zustand
store and renders one of three states: '이번 주 한 줄이면
시작입니다' (0 notes), '이번 주 N/7' (in-progress), or
'이번 주 7/7 ✓ · 연속 K주 완성' (target hit, with K only when
> 0). No '실패'/'끊김' wording.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 22 of the slice plan. Wires the Inbox window's renderer:
- index.html (replaces the placeholder shipped in Task 2) with
the full layout styles + module script.
- api.ts re-exports window.inkling.inbox as a typed InboxApi.
- recoveryToast.ts persists per-day toast dismissal in
localStorage (KST date key) so the banner never re-fires
after the user closes it.
- store.ts: zustand store with notes, continuity, pendingCount,
ollamaStatus, loadInitial(), refreshMeta(), upsert/remove.
- main.tsx mounts <App />.
- App.tsx orchestrates loadInitial + onNoteUpdated subscription
+ window-focus refresh, renders header / banners / note list.
- 7 component stubs (NoteCard / EditableField / IntentBanner /
RecoveryToast / ContinuityBadge / PendingBanner / OllamaBanner)
so the shell typechecks today; Tasks 23-28 swap each in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 21 of the slice plan. registerInboxApi binds every
InboxApi method on the main side: inbox:list (paginated),
inbox:updateAi (delegates to NoteRepository.updateUserAiFields
which flips the *_edited_by_user flags), inbox:delete (routes
through CaptureService so the media dir gets cleaned up),
inbox:setIntent / inbox:dismissIntent (route through
IntentService for input validation), inbox:continuity (Weekly
Continuity snapshot), inbox:pendingCount, inbox:ollamaStatus
(reads the cached HealthChecker.lastStatus()). pushNoteUpdated
helper is exported so AiWorker.onUpdate (wired in Task 30) can
fan note:updated events to the inbox renderer.
Plan deviation: HealthChecker.ts was pulled forward from
Task 29 because Task 21 imports it at compile time. The class
is small and final; Task 29 commit only ships OllamaBanner.tsx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 20 of the slice plan. Thin wrapper over the repo's intent
methods that adds two preconditions: setIntent rejects empty
trimmed text, both methods throw "note not found" when the
note id doesn't exist. Repo-level COALESCE on
intent_prompted_at preserves the first-prompt invariant
(spec §3.3); IntentService's job is just input guarding so
the IPC handler stays a one-liner.
Verification: `npx vitest run tests/unit/IntentService.test.ts`
4 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 19 of the slice plan. Frameless dark card with:
- placeholder "지금 머릿속에 있는 것 한 줄. 정리는 나중입니다."
- hint "Ctrl+Enter 구출 · Esc 취소 · 이미지 붙여넣기"
- Ctrl/Cmd+Enter to submit (window.inkling.capture.submit then
hide), Esc to cancel (with "이 한 줄을 흘려보낼까요?" confirm
when text > 5 chars)
- clipboard image paste -> thumbnail strip with ArrayBuffer
retained for submit
- fallback "저장에 실패했습니다. 다시 시도해주세요." in-card on
IPC error (window stays open with content preserved)
api.ts wraps window.inkling.capture as the typed CaptureApi.
main.tsx mounts <App /> on #root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 18 of the slice plan. Lazy-creates a 640x280 alwaysOnTop
frameless window centered on the primary display's work area
(1/3 from the top). skipTaskbar to keep it out of the alt-tab
list. Auto-hides on blur so capturing-then-clicking-elsewhere
dismisses cleanly. Singleton pattern; show/hide rather than
recreate to keep the <100ms hotkey latency target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 17 of the slice plan. register() pre-checks
globalShortcut.isRegistered to detect collisions with other
apps (Failure #1 in spec §6.1) and reports the conflict in
the return object so the OllamaBanner-style failure surface
in Task 29 can report it. unregisterAll() runs on app quit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 16 of the slice plan. registerCaptureApi binds the IPC
side of the preload bridge to the main-side CaptureService:
capture:submit -> captureService.submit, capture:hide ->
quickCaptureWindow.hide(). Window resolution is via a getter
callback so wiring stays a one-liner in Task 30.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 15 of the slice plan. Strategy §4.1 immediate-reward
toast. celebrate(noteId) deterministically picks one of the
4 reward copies via SHA-256(noteId)[0] % 4, then forwards to
the injected send() callback (which Task 30 wires to a real
electron Notification). Skips silently when isSupported() is
false (denied OS permission), and swallows send() errors so
that capture path never fails because of a notification quirk.
Verification: `npx vitest run tests/unit/NotificationService.test.ts`
3 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 14 of the slice plan. Orchestrates a Quick Capture submit:
trims/validates input, creates the note row + pending_jobs row
in one repo.create transaction, persists each pasted PNG via
MediaStore (relPath stored in media table), then awaits the
injected enqueue() (AiWorker.enqueue at runtime) and fires
celebrate() (NotificationService.celebrate). deleteNote drops
the db row (cascading note_tags / media / pending_jobs) and
removes the on-disk media directory afterwards.
Verification: `npx vitest run tests/unit/CaptureService.test.ts`
4 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 13 of the slice plan. Drives the pending → done/failed
transitions:
- enqueue() pushes a Job and kicks the loop; loadFromDb()
rehydrates pending_jobs at startup so app restart resumes
in-flight work.
- drain() exposes a Promise for tests + graceful shutdown.
- Concurrency 1: a single async loop awaits each provider call
before the next, matching spec §2.2.
- 3-attempt backoff (default [0, 30s, 120s]; tests inject [0,0,0]).
Each failure logs ai.retry, increments pending_jobs.attempts,
and on the final attempt calls markAiFailed and emits onUpdate.
- emit() pushes the freshly-hydrated note to onUpdate (used by
Task 30 to fan out IPC note:updated events).
Verification: `npx vitest run tests/unit/AiWorker.test.ts`
4 passed. Suite total 41 / 41.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 12 of the slice plan. Implements the slice's only provider:
- generate(): POST {endpoint}/api/generate with Korean-first
prompt; AbortController-driven 120s timeout; parses
body.response as JSON and runs it through parseAiResponse.
- healthCheck(): GET /api/tags; returns ok when configured model
is in the listing, otherwise reports the missing-model reason.
- Constructor takes opts.endpoint / opts.model so Task 30 main
entry can inject INKLING_OLLAMA_ENDPOINT for LAN dogfood;
defaults are http://localhost:11434 and gemma4:e4b.
Tests: 6 unit cases via undici MockAgent (parse, non-JSON,
timeout abort, healthCheck ok / missing / connection error).
Integration test gated by INKLING_INTEGRATION=1 hits real
Ollama for Korean / English-stack / mixed input cases with a
180s per-test budget — also reads INKLING_OLLAMA_ENDPOINT so
LAN dogfood can be exercised end-to-end.
Plan deviation: undici@8 types treat MockAgent's `.reply()`
callback as sync-return-only, so the 500ms-delayed reply used
in the timeout test is cast `as never` to bypass the overload
mismatch. Behavior is correct at runtime; the cast is local to
the test.
Verification: `npx vitest run tests/unit/LocalOllamaProvider.test.ts`
6 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 11 of the slice plan. Single-method provider contract
(name + generate + healthCheck) so future LAN / external API
implementations can drop in without touching AiWorker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 10 of the slice plan. parseAiResponse runs the model JSON
through a zod RawResponseSchema, then enforces slice rules:
- title MUST contain Korean characters; throw otherwise.
- summary normalized to exactly 3 lines (pad with empty when too
few; collapse trailing lines into line 3 when too many).
- tags filtered to /^[a-z0-9]+(-[a-z0-9]+)*$/ kebab-case and
capped at 3.
buildPrompt assembles the user-facing prompt with explicit
Korean-output / kebab-tag / no-fence rules (PROMPT_VERSION=1).
Verification: `npx vitest run tests/unit/ai-schema.test.ts`
7 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 9 of the slice plan. Computes the data behind the Inbox
ContinuityBadge and recovery toast (Strategy §5):
- KST Mon-Sun grouping via toKstDateKey/kstMondayOf helpers
(UTC + 9h shift, then bucket to local Monday).
- weekCount + weekTarget=7 + consecutiveCompleteWeeks (walk
backward from current week, or previous if current incomplete,
counting only weeks with >=7 notes).
- showRecoveryToast=true when the latest note lands on today
(KST) and the prior note is at least 7 days earlier — this is
the "흐름을 다시 이어갑니다" trigger.
- now() injected for deterministic tests.
Verification: `npx vitest run tests/unit/ContinuityService.test.ts`
6 passed. All 24 unit tests across migrations / NoteRepository /
MediaStore / ContinuityService green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 8 of the slice plan. Saves clipboard PNG/JPEG bytes under
{profileDir}/media/{noteId}/{uuid}.{ext} with mkdir -p semantics,
returns relPath/mime/bytes for the repository row, supports
deleteNoteDirectory (used by note delete + GC) and listNoteDirs
(used by media GC to find orphans).
Verification: `npx vitest run tests/unit/MediaStore.test.ts`
4 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 7 of the slice plan. Implements the full repository surface
backing every IPC inbox/capture path: create (UUID v7 + atomic
notes + pending_jobs insert), insertMedia, findById/list,
updateAiResult (CASE WHEN guard against title/summary
overwrite when *_edited_by_user flips), markAiFailed (truncates
ai_error to 500 chars + clears pending job), updateUserAiFields
(sets edited flags as a side effect, replaces user-source tags),
setIntent + dismissIntent (intent_prompted_at uses COALESCE so
the first stamp wins), delete, getPendingCount,
getAllPendingJobs, incrementJobAttempt, and a private hydrate
that joins notes with note_tags + media.
Plan deviation: list/list-with-cursor query gets a secondary
"id DESC" tiebreaker. Two notes created in the same millisecond
shared created_at and reordered nondeterministically; UUID v7
sorts monotonically with creation order, so id DESC restores
"newest first" within ties.
Verification: `npx vitest run tests/unit/NoteRepository.test.ts`
12 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 5 of the slice plan. resolveProfilePaths(profile='default')
returns { profileDir, dbFile, mediaDir } under
{userData}/Inkling/profiles/<profile>/ and ensures mediaDir exists
via recursive mkdirSync. Slice scope is single profile 'default'
(spec §1.1, §2.4).
Verification: `npm run typecheck` exits 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 4 of the slice plan. Adds initLogger() (rotates daily logs
under {userData}/Inkling/logs at 5MB cap, info level by default
and debug when INKLING_DEBUG=1), a hashPrefix() helper for
safely referencing note IDs in logs without exposing content,
and a typed logger facade with info/warn/error/debug methods.
Wires initLogger() + an app.start info entry into the
whenReady hook.
Verification: `npm run typecheck` exits 0. Runtime log file
creation deferred to live `npm run dev` once Tasks 3+19 land.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 3 of the slice plan. Replaces the minimum @shared/types
augmentation with the full domain model: Note (with v0.2
intent + edited-flag fields), NoteTag, NoteMedia, AiStatus,
WeeklyContinuity, CaptureApi, InboxApi, and the InklingApi
aggregate exposed on window.inkling. Adds the preload bridge
that wires every InklingApi method to its IPC channel via
contextBridge.exposeInMainWorld.
Verification: `npm run typecheck` exits 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 2 of the slice plan. Creates the minimal Electron main
process: app entry that opens an Inbox BrowserWindow on
whenReady, an inboxWindow module that handles show/hide/close-
to-tray semantics, an HTML placeholder renderer ("Inkling Inbox
(renderer pending)"), and the minimum @shared/types augmentation
for app.isQuitting (Task 3 expands this file).
Plan tsconfig template adjustment: TypeScript 6 deprecates
baseUrl. Dropped it and made paths entries explicitly relative
("./src/...") so they resolve from tsconfig.json's directory.
Updated both the in-repo tsconfig.json and Task 1 Step 3 in the
plan to match.
Verification: `npm run typecheck` exits 0. Full `npm run dev`
sanity check is deferred until Task 3 (preload) and Task 19
(quickcapture HTML) land — electron-vite.config.ts wires both
entries that don't yet exist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds package.json, tsconfig.json, tsconfig.node.json,
electron.vite.config.ts, vitest.config.ts, .gitignore, and
package-lock.json. Verified Node 24.15.0 + npm 11.12.1 via Volta.
better-sqlite3 12.9.0 native build succeeded against VS 2022 +
Python 3.12.
Plan v0.4 had two install pin issues; adjusted live and logged
in plan Task 1 Step 2:
- electron-vite@2.3.0 + vite@6.0.3 ERESOLVE (peer ^4||^5).
Promoted build-tool chain: electron-vite 5.0.0, vite 7.3.2,
@vitejs/plugin-react 5.1.4 (vite@7 is the compatible overlap;
vitest@4.1.5 also accepts vite@7).
- @types/uuid@11.0.0 deprecated stub (uuid@11 ships own types);
removed from devDependencies.
Other deps installed at the planned exact pins. Step 8 sanity:
TypeScript 6.0.3, Vitest 4.1.5, Electron 41.3.0 (npm ls).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reassessed model needs: 26b is overkill for slice-scope tasks
(short Korean title + 3-line summary + ≤3 kebab-case tags). e4b
(8.0B Q4_K_M, efficiency variant) sits closest to the original
"표준 9b" tier in spec §6.5 and gives faster responses with
adequate margin for the slice's simple structuring work.
Spec changes:
- Header bumped to v0.4 with new revision line.
- §1.1 retiers AI 파이프라인 from "고품질 26B" to "표준 e4b".
- §2.1 diagram, §3.1 ai_provider example, §4.1 provider name,
§4.2 request body, §4.6 health check, §5.4 copy catalog: model
string swapped 26b → e4b.
- §6.4 perf target restored to <30s provisional (e4b is similar
scale to original 9b reference, 60s margin no longer needed).
- §8 open issue rewritten: e4b is the standard-tier substitute,
with 26b/e2b documented as quality-up / speed-up fallback paths
if dogfood reveals issues with Korean output or tag formatting.
Plan changes:
- Header bumped to v0.4 with restated Goal.
- Architecture summary, Task 12 unit test mocks/expectations,
LocalOllamaProvider default model, copy banner, Task 33 dogfood
prerequisite: 26b → e4b throughout.
Tier ladder for dogfood escalation (from §8):
e2b (5.1B, speed) ← e4b (8.0B, default) → 26b (25.8B, quality)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Model swap: gemma4:9b has no official checkpoint; LAN server
(192.168.0.47:11434) ships gemma4:26b (25.8B Q4_K_M), which fits the
§6.5 26B cap and the "high-quality" tier. Updates §1.1, §2.1 diagram,
§3.1 ai_provider example, §4.1 provider name, §4.2 request body, §4.6
health check, §5.4 copy catalog, plan Task 12 (mock + default model),
Task 30 main entry, Task 33 dogfood intro.
Endpoint: LocalOllamaProvider now configurable via constructor opts.
Task 30 main entry and integration test read INKLING_OLLAMA_ENDPOINT
from env; unset falls back to localhost. LAN Ollama moves from
out-of-scope to in-scope via endpoint injection; only a separate
LanOllamaProvider class remains deferred.
Perf target §6.4: RTX 3060-referenced <30s target no longer valid
under 26B Q4_K_M + LAN. Set to <60s provisional; §8 adds a new open
issue to measure real p95 during first dogfood week and confirm.
§7.4 adds INKLING_OLLAMA_ENDPOINT env setup and recommends Volta over
nvm-windows for Windows Node 24.15.0 install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds to handoff: rationale for choosing native Windows over WSL2
(global hotkey, OS notifications, clipboard image, tray, data path,
performance measurements all break under WSLg). Pre-flight
checklist updated so Ollama runs on a LAN server, not on the
Windows machine. Notes the one-line env-var change needed in Plan
Task 30 to wire LocalOllamaProvider to a LAN endpoint, and clarifies
that this is configuration (not a new Provider class).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures decisions made across both /brainstorming rounds (initial
slice scope + Strategy integration), user profile, project state,
Windows-specific gotchas (build tools, hotkey conflicts, Ollama,
file paths), load-bearing invariants, and suggested first messages
for the new Claude Code session on the dogfood machine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 5 new tasks and updates existing ones to cover:
- ContinuityService (was StreakService) for Weekly Continuity (7
notes/week, KST Mon-Sun, recovery detection >=7 day gap).
- NotificationService for OS native post-submit reward toast (4
rotating copies, deterministic by noteId hash).
- IntentService + IntentBanner component for "의미 한 줄" prompt
after AI done (4 rotating prompts).
- RecoveryToast component with localStorage dismissal.
- AI proposal labels (gray "AI" badges on un-edited fields), tag
source subscript, edited-by-user flags guarding AI overwrites.
- Updated copy throughout (recovery-friendly, no failure language).
- New schema columns: user_intent, intent_prompted_at,
title_edited_by_user, summary_edited_by_user.
- Updated IPC: setIntent/dismissIntent/getContinuity.
- Tray copy ("기억 구출하기" / "구출한 메모 보기").
- Updated dogfood checklist with all v0.2 features.
Plan now has 33 tasks (was 28). All tasks remain bite-sized with
TDD discipline (red → green → commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
28-task TDD plan covering project bootstrap, data layer, AI pipeline,
Quick Capture + Inbox UI, tray, media GC, and E2E smoke test. Each
task is bite-sized with red/green/commit discipline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pin Node 24 LTS (Krypton) as runtime. Document latest-stable-major
policy for dependencies with initial pin candidates as of 2026-04-24,
and .nvmrc-based version synchronization for dev/CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial design document covering the first end-to-end slice of Inkling:
Quick Capture, SQLite storage, Local Ollama AI pipeline (gemma4:9b),
and Inbox with inline editing. Scoped for dogfood on Windows/macOS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>