Compare commits

...

1359 Commits

Author SHA1 Message Date
lucaronin
bf3caab862 fix: recover blocked native folder picker 2026-06-07 15:00:51 +02:00
lucaronin
2556907f36 fix: launch Pi CLI from Windows shims 2026-06-07 12:04:55 +02:00
lucaronin
8e3f8fbb77 fix: bound ai agent startup status probe 2026-06-07 05:46:41 +02:00
lucaronin
501df0e265 refactor: share rich editor input transforms 2026-06-07 03:25:09 +02:00
lucaronin
48eac1e40d fix: recover paragraph editor index errors 2026-06-07 02:49:04 +02:00
lucaronin
3d28e21a5a fix: keep palette keyboard selection stable 2026-06-06 14:49:43 +02:00
lucaronin
191066a3bf feat: copy note git URLs 2026-06-06 14:24:51 +02:00
lucaronin
3a88724d8f test: update stable signing bypass guard 2026-06-06 12:06:17 +02:00
lucaronin
65ea3f37fc ci: allow v2026-06-06 stable without windows signing 2026-06-06 11:42:57 +02:00
lucaronin
0e81f20e31 docs: add release notes for v2026-06-06 2026-06-06 11:32:51 +02:00
lucaronin
3c8edd8346 fix: let OpenAI API create notes 2026-06-06 10:42:33 +02:00
lucaronin
80ee94da73 fix: localize highlight toolbar copy 2026-06-06 10:35:21 +02:00
lucaronin
008e8f3471 fix: support rich-editor markdown highlights 2026-06-06 10:15:44 +02:00
lucaronin
6aaaca8e24 fix: sync mcp server lockfile 2026-06-06 09:31:38 +02:00
lucaronin
c5dc4a9300 fix: handle symlinked gitignored vault paths 2026-06-06 08:21:49 +02:00
lucaronin
25c7ba0926 test: cover stack-based null append recovery 2026-06-06 04:24:52 +02:00
lucaronin
2b1f4395b3 fix: recover null editor transform errors 2026-06-06 03:57:21 +02:00
lucaronin
e3ef3f6073 test: stabilize wikilink ime composition test 2026-06-06 03:50:51 +02:00
lucaronin
8d47b8d7d2 fix: correct Chinese file manager labels 2026-06-06 03:45:14 +02:00
lucaronin
8fc366c8c0 fix: scope math block selection highlight 2026-06-06 03:23:04 +02:00
lucaronin
42d7a3ac44 refactor: share mcp tool orchestration 2026-06-06 02:27:19 +02:00
lucaronin
3e349fa017 fix: render markdown highlight syntax 2026-06-06 01:34:06 +02:00
lucaronin
8138925bb0 fix: localize custom menus and feedback 2026-06-06 00:48:10 +02:00
lucaronin
cc4203ab1c fix: expose mermaid raw edit action 2026-06-05 23:32:01 +02:00
lucaronin
401087a8ef fix: recover plain table row index errors 2026-06-05 22:58:28 +02:00
lucaronin
0053a36b6e fix: guard title focus without block id 2026-06-05 22:36:26 +02:00
lucaronin
762ac4ff48 fix: guard native drag drop payloads 2026-06-05 22:11:49 +02:00
lucaronin
4716003ca1 fix: refresh sync history surfaces 2026-06-05 21:43:53 +02:00
lucaronin
af9cc118e7 fix: inherit shell ai provider env 2026-06-05 20:44:28 +02:00
lucaronin
95bdbc90c7 feat: add whiteboard fullscreen workspace 2026-06-05 20:01:55 +02:00
lucaronin
12a229e604 fix: align inline math with text baseline 2026-06-05 19:21:22 +02:00
lucaronin
cc52a52216 fix: recognize raw editor backslash shortcut 2026-06-05 18:49:14 +02:00
lucaronin
057c73376c fix: preserve rich editor copy html 2026-06-05 18:16:16 +02:00
lucaronin
a194763847 fix: restore editor focus after active refresh 2026-06-05 17:20:43 +02:00
lucaronin
6945de0060 fix: tolerate stale Pi agent config entries 2026-06-05 16:45:59 +02:00
lucaronin
4e81ab8aa3 fix: add MCP create note tool 2026-06-05 16:27:18 +02:00
lucaronin
1309cf322e fix: respect configured Git author identity 2026-06-05 15:55:28 +02:00
lucaronin
c9467711ad fix: keep view filters focused across workspaces 2026-06-05 15:22:13 +02:00
lucaronin
82b2ff2ac4 ci: allow unsigned Windows stable build for v2026-06-01 2026-06-01 16:25:07 +02:00
lucaronin
34a22c485d docs: add release notes for v2026-06-01 2026-06-01 16:13:26 +02:00
Tolaria
c12aa4f4fd fix: keep math source selection readable 2026-06-01 15:42:44 +02:00
Luca Rossi
6c979addb3 Delete docs/LARGE-VAULT-LOADING-QA.md 2026-06-01 12:45:46 +02:00
Tolaria
7fa284f0d0 fix: edit display math as block property 2026-06-01 10:05:11 +02:00
Tolaria
74891e0283 fix: export notes directly to PDF 2026-05-31 15:11:29 +02:00
lucaronin
f003b38518 fix: avoid Windows npm cmd shim launches 2026-05-31 12:13:18 +02:00
lucaronin
b84d6579da fix: refresh clean active notes after external edits 2026-05-30 23:04:02 +02:00
lucaronin
4f52f3e803 refactor: reduce view filter test duplication 2026-05-30 21:39:09 +02:00
lucaronin
83cd7bc20d refactor: improve ai workspace code health 2026-05-30 21:10:41 +02:00
lucaronin
0cdf758f7d fix: localize ai workspace copy 2026-05-30 04:19:51 +02:00
lucaronin
cba038766c refactor: share ai workspace cross-window store 2026-05-30 03:49:40 +02:00
lucaronin
944ab9705a refactor: extract app shell side effects 2026-05-30 03:23:29 +02:00
lucaronin
1a08102188 refactor: extract app orchestration hooks 2026-05-30 02:43:28 +02:00
lucaronin
1633f10752 fix: swallow stale tauri listener cleanup 2026-05-29 21:12:23 +02:00
lucaronin
c09f1b9c93 test: stabilize ai workspace title persistence 2026-05-29 20:46:35 +02:00
lucaronin
8cee321ebe feat: add common code block grammars 2026-05-29 20:35:53 +02:00
lucaronin
f79cdac570 fix: require signing for stable windows releases 2026-05-29 19:43:48 +02:00
lucaronin
10aed871da feat: export notes as pdf 2026-05-29 19:37:38 +02:00
lucaronin
30cde097d2 ci: allow unsigned Windows stable build 2026-05-29 19:04:15 +02:00
lucaronin
d4e66443bd docs: add release notes for v2026-05-29 2026-05-29 18:36:36 +02:00
lucaronin
1d5d573138 feat: refine AI workspace side panel 2026-05-29 18:00:32 +02:00
lucaronin
ed8a0b155b fix: recover block type render mismatch 2026-05-29 17:44:24 +02:00
lucaronin
8b937012f9 fix: restore editable math source 2026-05-29 17:16:26 +02:00
lucaronin
6a80d01c34 fix: create unmatched quick open notes safely 2026-05-29 16:34:05 +02:00
lucaronin
ef55891cc2 fix: preserve katex square root radicals 2026-05-29 11:50:09 +02:00
lucaronin
9a2c025d07 fix: recover table row render errors 2026-05-29 11:20:45 +02:00
lucaronin
9be7049bfd docs: streamline agent instructions 2026-05-29 11:02:13 +02:00
Luca Rossi
ff569bfba4 updated README 2026-05-29 10:54:15 +02:00
lucaronin
34fda19b62 docs: add ADR for request-scoped AI stream events (guard — from commit 78892c4) 2026-05-29 08:01:49 +02:00
lucaronin
b69cf8dd5b refactor: consolidate git remote helpers 2026-05-29 05:18:52 +02:00
lucaronin
0234314945 test: guard note-list body search against stale results 2026-05-29 04:06:09 +02:00
lucaronin
6bf70eb635 refactor: consolidate note-list body search 2026-05-29 03:19:59 +02:00
lucaronin
78892c4637 feat: add side AI workspace 2026-05-29 00:58:40 +02:00
lucaronin
6910347bf3 fix: preserve edited vault guidance status 2026-05-28 22:00:29 +02:00
lucaronin
181ff6de8c fix: search note list body content 2026-05-28 21:19:55 +02:00
lucaronin
07c47e68f9 fix: preserve spaced filenames in pending changes 2026-05-28 20:02:21 +02:00
lucaronin
05a6e61f11 fix: extend favorite row hover state 2026-05-28 15:56:38 +02:00
lucaronin
7de2726f27 fix: resolve packaged macos mcp server path 2026-05-28 11:18:11 +02:00
lucaronin
a45e44d0f3 test: align release workflow assertions 2026-05-28 10:30:42 +02:00
lucaronin
14b1668ebe fix: unblock alpha release without windows cert 2026-05-28 10:18:45 +02:00
lucaronin
c5049e4332 docs: add ADR for reusable release artifact workflow (guard - from commit c6e570d) 2026-05-28 08:01:42 +02:00
lucaronin
c6e570d2e3 refactor: deduplicate release artifact workflows 2026-05-28 01:51:08 +02:00
lucaronin
e93676bbb7 test: format git no remote regression 2026-05-28 00:59:55 +02:00
lucaronin
952e632622 test: stabilize add remote smoke 2026-05-28 00:53:25 +02:00
lucaronin
641dc2c885 fix: merge local-only git remote PR #739 2026-05-28 00:42:44 +02:00
lucaronin
78f8469c98 perf: merge AI agent startup discovery PR #751 2026-05-27 23:59:59 +02:00
lucaronin
1ec5f7c745 fix: complete Indonesian locale keys 2026-05-27 23:20:15 +02:00
lucaronin
f616fe449b feat: merge Indonesian translation PR #757 2026-05-27 23:14:50 +02:00
lucaronin
8ad23f72e1 fix: recover table-cell transform errors 2026-05-27 19:50:16 +02:00
lucaronin
41b531e899 fix: avoid inactive AI chat context work 2026-05-27 19:23:06 +02:00
lucaronin
03a667661e fix: guard whiteboard permission rejections 2026-05-27 18:56:36 +02:00
lucaronin
908994c253 fix: repair malformed nested editor blocks 2026-05-27 18:32:18 +02:00
lucaronin
af83d270fd fix: coalesce overlapping editor saves 2026-05-27 17:37:16 +02:00
lucaronin
24f4ee3272 fix: harden tldraw text measurement guard 2026-05-27 16:42:12 +02:00
lucaronin
bbb30d6b91 refactor: consolidate anchored dropdown positioning 2026-05-27 16:08:24 +02:00
lucaronin
ba3f324a52 fix: sign Windows release installers 2026-05-27 15:27:34 +02:00
github-actions[bot]
5240aa3b4e Merge branch 'main' into feat/add-indonesia-locale 2026-05-27 13:04:29 +00:00
lucaronin
f80e571c78 feat: add vault item deep links 2026-05-27 14:51:32 +02:00
github-actions[bot]
df5c2a46e6 Merge branch 'main' into feat/add-indonesia-locale 2026-05-27 10:53:16 +00:00
lucaronin
5090ed31bb fix: stabilize native search navigation 2026-05-27 12:42:04 +02:00
github-actions[bot]
0bad22eb57 Merge branch 'main' into feat/add-indonesia-locale 2026-05-27 09:39:26 +00:00
lucaronin
05fc93677c fix: stabilize wikilink ime flushing 2026-05-27 11:26:24 +02:00
lucaronin
bdb9fabfd0 fix: stabilize global search keyboard navigation 2026-05-27 11:13:45 +02:00
lucaronin
4296977fe3 test: cover folder note creation event bridge 2026-05-27 04:05:26 +02:00
github-actions[bot]
bb5296ba3d Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 23:39:16 +00:00
lucaronin
cfc62b1ce2 test: document folder parent creation 2026-05-27 01:25:34 +02:00
github-actions[bot]
29bfde6a20 Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 23:18:02 +00:00
lucaronin
759af948b4 fix: polish ai workspace follow-up 2026-05-27 01:03:31 +02:00
github-actions[bot]
5fb1389d69 Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 17:51:07 +00:00
lucaronin
f5475f7773 fix: complete action history undo redo 2026-05-26 19:41:25 +02:00
github-actions[bot]
1d93da3ade Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 17:29:26 +00:00
lucaronin
29e5dd8f7b fix: stabilize AI workspace QA 2026-05-26 19:15:05 +02:00
github-actions[bot]
18c6d18474 Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 15:02:15 +00:00
lucaronin
8828516d43 feat: rework AI workspace window 2026-05-26 16:52:38 +02:00
github-actions[bot]
8df1f3043d Merge branch 'main' into pr-726 2026-05-26 13:35:13 +00:00
github-actions[bot]
95b89e331d Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 13:35:09 +00:00
lucaronin
2382305576 feat: support action history undo redo 2026-05-26 14:56:39 +02:00
lucaronin
b9f90b0657 fix: use cached startup loads for note windows 2026-05-26 12:58:08 +02:00
Aris Ripandi
0f5ac35f67 fix: add missing locales keys for Indonesian translation 2026-05-26 17:21:07 +07:00
Aris Ripandi
6424509bb9 Merge branch 'main' into feat/add-indonesia-locale 2026-05-26 17:10:28 +07:00
Aris Ripandi
7614db62f2 fix: increase sidebar width to avoid truncated label 2026-05-26 17:06:15 +07:00
github-actions[bot]
42dc2f726f Merge branch 'main' into pr-726 2026-05-26 10:05:58 +00:00
lucaronin
56c6cb56d6 feat: create notes inside selected folders 2026-05-26 11:56:28 +02:00
github-actions[bot]
37536e1974 Merge branch 'main' into pr-726 2026-05-26 09:53:49 +00:00
Aris Ripandi
9234ffcac2 Merge branch 'refactoringhq:main' into feat/add-indonesia-locale 2026-05-26 16:49:37 +07:00
Aris Ripandi
198b7c993f feat: add Indonesia translation 2026-05-26 16:47:45 +07:00
lucaronin
ea54560e11 fix: simplify local agent setup surfaces 2026-05-26 11:42:22 +02:00
lucaronin
fbb51a0b4c fix: resolve codacy high severity findings 2026-05-26 10:52:53 +02:00
github-actions[bot]
c8eba73863 Merge branch 'main' into pr-726 2026-05-26 08:42:14 +00:00
lucaronin
ae7ee4064f refactor: keep merged PRs above code health gate 2026-05-26 10:26:29 +02:00
lucaronin
e3bfeba163 Merge remote-tracking branch 'refs/remotes/pr/745' 2026-05-26 10:24:56 +02:00
lucaronin
8c8a2bccfe Merge remote-tracking branch 'refs/remotes/pr/740' 2026-05-26 10:24:52 +02:00
lucaronin
42065d9843 Merge remote-tracking branch 'refs/remotes/pr/733' 2026-05-26 10:24:49 +02:00
lucaronin
6080500df0 Merge remote-tracking branch 'refs/remotes/pr/738' 2026-05-26 10:24:46 +02:00
Elias Stiffel
fe1ff59879 perf: defer and parallelize AI agent CLI discovery at startup
get_ai_agents_status used to probe all six agent CLIs (claude_code,
codex, opencode, pi, gemini, kiro) sequentially during cold start. For
each agent missing from PATH it would fall through to three login-shell
spawns (`<shell> -lc 'command -v <agent>'`), each evaluating the user's
full zsh init. On a machine with no agents installed this added ~5 s to
cold start.

The result is not on the first-paint render path. It feeds the status
bar AI badge, the AI tab in settings, and the AI agents onboarding
prompt — all of which are gated behind aiFeaturesEnabled and the
onboarding flow, so they happily render nothing while the probe is
pending.

Three additive changes:

* useAiAgentsStatus now takes an `enabled` option. When false (AI
  features off, or running in a detached note window) the probe never
  fires. App.tsx passes `aiFeaturesEnabled && !noteWindowParams`.
* When enabled, the invoke call is deferred via requestIdleCallback
  (with setTimeout(0) fallback because WKWebView lacks the API) so it
  lands after first paint instead of during it.
* Rust get_ai_agents_status is now async and fans the six check_cli()
  calls out under tokio::task::spawn_blocking + tokio::join!. Per-agent
  check_cli() signatures stay sync, so run_agent_stream callers are
  unaffected and the per-agent unit tests need no changes.

Cold-start measurements on this machine (no agents on PATH, dev mode):

  Baseline (main, AI on):       cascade settles at t+15.5 s
  Patched, AI features off:                       t+5.0 s
  Patched, AI features on:                        t+6.0 s,
                                  AI status arrives ~1.6 s after first
                                  paint, fully off the critical path.

Fixes: https://github.com/refactoringhq/tolaria/issues/726

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:35:15 +02:00
github-actions[bot]
fe939763cf Merge branch 'main' into pr-728 2026-05-25 15:18:46 +00:00
github-actions[bot]
e15aa76826 Merge branch 'main' into pr-736 2026-05-25 15:18:44 +00:00
github-actions[bot]
ebe85faf53 Merge branch 'main' into pr-735 2026-05-25 15:18:39 +00:00
github-actions[bot]
d0ce5ae165 Merge branch 'main' into main 2026-05-25 15:18:35 +00:00
lucaronin
780e33bff2 fix: recover rich editor keydown join errors 2026-05-25 17:09:11 +02:00
github-actions[bot]
443d34b8b2 Merge branch 'main' into pr-728 2026-05-25 10:22:31 +00:00
github-actions[bot]
77fa49d5f5 Merge branch 'main' into pr-736 2026-05-25 10:22:29 +00:00
github-actions[bot]
a2094b57b7 Merge branch 'main' into pr-735 2026-05-25 10:22:25 +00:00
github-actions[bot]
59d272cf24 Merge branch 'main' into main 2026-05-25 10:22:21 +00:00
lucaronin
afc2ead14c fix: restore full app note windows 2026-05-25 12:07:25 +02:00
github-actions[bot]
80fb3dc6c9 Merge branch 'main' into pr-728 2026-05-25 09:58:00 +00:00
github-actions[bot]
181906cb17 Merge branch 'main' into pr-736 2026-05-25 09:57:58 +00:00
github-actions[bot]
1b00eaf461 Merge branch 'main' into pr-735 2026-05-25 09:57:54 +00:00
github-actions[bot]
9ad5ab822a Merge branch 'main' into main 2026-05-25 09:57:49 +00:00
lucaronin
c255c91a0b fix: keep sort dropdown within viewport 2026-05-25 11:45:34 +02:00
github-actions[bot]
384aa22fbd Merge branch 'main' into pr-728 2026-05-25 09:24:11 +00:00
github-actions[bot]
093d331719 Merge branch 'main' into pr-736 2026-05-25 09:24:09 +00:00
github-actions[bot]
f9c5e70916 Merge branch 'main' into pr-735 2026-05-25 09:24:05 +00:00
github-actions[bot]
d08a08195c Merge branch 'main' into main 2026-05-25 09:23:59 +00:00
lucaronin
2a3589181e fix: dedupe multi-vault auto sync fetches 2026-05-25 11:09:49 +02:00
github-actions[bot]
fb02644358 Merge branch 'main' into pr-728 2026-05-25 08:56:32 +00:00
github-actions[bot]
7c2e7a8990 Merge branch 'main' into pr-736 2026-05-25 08:56:30 +00:00
github-actions[bot]
e33f1b3106 Merge branch 'main' into pr-735 2026-05-25 08:56:26 +00:00
github-actions[bot]
815827b6e2 Merge branch 'main' into main 2026-05-25 08:56:21 +00:00
lucaronin
deb6998c97 refactor: consolidate type definition lookup 2026-05-25 10:41:24 +02:00
github-actions[bot]
1427035623 Merge branch 'main' into pr-728 2026-05-25 08:03:29 +00:00
github-actions[bot]
b7301568b8 Merge branch 'main' into pr-736 2026-05-25 08:03:26 +00:00
github-actions[bot]
0bf921dad1 Merge branch 'main' into pr-735 2026-05-25 08:03:21 +00:00
github-actions[bot]
563030982b Merge branch 'main' into main 2026-05-25 08:03:15 +00:00
lucaronin
41121ebf70 docs: add release notes for v2026-05-25 2026-05-25 10:02:26 +02:00
mehmet turac
7516dfe48a fix: treat name-only git remotes as local-only 2026-05-24 22:14:05 +03:00
mehmet turac
8764b6f78e fix: respect Kiro default agent fallback 2026-05-24 21:00:37 +03:00
github-actions[bot]
094f59eb7b Merge branch 'main' into pr-728 2026-05-24 12:54:58 +00:00
github-actions[bot]
62e1ab0e26 Merge branch 'main' into pr-736 2026-05-24 12:54:56 +00:00
github-actions[bot]
0135ce3f10 Merge branch 'main' into pr-735 2026-05-24 12:54:52 +00:00
lucaronin
c752a52e28 docs: update native QA guidance 2026-05-24 14:54:26 +02:00
github-actions[bot]
ede82fc7a2 Merge branch 'main' into pr-728 2026-05-24 12:28:24 +00:00
github-actions[bot]
8c2432339a Merge branch 'main' into pr-736 2026-05-24 12:28:21 +00:00
github-actions[bot]
fbfd0f1cf3 Merge branch 'main' into pr-735 2026-05-24 12:28:18 +00:00
lucaronin
2fb14f4c35 fix: disambiguate sidebar type visibility by vault 2026-05-24 14:18:17 +02:00
github-actions[bot]
51e32dc832 Merge branch 'main' into pr-728 2026-05-24 12:11:53 +00:00
github-actions[bot]
a541c1b916 Merge branch 'main' into pr-736 2026-05-24 12:11:51 +00:00
github-actions[bot]
411c1947ae Merge branch 'main' into pr-735 2026-05-24 12:11:47 +00:00
lucaronin
5bebd1c458 fix: restore light app icon 2026-05-24 13:52:16 +02:00
github-actions[bot]
e2e9e81ee9 Merge branch 'main' into pr-728 2026-05-24 11:49:09 +00:00
github-actions[bot]
11c7e35519 Merge branch 'main' into pr-736 2026-05-24 11:49:07 +00:00
github-actions[bot]
b9bff9223e Merge branch 'main' into pr-735 2026-05-24 11:49:04 +00:00
lucaronin
d204e95460 fix: expose desktop menu in window chrome 2026-05-24 13:33:14 +02:00
github-actions[bot]
764c899b63 Merge branch 'main' into pr-728 2026-05-24 11:26:39 +00:00
github-actions[bot]
a3a9738034 Merge branch 'main' into pr-736 2026-05-24 11:26:36 +00:00
github-actions[bot]
ad28aed309 Merge branch 'main' into pr-735 2026-05-24 11:26:33 +00:00
lucaronin
b5d462b3cd fix: sync active git vaults together 2026-05-24 13:11:29 +02:00
github-actions[bot]
35e50b75fe Merge branch 'main' into pr-728 2026-05-24 10:54:12 +00:00
github-actions[bot]
018a0aff4f Merge branch 'main' into pr-736 2026-05-24 10:54:10 +00:00
github-actions[bot]
0ffacb3a83 Merge branch 'main' into pr-734 2026-05-24 10:54:08 +00:00
github-actions[bot]
46f82d201d Merge branch 'main' into pr-735 2026-05-24 10:54:06 +00:00
lucaronin
c77d2af537 fix: ignore canceled external URL opens 2026-05-24 12:44:51 +02:00
github-actions[bot]
e662730fbc Merge branch 'main' into pr-728 2026-05-24 10:39:32 +00:00
github-actions[bot]
dc19b2d002 Merge branch 'main' into pr-736 2026-05-24 10:39:30 +00:00
github-actions[bot]
bb68ba78b6 Merge branch 'main' into pr-734 2026-05-24 10:39:28 +00:00
github-actions[bot]
4b375932f4 Merge branch 'main' into pr-735 2026-05-24 10:39:25 +00:00
lucaronin
7d6ba934c6 feat: switch app icon with theme 2026-05-24 12:28:29 +02:00
github-actions[bot]
aa4de6f155 Merge branch 'main' into pr-728 2026-05-24 10:18:53 +00:00
github-actions[bot]
7b62cd5e13 Merge branch 'main' into pr-736 2026-05-24 10:18:51 +00:00
github-actions[bot]
86e414d192 Merge branch 'main' into pr-734 2026-05-24 10:18:49 +00:00
github-actions[bot]
69bf1ca27b Merge branch 'main' into pr-735 2026-05-24 10:18:47 +00:00
lucaronin
9ca05c1665 fix: stack narrow status bar footer 2026-05-24 11:04:18 +02:00
github-actions[bot]
6fd0638d14 Merge branch 'main' into pr-728 2026-05-24 09:01:13 +00:00
github-actions[bot]
c62fb2227e Merge branch 'main' into pr-736 2026-05-24 09:01:10 +00:00
github-actions[bot]
29774c4f46 Merge branch 'main' into pr-734 2026-05-24 09:01:07 +00:00
github-actions[bot]
437c15a280 Merge branch 'main' into pr-735 2026-05-24 09:01:04 +00:00
lucaronin
194cebdfe0 fix: clamp selection toolbar to viewport 2026-05-24 10:48:14 +02:00
github-actions[bot]
d7e1de65b6 Merge branch 'main' into pr-728 2026-05-24 08:44:23 +00:00
github-actions[bot]
38b336357f Merge branch 'main' into pr-736 2026-05-24 08:44:21 +00:00
github-actions[bot]
7f7272e09b Merge branch 'main' into pr-734 2026-05-24 08:44:19 +00:00
github-actions[bot]
8458a337bd Merge branch 'main' into pr-735 2026-05-24 08:44:16 +00:00
lucaronin
d8e0524613 test: cover immediate new note editing 2026-05-24 10:30:16 +02:00
github-actions[bot]
72e10d3b41 Merge branch 'main' into pr-728 2026-05-24 08:22:27 +00:00
github-actions[bot]
80e685cc33 Merge branch 'main' into pr-736 2026-05-24 08:22:25 +00:00
github-actions[bot]
d87d15982f Merge branch 'main' into pr-734 2026-05-24 08:22:23 +00:00
github-actions[bot]
b8b5515a7d Merge branch 'main' into pr-735 2026-05-24 08:22:21 +00:00
lucaronin
e4afa7cfa5 fix: ratchet codescene thresholds 2026-05-24 10:08:24 +02:00
Elias Stiffel
42c3b93886 refactor: group applyMountedChange callbacks under a single field
Code review on pr-735 flagged `applyMountedChange` for exceeding the
8-parameter limit. The three effect callbacks (`onSetDefaultWorkspace`,
`onSwitchVault`, `onUpdateWorkspaceIdentity`) are now grouped under a
single `callbacks` field. The function's destructure surface drops
from 8 to 6 fields, and the semantic split between "state + change"
and "effects" is now explicit in the type.

No behaviour change; the six existing tests pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:45:01 +02:00
Elias Stiffel
da3e9b9865 fix: switch active vault when unmounting it via the vault switcher
Part of the fix for #735 — top-level folder creation invisible in
multi-workspace mode. The primary route, where the create-folder flow
ignored the sidebar selection and always wrote to the legacy
`resolvedPath`, is fixed in #728 (selection-aware routing in
`handleCreateFolder` and `FolderTree`). This commit fixes the
complementary state-machine bug: the vault switcher could leave
`vaultPath` pointing at an unmounted vault, so any consumer that still
reads `vaultPath` would operate on a vault the user no longer sees.

The vault switcher tracks two related paths:
- `defaultWorkspacePath` — the workspace the user is currently "on"
  (used as the destination for new notes, the graph default, etc.).
- `vaultPath` — the active vault the switcher tracks; read by the file
  watcher, folder/view reloaders, and the active-vault fallback in the
  folder loader.

Unchecking a vault in the switcher previously re-routed only
`defaultWorkspacePath` to another mounted vault, leaving `vaultPath`
pointing at the just-unmounted vault. The two paths drifted apart and
every downstream consumer that read `vaultPath` operated on stale
state — for example the folder loader's active-vault fallback (when
enabled) would silently re-introduce the unmounted vault into the
sidebar, and the file watcher would keep listening to it.

`applyMountedChange` now also calls `onSwitchVault(nextPath)` when the
unmounted vault is the active one, mirroring the existing reroute for
`defaultWorkspacePath`. After the change `vaultPath` and mounted state
stay consistent for every consumer.

`applyMountedChange` is extracted from `VaultMenu.tsx` to a sibling
helper module (`vaultMenuMountedChange.ts`) so it can be unit-tested
directly without tripping `react-refresh/only-export-components`.

Tests:
- New `VaultMenu.applyMountedChange.test.ts` covers the four reroute
  permutations (default+active, active only, default only, neither),
  the bail-out when no alternative mounted vault exists, and the
  remount no-op.

Fixes: https://github.com/refactoringhq/tolaria/issues/735
Related: https://github.com/refactoringhq/tolaria/pull/728

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:08:57 +02:00
lucaronin
c6faf005b5 fix: prefer Windows OpenCode cmd shims 2026-05-24 00:06:53 +02:00
github-actions[bot]
5374e66a80 Merge branch 'main' into pr-728 2026-05-23 21:49:29 +00:00
github-actions[bot]
3efa44df6d Merge branch 'main' into pr-736 2026-05-23 21:49:27 +00:00
github-actions[bot]
f1c2470a6e Merge branch 'main' into pr-734 2026-05-23 21:49:25 +00:00
lucaronin
2e9f07bab4 fix: use custom chrome on Windows 2026-05-23 23:36:22 +02:00
lucaronin
39159b9b1e fix: detect installed Codex on Windows 2026-05-23 23:25:38 +02:00
github-actions[bot]
1be0c4b32f Merge branch 'main' into pr-728 2026-05-23 21:01:34 +00:00
github-actions[bot]
a9f38fb372 Merge branch 'main' into pr-736 2026-05-23 21:01:32 +00:00
github-actions[bot]
2a15ecb3e3 Merge branch 'main' into pr-734 2026-05-23 21:01:30 +00:00
lucaronin
770fdee881 fix: preserve AI session when panel closes 2026-05-23 22:39:02 +02:00
github-actions[bot]
205f3641e3 Merge branch 'main' into pr-728 2026-05-23 20:30:14 +00:00
github-actions[bot]
56e5c98a84 Merge branch 'main' into pr-736 2026-05-23 20:30:11 +00:00
github-actions[bot]
73766ca471 Merge branch 'main' into pr-734 2026-05-23 20:30:09 +00:00
lucaronin
0979c0664c fix: derive AI agent targets from registry 2026-05-23 22:09:39 +02:00
github-actions[bot]
bcbd9d3c5a Merge branch 'main' into pr-728 2026-05-23 20:08:08 +00:00
github-actions[bot]
69000f205a Merge branch 'main' into pr-736 2026-05-23 20:08:06 +00:00
github-actions[bot]
d8e280b9ee Merge branch 'main' into pr-734 2026-05-23 20:08:04 +00:00
lucaronin
ce237039c9 fix: preserve editor focus during active note refresh 2026-05-23 21:53:21 +02:00
github-actions[bot]
6a3d730f0b Merge branch 'main' into pr-728 2026-05-23 19:46:31 +00:00
github-actions[bot]
c1cd894516 Merge branch 'main' into pr-736 2026-05-23 19:46:28 +00:00
github-actions[bot]
2d04335648 Merge branch 'main' into pr-734 2026-05-23 19:46:26 +00:00
lucaronin
1c8207eeb5 refactor: split vault loader workspace tests 2026-05-23 21:26:03 +02:00
github-actions[bot]
fa9855fbf3 Merge branch 'main' into pr-728 2026-05-23 19:06:37 +00:00
github-actions[bot]
d03c780180 Merge branch 'main' into pr-736 2026-05-23 19:06:35 +00:00
github-actions[bot]
c903eb1e79 Merge branch 'main' into pr-734 2026-05-23 19:06:32 +00:00
lucaronin
35d258c0ba fix: keep active vault folder roots visible 2026-05-23 21:01:36 +02:00
github-actions[bot]
720e2a9f60 Merge branch 'main' into pr-728 2026-05-23 18:18:00 +00:00
github-actions[bot]
b8b07a0310 Merge branch 'main' into pr-736 2026-05-23 18:17:57 +00:00
github-actions[bot]
f2bbcef7f0 Merge branch 'main' into pr-734 2026-05-23 18:17:55 +00:00
lucaronin
2df076f834 fix: bound code-block clipboard copies 2026-05-23 19:59:13 +02:00
github-actions[bot]
7e119ee8f0 Merge branch 'main' into pr-728 2026-05-23 17:26:45 +00:00
github-actions[bot]
58f703e0a7 Merge branch 'main' into pr-736 2026-05-23 17:26:43 +00:00
github-actions[bot]
2172719f3e Merge branch 'main' into pr-734 2026-05-23 17:26:40 +00:00
lucaronin
49008eb79c fix: recover empty startup vault reloads 2026-05-23 19:10:10 +02:00
lucaronin
454181456d fix: insert emoji shortcode suggestions 2026-05-23 19:06:07 +02:00
lucaronin
e07dab2310 docs: record app preferences context ADR 2026-05-23 19:06:03 +02:00
github-actions[bot]
6e97f3d4d3 Merge branch 'main' into pr-728 2026-05-23 16:30:09 +00:00
github-actions[bot]
a89416e9a5 Merge branch 'main' into pr-736 2026-05-23 16:30:07 +00:00
github-actions[bot]
5b275629be Merge branch 'main' into pr-734 2026-05-23 16:30:05 +00:00
Elias Stiffel
4a45616b16 Merge remote-tracking branch 'origin/pr-728' into pr-728 2026-05-23 18:17:22 +02:00
lucaronin
ae529f64cd fix: persist property-only frontmatter edits 2026-05-23 18:12:58 +02:00
Elias Stiffel
d337e2204a fix(lint): use optional chain in FolderTree create handler 2026-05-23 18:10:34 +02:00
github-actions[bot]
7110fef6f8 Merge branch 'main' into pr-728 2026-05-23 16:00:39 +00:00
github-actions[bot]
ec286e98d2 Merge branch 'main' into pr-736 2026-05-23 16:00:36 +00:00
github-actions[bot]
66b1a2b301 Merge branch 'main' into pr-734 2026-05-23 16:00:34 +00:00
lucaronin
2a3ecf7270 fix: recover invalid editor block joins 2026-05-23 17:49:00 +02:00
Elias Stiffel
3403c56394 test: align desktop_remote_commands_report_no_remote with has_remote fix
The test's name says "report_no_remote" but its push assertion still
expected the pre-fix behavior ("error"). After 6d97335d made has_remote
URL-aware, git_push on a vault with no URL-bearing remote short-circuits
with "no_remote" — matching the test name and the pull assertion above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:47:58 +02:00
Elias Stiffel
2b368dc8f0 fix(git): list_remotes ignores name-only inherited remote sections
Sibling to the has_remote fix on this branch (6d97335d). Plain `git remote`
lists remote names defined in any config layer, so a [remote "origin"]
section inherited from ~/.gitconfig (e.g. with `prune = true`) made
list_remotes return ["origin"] for fresh local vaults. That broke
git_add_remote (returned "already_configured") and silently neutered
disconnect_all_remotes.

Reuse remote::remote_line_has_url to parse `git remote -v` output and
keep only remotes with a non-empty URL column. Dedupe by name across
the (fetch)/(push) line pair.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:46:54 +02:00
Elias Stiffel
6d97335d68 fix: treat name-only remote sections as no remote
`has_remote()` ran bare `git remote`, which lists every remote name from
any config layer — so a `[remote "origin"]` section inherited from the
user's `~/.gitconfig` (e.g. with only `prune = true`) made every vault
look remote-backed. `git_pull` then ran `git pull --no-rebase`, hit
"no tracking information", and the status bar surfaced "Sync failed".

Parse `git remote -v` and require at least one line with a non-empty URL
column. Mirror the guard on `git_push` so direct callers return
`no_remote` instead of issuing a doomed push. Handle `no_remote`
explicitly in `useAutoSync.performPull` / `pullAndPush` / `handlePushResult`
so the frontend short-circuits the push step and keeps a neutral status.

Fixes: https://github.com/refactoringhq/tolaria/issues/734

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:19:34 +02:00
github-actions[bot]
3042d6180f Merge branch 'main' into pr-728 2026-05-23 15:09:57 +00:00
lucaronin
12646d4e98 fix: recover stale editor block references 2026-05-23 16:53:43 +02:00
github-actions[bot]
972d13e2cc Merge branch 'main' into pr-728 2026-05-23 14:28:31 +00:00
github-actions[bot]
86e88a3d43 Merge branch 'main' into pr-736 2026-05-23 14:28:29 +00:00
lucaronin
f0fb7b7418 fix: recover invalid insertion depth transforms 2026-05-23 16:11:20 +02:00
github-actions[bot]
ba5464cc24 Merge branch 'main' into pr-728 2026-05-23 13:45:10 +00:00
github-actions[bot]
ec20c02784 Merge branch 'main' into pr-736 2026-05-23 13:45:08 +00:00
lucaronin
5bb3fe0550 fix: recover stale table editor transforms 2026-05-23 15:28:35 +02:00
github-actions[bot]
b4610ab9be Merge branch 'main' into pr-728 2026-05-23 12:57:07 +00:00
github-actions[bot]
7c231b08d7 Merge branch 'main' into pr-736 2026-05-23 12:57:04 +00:00
lucaronin
7709ad1002 fix: recover missing note frontmatter writes 2026-05-23 14:41:11 +02:00
lucaronin
948b1501bf fix: route sync to selected git repository 2026-05-23 14:07:56 +02:00
Elias Stiffel
5dd3b98fec fix: avoid duplicate H1 when template begins with one
`buildNoteBody` unconditionally prepended an empty `# ` placeholder
whenever `initialEmptyHeading` was true, which collided with templates
whose first line is already an H1 (e.g. a Weekly Notes type whose
template starts with `# Woche 2026.21`). The placeholder is now
suppressed when the template's first non-whitespace line is `# `.

Fixes: https://github.com/refactoringhq/tolaria/issues/736

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:35:37 +02:00
Elias Stiffel
f0e32cfd5f fix: nest new folders under the selected parent
The Create Folder action ignored the sidebar selection and always
wrote to the vault root. Now it threads the selected folder as
parent_path to the backend, and expands the parent in the tree so
the new child is visible without re-clicking.

Fixes: https://github.com/refactoringhq/tolaria/issues/728

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 08:03:47 +02:00
lucaronin
806df2c586 feat: create notes from quick open 2026-05-21 19:03:13 +02:00
Luca Rossi
3bed1f9235 Merge pull request #717 from mvanhorn/fix/710-numeric-property-rendered-as-ipv4
fix: do not treat pure-numeric property values as bare-domain URLs (#710)
2026-05-21 16:20:10 +02:00
Luca Rossi
07248fb868 Merge pull request #711 from mvanhorn/fix/705-mcp-node-discovery-linuxbrew-fallback
fix: include linuxbrew node path in MCP runtime fallbacks (#705)
2026-05-21 16:19:46 +02:00
github-actions[bot]
8f948d4c41 Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-21 14:19:12 +00:00
github-actions[bot]
cfb8e0aa4f Merge branch 'main' into fix/710-numeric-property-rendered-as-ipv4 2026-05-21 14:19:08 +00:00
lucaronin
fa9de47729 Merge remote-tracking branch 'refs/remotes/origin/pr-711' 2026-05-21 16:04:56 +02:00
lucaronin
fe84ed91ac Merge remote-tracking branch 'refs/remotes/origin/pr-716' 2026-05-21 16:00:58 +02:00
lucaronin
b5e0c9d890 Merge remote-tracking branch 'refs/remotes/origin/pr-717' 2026-05-21 15:56:29 +02:00
github-actions[bot]
5d9deb3028 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 13:30:12 +00:00
lucaronin
600ddccff4 docs: align stable release notes filename 2026-05-21 15:29:37 +02:00
github-actions[bot]
e3e7981d8a Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 13:25:11 +00:00
lucaronin
bf1feb204b docs: add stable release notes 2026-05-21 15:24:35 +02:00
github-actions[bot]
2f4d52e53a Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 12:08:49 +00:00
lucaronin
2c9b897430 docs: add Portent templates page 2026-05-21 13:57:23 +02:00
github-actions[bot]
e1486959bb Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 10:13:52 +00:00
lucaronin
74dac301d3 fix: speed up note window startup 2026-05-21 12:01:14 +02:00
github-actions[bot]
2011acf1e2 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 08:33:24 +00:00
github-actions[bot]
50c46f9924 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 08:22:15 +00:00
lucaronin
ecfc76af59 fix: restore note window opening 2026-05-21 10:19:49 +02:00
lucaronin
b2a94b482e chore: tighten eslint pipeline 2026-05-21 10:08:15 +02:00
github-actions[bot]
425393b4a2 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-21 08:03:06 +00:00
lucaronin
ac196bec58 feat: expand note list context menu
Co-authored-by: Brian Lee <engbrianlee@gmail.com>
2026-05-21 09:49:38 +02:00
github-actions[bot]
4e3dfaad55 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 21:32:53 +00:00
lucaronin
d5b2773436 fix: satisfy release project build 2026-05-20 23:19:52 +02:00
github-actions[bot]
695c8423f5 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 18:02:43 +00:00
lucaronin
c7c53cafa2 feat: add note item context menu 2026-05-20 19:50:42 +02:00
github-actions[bot]
6c29a01a69 Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-20 17:42:41 +00:00
github-actions[bot]
7a72df1c2f Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 17:42:38 +00:00
lucaronin
5f87debecb fix: support go code block highlighting 2026-05-20 19:30:32 +02:00
github-actions[bot]
57c61a1ddd Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-20 17:27:45 +00:00
github-actions[bot]
08abcd5eff Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 17:27:42 +00:00
github-actions[bot]
bb47cf875d Merge branch 'main' into fix/710-numeric-property-rendered-as-ipv4 2026-05-20 17:27:39 +00:00
lucaronin
0a36915903 fix: detect shell-managed pi installs 2026-05-20 19:14:14 +02:00
github-actions[bot]
df1e2913c0 Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-20 17:09:24 +00:00
github-actions[bot]
ecf4df2d11 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 17:09:21 +00:00
github-actions[bot]
54595ab174 Merge branch 'main' into fix/710-numeric-property-rendered-as-ipv4 2026-05-20 17:09:18 +00:00
lucaronin
73742495b0 fix: unzoom fullscreen mermaid diagrams 2026-05-20 18:57:08 +02:00
github-actions[bot]
b93e25ddb1 Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-20 16:51:53 +00:00
github-actions[bot]
5c5da8a670 Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 16:51:50 +00:00
github-actions[bot]
3a7ce6bb5f Merge branch 'main' into fix/710-numeric-property-rendered-as-ipv4 2026-05-20 16:51:46 +00:00
lucaronin
ae6bd8ec73 fix: resolve non-markdown wikilink paths 2026-05-20 18:39:44 +02:00
github-actions[bot]
c7f957775e Merge branch 'main' into fix/705-mcp-node-discovery-linuxbrew-fallback 2026-05-20 16:36:03 +00:00
github-actions[bot]
9ea4c367bd Merge branch 'main' into fix/695-claude-codex-windows-spawn-shim 2026-05-20 16:36:00 +00:00
github-actions[bot]
55c0153692 Merge branch 'main' into fix/710-numeric-property-rendered-as-ipv4 2026-05-20 16:35:57 +00:00
lucaronin
67007e75ff fix: refresh active note after external edits 2026-05-20 18:21:16 +02:00
Matt Van Horn
57c6a4a2a4 fix: do not treat pure-numeric property values as bare-domain URLs
Fixes #710

A number property like "Year: 2026" rendered as "0.0.7.234" in the
note list column because the WHATWG URL parser canonicalizes integer-only
hosts to IPv4 form: new URL('https://2026').hostname === '0.0.7.234'.
formatChipLabel called isUrlValue('2026'), which returned true via the
bare-domain branch of normalizeExternalUrl, and then asked the URL parser
for the hostname.

Add a single guard in normalizeExternalUrl: when the input contains no '.',
reject it before the bare-domain candidate is constructed. The
parseHttpUrl(trimmed) branch above already handles inputs with an explicit
scheme (https://...), so the guard only narrows the bare-domain path -
the path that produced the bug. Existing cases ('example.com', 'localhost')
remain unaffected.

Two new vitest cases cover the regression ('2026' and '0') plus a parity
case for https://example.com to make sure the scheme branch is untouched.
2026-05-19 23:30:59 -07:00
Matt Van Horn
663a7d9a3f fix: prefer .cmd shim over POSIX wrapper for claude/codex on Windows
Fixes #695

`where claude` on Windows returns both the npm-generated POSIX shell
wrapper (extensionless) and the Windows batch shim (`claude.cmd`)
side-by-side. The existing `path_from_successful_output` helpers in
`claude_cli.rs` and `codex_cli.rs` picked the first existing line,
which gave the POSIX wrapper - Windows tried to load it as a PE32,
`CreateProcessW` returned ERROR_BAD_EXE_FORMAT (193), and the AI chat
panel surfaced "%1 is not a valid Win32 application."

Mirror the fix that landed for Gemini in `gemini_discovery.rs`: when
`cfg!(windows)`, filter candidates through the existing shared utility
`cli_agent_runtime::has_windows_cli_extension` so .bat/.cmd/.com/.exe
win over extensionless POSIX scripts. Non-Windows behavior is unchanged.

Added a regression test in each file matching the
`windows_path_lookup_prefers_cmd_shim_over_extensionless_npm_script`
pattern from gemini_discovery.rs.
2026-05-19 23:28:16 -07:00
Matt Van Horn
0abf7b492a fix: include linuxbrew node path in MCP runtime fallbacks
Fixes #705

GUI launches of the AppImage on Linux do not inherit the shell PATH, so
`find_on_path` and `find_in_user_shell` cannot reach Homebrew-on-Linux's
Node install. Add the canonical Linuxbrew paths to `fallback_node_paths`
and to `node_binary_candidates_for_home`; `.is_file()` filtering keeps
machines without Linuxbrew unaffected.
2026-05-19 23:23:58 -07:00
lucaronin
6cf77f9b94 fix: restore autogit multi-vault pushes 2026-05-19 15:46:29 +02:00
lucaronin
2c1fe9f308 fix: reduce retained editor memory 2026-05-19 15:34:46 +02:00
lucaronin
efd3cd4f27 Merge pull request #572 from oksusucha/feat/add-kiro-cli-agent
feat: add Kiro CLI as AI agent

Co-authored-by: oksusucha <sol4877@gmail.com>
2026-05-18 13:55:43 +02:00
lucaronin
3429474fc5 fix: finish pr 412 693 integration 2026-05-18 13:22:09 +02:00
lucaronin
c6ce5f421b fix: save new views in default workspace 2026-05-18 13:17:11 +02:00
lucaronin
7c307e8baf Merge pull request #693 from kzadorozhny/feat/support-bun-runtime
feat: support Bun as MCP server runtime
2026-05-18 13:09:06 +02:00
lucaronin
9d447741f3 Merge pull request #412 from kossoy/main
fix: vault watcher reload loop on iCloud .git symlink vaults
2026-05-18 13:09:00 +02:00
github-actions[bot]
e9079df63c Merge branch 'main' into feat/support-bun-runtime 2026-05-18 10:31:34 +00:00
lucaronin
48fb6710e1 Merge pull request #583 from alroniks/main
feat: add Belarusian basic and latin locales
2026-05-18 12:18:41 +02:00
github-actions[bot]
ae72a43a43 Merge branch 'main' into feat/support-bun-runtime 2026-05-18 09:17:41 +00:00
lucaronin
49935db529 docs: add release notes for v2026-05-18 2026-05-18 11:17:08 +02:00
github-actions[bot]
cd4d3dece2 Merge branch 'main' into feat/support-bun-runtime 2026-05-18 08:43:27 +00:00
lucaronin
0ca0f4a6cf fix: point update banner to release notes 2026-05-18 10:33:50 +02:00
Konstantin Zadorozhny
c2a0988368 docs: note Bun runtime alongside Node for MCP server
Mention `find_mcp_runtime` (Node.js 18+ preferred, Bun 1+ fallback) wherever
the docs previously said Tolaria "verifies Node.js" before writing MCP config,
update the mcp-server directory description, and broaden the AppImage subprocess
sanitisation note to cover both runtimes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:46:59 -07:00
Konstantin Zadorozhny
ab123a1428 feat: support Bun as MCP server runtime
Tolaria's MCP server is pure ESM with only standard `node:fs|path|http|child_process`
imports and pure-JS dependencies, so Bun can execute it identically to Node.
Until now `find_node()` was the single entry point for spawning the WebSocket
bridge and writing external AI tool config — users with Bun but no Node would
hit "node not found in PATH or common install locations" and lose access to MCP
tools entirely.

Introduce `find_mcp_runtime()` which returns the first verifying runtime,
preferring Node 18+ when present and falling back to Bun 1+. The generic
PATH and login-shell lookup helpers (`find_on_path`, `find_in_user_shell`,
`lookup_command`, `lookup_paths`) are now parameterised by command name so
both runtimes share the same machinery. Bun candidates cover `~/.bun/bin/bun`,
mise/asdf/proto shims, Homebrew, and `%USERPROFILE%\.bun\bin\bun.exe` on
Windows. The Codex CLI and Windows .cmd-shim resolution paths keep using the
strict `find_node()` since they specifically need Node.

`spawn_ws_bridge_with_paths`, `mcp_config_snippet`, and `register_mcp` now
resolve through `find_mcp_runtime` so the runtime that gets registered into
`~/.claude.json`, `~/.gemini/settings.json`, `~/.cursor/mcp.json`, etc.
matches the one actually present on the machine.

Locale copy updated from "nodeRequirement" to "runtimeRequirement" across
all 15 catalogs to reflect "Node.js 18+ or Bun 1+".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:42:31 -07:00
lucaronin
c1a5042fb9 test: cover image toolbar hover access 2026-05-17 20:06:16 +02:00
lucaronin
9d331ffda8 fix: respect rtl direction in editor blocks 2026-05-17 19:46:18 +02:00
lucaronin
41992506f1 fix: align raw editor line numbers 2026-05-17 19:34:18 +02:00
lucaronin
761f232647 fix: address clawpatch findings 2026-05-17 19:31:12 +02:00
lucaronin
c09b25d3c5 fix: restore release build type safety 2026-05-17 16:34:09 +02:00
lucaronin
4061d5e88e fix: remove built-in note body templates 2026-05-17 16:31:20 +02:00
lucaronin
0983e4e288 test: cover attachment preservation during tab switch 2026-05-17 04:09:47 +02:00
lucaronin
247c3eefb9 fix: repair rich editor invalid list content 2026-05-17 03:06:49 +02:00
lucaronin
28e2072f9b fix: stabilize mermaid diagram reload clicks 2026-05-17 00:24:17 +02:00
lucaronin
7adecaea60 fix: preserve embedded attachment paths 2026-05-16 19:25:48 +02:00
lucaronin
2616f87185 fix: prevent editor file drop navigation 2026-05-16 18:54:19 +02:00
lucaronin
d81c696742 fix: resolve relative dates in view filters 2026-05-16 18:34:41 +02:00
lucaronin
678cc8f7ba fix: preserve aliased wikilinks in markdown tables 2026-05-16 17:58:44 +02:00
lucaronin
35176d6eff feat: expose rpm download option 2026-05-16 13:44:11 +02:00
lucaronin
42130e116d feat: add note-list search clear action 2026-05-16 13:13:16 +02:00
lucaronin
1040d95eec feat: add granular git controls 2026-05-16 12:28:56 +02:00
lucaronin
480876558a fix: preserve new note edits during empty heading swap 2026-05-16 11:34:59 +02:00
lucaronin
ce04a49bae fix: keep default workspace views mounted 2026-05-15 11:33:55 +02:00
lucaronin
3695b6f3fe fix: mark MCP tools approval-safe 2026-05-15 11:19:07 +02:00
lucaronin
7e50da7816 fix: match scalar array properties in views 2026-05-15 08:33:17 +02:00
lucaronin
17a938f8cf docs: add/update ADR for AppImage media preview fallback (guard — from commit 9b6a43c) 2026-05-15 08:03:35 +02:00
lucaronin
3d2efcedeb fix: bound parsed note-list preload warmups 2026-05-15 03:16:38 +02:00
lucaronin
c7b7b97022 fix: suppress app-owned frontmatter reloads 2026-05-15 02:42:51 +02:00
lucaronin
0c8be62831 refactor: consolidate vault ordering 2026-05-15 02:29:39 +02:00
lucaronin
9b28f60be0 chore: ratchet codescene thresholds 2026-05-15 01:36:10 +02:00
lucaronin
9b6a43cec7 fix: guard linux appimage media previews 2026-05-15 01:22:56 +02:00
lucaronin
384c752c33 docs: add release notes for v2026-05-14 2026-05-14 18:12:17 +02:00
lucaronin
e844cfa0bc feat: support vault reordering 2026-05-14 17:30:13 +02:00
lucaronin
7174061b2a fix: warm parsed blocks for large notes 2026-05-14 17:20:49 +02:00
lucaronin
cf03c01b75 fix: use stock appimage packaging in release 2026-05-14 17:04:36 +02:00
lucaronin
8b83b2c691 fix: preserve appimage plugin name 2026-05-14 16:44:46 +02:00
lucaronin
d445c72edb fix: extract appimage tools in linux release 2026-05-14 16:33:56 +02:00
lucaronin
9a8aacad40 fix: install fuse for linux appimage builds 2026-05-14 16:27:21 +02:00
lucaronin
416131aa8f fix: restore TypeScript build 2026-05-14 16:09:54 +02:00
lucaronin
8b1d2bbed8 fix: support relationship equality filters 2026-05-14 16:01:34 +02:00
github-actions[bot]
9a7a31c47b Merge branch 'main' into fix/view_filters 2026-05-14 13:27:51 +00:00
lucaronin
62e62790c1 feat: stabilize external mcp appimage registration
Co-authored-by: Domenico Lupinetti <domenico@translated.net>
2026-05-14 15:22:47 +02:00
lucaronin
3b2f675264 fix: create notes in selected vault 2026-05-14 15:12:56 +02:00
github-actions[bot]
9706ee8ec2 Merge branch 'main' into fix/view_filters 2026-05-14 11:57:37 +00:00
lucaronin
c9a4b273d6 refactor: simplify file attachment serialization 2026-05-14 13:43:14 +02:00
github-actions[bot]
8fe02f8c26 Merge branch 'main' into fix/view_filters 2026-05-14 11:38:23 +00:00
lucaronin
18f91af77f fix: preserve file attachment raw toggles 2026-05-14 13:29:33 +02:00
github-actions[bot]
1c047079f0 Merge branch 'main' into fix/view_filters 2026-05-14 11:16:48 +00:00
lucaronin
a8273ad7a9 feat: make external mcp vault resolution dynamic
Port the useful dynamic-vault MCP direction from Domenico Lupinetti's proposal into the current mounted-workspace model.

Durable external MCP registration is now vault-neutral, Node MCP resolves active mounted vaults from explicit env or Tolaria vaults.json, and vault context includes root AGENTS.md instructions per active vault.

Based-on: https://github.com/refactoringhq/tolaria/pull/603

Co-authored-by: Domenico Lupinetti <domenico@translated.net>
2026-05-14 13:06:43 +02:00
github-actions[bot]
133b2601a2 Merge branch 'main' into fix/view_filters 2026-05-14 11:06:39 +00:00
lucaronin
21d4577079 fix: apply webkit safeguards on linux wayland 2026-05-14 12:55:09 +02:00
github-actions[bot]
c19003cf39 Merge branch 'main' into fix/view_filters 2026-05-14 10:42:50 +00:00
lucaronin
d99da92b43 fix: preserve windows fullscreen navigation 2026-05-14 12:32:42 +02:00
github-actions[bot]
8b9c4592f1 Merge branch 'main' into fix/view_filters 2026-05-14 10:23:26 +00:00
lucaronin
832feb7df1 fix: standardize icons on phosphor 2026-05-14 12:14:16 +02:00
github-actions[bot]
506e8d0eac Merge branch 'main' into fix/view_filters 2026-05-14 10:11:02 +00:00
lucaronin
7cd346bde0 fix: prevent duplicate vault folder pickers 2026-05-14 11:59:20 +02:00
Andrius Miasnikovas
47f5da1798 Fix equals and not equals filters not working in views 2026-05-14 11:08:48 +03:00
lucaronin
ff4939fb29 fix: recover procedure editor schema errors 2026-05-14 09:55:20 +02:00
lucaronin
3fa1d34c32 fix: stabilize table handle controls after reload 2026-05-14 09:22:44 +02:00
lucaronin
2ef4d95bb5 fix: retarget externally moved inbox notes 2026-05-14 08:15:49 +02:00
lucaronin
ce715ed5aa docs: add/update ADR for entry-scoped note windows (guard — from commit 6bb9c7f) 2026-05-14 08:05:28 +02:00
lucaronin
95dab219b8 fix: preserve cjk code block copy text 2026-05-14 07:48:04 +02:00
lucaronin
2fcea0624d test: stabilize type-derived smoke setup 2026-05-14 06:57:25 +02:00
lucaronin
7f8aad0686 fix: clean up invalid mermaid renders 2026-05-14 06:43:50 +02:00
lucaronin
2eae7a0899 fix: preserve focused editor after pulls 2026-05-14 05:53:37 +02:00
lucaronin
6bb9c7fbc9 fix: avoid note-window vault scans 2026-05-14 05:05:33 +02:00
lucaronin
9d9681345d ci: keep linux check off push lane 2026-05-14 04:05:05 +02:00
lucaronin
201aefca05 fix: bundle fcitx gtk module in appimage 2026-05-13 16:44:38 +02:00
lucaronin
41f0e00cfb fix: support symlinked appimage launches 2026-05-13 16:06:57 +02:00
lucaronin
9be428aad3 fix: sanitize appimage mcp node launches 2026-05-13 15:21:36 +02:00
lucaronin
b2d23a8559 fix: dedupe calendar stable releases 2026-05-13 14:47:26 +02:00
lucaronin
d6ad2ff836 test: isolate pi agent config tests 2026-05-13 14:32:04 +02:00
lucaronin
6ac8f822ff test: stabilize autogit smoke setup 2026-05-13 14:18:03 +02:00
lucaronin
b0c8eff6c7 fix: handle autogit author failures 2026-05-13 14:12:23 +02:00
lucaronin
ae7a468310 test: guard reload editing loop 2026-05-13 13:24:41 +02:00
lucaronin
84a993b948 test: guard fresh-note enter selection 2026-05-13 13:03:36 +02:00
lucaronin
2a5ebcf73d fix: prevent whiteboard dialog crash 2026-05-13 12:42:37 +02:00
lucaronin
09978cf8e5 fix: preserve ampersand in native commit menu 2026-05-13 11:52:06 +02:00
lucaronin
0e981228e2 fix: point release history link to public site 2026-05-13 11:41:36 +02:00
lucaronin
b77ebd2a02 docs: add release notes for v2026-05-12 2026-05-13 11:33:02 +02:00
lucaronin
2e414b0867 fix: restore production build 2026-05-13 10:39:34 +02:00
lucaronin
00fec78b16 fix: stabilize editing and mounted vault updates 2026-05-13 10:29:22 +02:00
lucaronin
e774fcd824 fix: load vault index in note windows 2026-05-13 09:34:11 +02:00
lucaronin
043cdec141 fix: allow default vault notes to move workspaces 2026-05-13 09:18:54 +02:00
lucaronin
11b93638d6 docs: add/update ADR for rich/raw transition ownership (guard — from commit 5020771) 2026-05-13 08:03:41 +02:00
lucaronin
713c2a9dbb fix: set git author before app commits 2026-05-13 07:59:34 +02:00
lucaronin
553ed4de07 fix: recover stale rich-editor transforms 2026-05-13 07:33:37 +02:00
lucaronin
83cf82294c test: preserve raw exit edits during tab state sync 2026-05-13 04:06:34 +02:00
lucaronin
502077170d refactor: consolidate editor mode handoff 2026-05-13 02:18:19 +02:00
lucaronin
16a836b59c fix: register native mac window menu 2026-05-12 23:06:03 +02:00
lucaronin
68374109f4 feat: add ai visibility setting 2026-05-12 22:30:24 +02:00
lucaronin
8a3b6b57f8 fix: restore ai panel view menu 2026-05-12 21:07:40 +02:00
lucaronin
376069d124 fix: preserve pi agent config 2026-05-12 20:18:00 +02:00
lucaronin
1d563a8978 fix: scope saved views to source vault 2026-05-12 19:33:53 +02:00
lucaronin
5215754ffa feat: improve editor code blocks 2026-05-12 18:21:26 +02:00
lucaronin
c0b0a02ce6 fix: show commit dialog opening feedback 2026-05-12 16:28:58 +02:00
lucaronin
dfb363e768 fix: keep mounted workspace notes during vault switch 2026-05-12 16:11:31 +02:00
lucaronin
e82fc964d6 fix: avoid editor stalls during git status refresh 2026-05-12 15:01:41 +02:00
lucaronin
3f029b914c fix: hide startup boot diagnostics 2026-05-12 12:15:09 +02:00
lucaronin
96e8857a77 fix: recover startup vault loading 2026-05-12 11:37:14 +02:00
lucaronin
442b5f0256 fix: normalize renamed save paths 2026-05-12 11:13:41 +02:00
lucaronin
90cfb06e74 fix: narrow manual commit flow config 2026-05-12 10:30:42 +02:00
lucaronin
992c042328 fix: dispatch stable release docs deploy 2026-05-12 10:26:48 +02:00
lucaronin
91ca97bcd4 docs: document multi-vault usage 2026-05-12 10:13:16 +02:00
lucaronin
d3624edf9f refactor: consolidate repository git state 2026-05-12 08:59:30 +02:00
lucaronin
aa942362e9 fix: stabilize editor mode round trips 2026-05-12 08:15:03 +02:00
lucaronin
f2ec12aff6 fix: insert emoji shortcode suggestions 2026-05-12 07:51:10 +02:00
lucaronin
c392b84f3d fix: refresh mounted workspace startup marker 2026-05-12 07:13:01 +02:00
lucaronin
79d3a924f0 refactor: consolidate date display preference flow 2026-05-12 06:57:42 +02:00
lucaronin
b33d1060ef refactor: split vault view helpers 2026-05-12 05:24:51 +02:00
lucaronin
6c90865542 refactor: extract vault workspace entry helpers 2026-05-12 04:57:44 +02:00
lucaronin
956619237e test: preserve editor draft when tab closes 2026-05-12 04:30:41 +02:00
lucaronin
66dfc26bc9 refactor: extract git file workflows 2026-05-12 04:04:31 +02:00
lucaronin
7709255fb3 refactor: extract app orchestration hooks 2026-05-12 03:38:54 +02:00
lucaronin
a3fdaee7ad refactor: extract inbox organize advance 2026-05-12 02:27:53 +02:00
lucaronin
e84bd1a8cd fix: keep note opening responsive 2026-05-11 19:58:01 +02:00
lucaronin
8ff4cfde4f fix: type selected UI language preference 2026-05-11 18:41:25 +02:00
lucaronin
b2a6111a65 refactor: extract app vault setup state 2026-05-11 18:21:12 +02:00
lucaronin
487884bcaf fix: support nested mock Tauri args 2026-05-11 18:21:12 +02:00
lucaronin
07edfac400 feat: support mounted vault workspaces 2026-05-11 18:21:12 +02:00
lucaronin
5c05347690 fix: recover editor block ids before remount 2026-05-11 17:47:15 +02:00
lucaronin
f9fe3d833d fix: restore previous list after neighborhood toggle 2026-05-11 15:47:51 +02:00
lucaronin
7a4d027f12 feat: add collapsed sidebar reopen button 2026-05-11 14:36:20 +02:00
lucaronin
5a14848cf8 fix: tolerate malformed MCP frontmatter 2026-05-11 14:07:23 +02:00
lucaronin
0715f51b02 feat: add date display setting 2026-05-11 13:46:18 +02:00
lucaronin
e727afdd81 fix: pipe oversized claude prompts on windows 2026-05-11 12:30:44 +02:00
lucaronin
6c21aa1087 fix: avoid claude cmd shim on windows 2026-05-11 11:23:39 +02:00
lucaronin
21c68a1138 test: include save-before-switch smoke in core lane 2026-05-11 04:10:16 +02:00
lucaronin
8bee3b51cb fix: launch opencode cmd shims on windows 2026-05-10 11:18:56 +02:00
lucaronin
b4b49eaad2 fix: toggle active neighborhood back to notes 2026-05-10 10:56:57 +02:00
lucaronin
f8515fb191 fix: resolve note-relative markdown images 2026-05-10 10:30:31 +02:00
lucaronin
a2a6feea52 fix: support current Claude and Gemini CLI behavior 2026-05-10 09:58:54 +02:00
lucaronin
b9d417ec1d test: protect Cmd+N note creation persistence 2026-05-10 04:07:41 +02:00
lucaronin
6076332b8b fix: preserve list markers on fresh paste 2026-05-09 18:39:41 +02:00
lucaronin
fb08dd0c71 ci: avoid duplicate frontend typecheck in build gate 2026-05-09 04:05:50 +02:00
lucaronin
064a8879e9 fix: resolve windows mcp server install path 2026-05-08 23:13:41 +02:00
lucaronin
eb731bbaad fix: guard inspector property mutations 2026-05-08 17:37:04 +02:00
lucaronin
852c153d24 fix: avoid math placeholders for currency prose 2026-05-08 17:07:36 +02:00
lucaronin
2b345e9f76 fix: guard inline link clicks during reload 2026-05-08 16:46:22 +02:00
lucaronin
e8e30611fb test: cover rich editor media reload crash 2026-05-08 16:13:52 +02:00
lucaronin
6b71faa6e2 fix: publish docs at custom domain root 2026-05-08 15:51:10 +02:00
lucaronin
dd32f14d3a fix: clear stale editor selection on reload 2026-05-08 15:06:50 +02:00
lucaronin
5a75548652 fix: guard ai markdown highlighting on legacy webkit 2026-05-08 14:59:44 +02:00
lucaronin
81ba8f80d5 fix: tolerate stale file block clicks 2026-05-08 14:19:52 +02:00
lucaronin
ee0d4fc470 fix: allow delete menu clicks in drag regions 2026-05-08 13:44:19 +02:00
lucaronin
f7e9b66988 fix: keep fullscreen mermaid dark themed 2026-05-08 13:24:16 +02:00
lucaronin
91e02f0719 fix: sync whiteboard theme with app mode 2026-05-08 13:04:24 +02:00
lucaronin
4fd0a265f4 fix: tolerate missing note titles in list search 2026-05-08 12:33:04 +02:00
lucaronin
5a2642ba81 fix: allow multimedia embed url input 2026-05-08 12:06:28 +02:00
lucaronin
4870d2a05f fix: keep new note title rename current 2026-05-08 12:06:28 +02:00
lucaronin
6b7de99f78 fix: deploy release pages via github pages 2026-05-08 11:54:48 +02:00
lucaronin
901560467f fix: harden alpha updater metadata lookup 2026-05-08 11:34:27 +02:00
lucaronin
707c164b1c test: harden telemetry onboarding wait 2026-05-08 11:05:46 +02:00
lucaronin
5f00bae2cf fix: preserve slash menu mouse selection 2026-05-08 11:01:44 +02:00
lucaronin
cef3e6a805 fix: allow tldraw context menus in tauri 2026-05-08 10:06:25 +02:00
lucaronin
5e978e315e fix: apply rustfmt to workspace boundary 2026-05-08 01:12:39 +02:00
lucaronin
5e7d29f620 fix: satisfy production workspace build 2026-05-08 01:12:39 +02:00
lucaronin
ef3aed64e3 feat: unify mounted workspace graph 2026-05-08 01:12:39 +02:00
lucaronin
7bda58bbf6 fix: clear codacy focus and history highs 2026-05-07 22:59:58 +02:00
lucaronin
b8d1681aae fix: constrain mock vault api fetches 2026-05-07 22:40:33 +02:00
lucaronin
a29ce24280 fix: clear codacy regex and xss highs 2026-05-07 22:22:29 +02:00
lucaronin
27f1092251 feat: add math slash command 2026-05-07 21:30:19 +02:00
lucaronin
f89c5e8f7d fix: harden mock vault api requests 2026-05-07 21:06:06 +02:00
lucaronin
5965d5f23f fix: clear codacy object injection highs 2026-05-07 20:54:32 +02:00
lucaronin
8d35fbcba3 fix: clear codacy high severity batches 2026-05-07 19:52:14 +02:00
lucaronin
f5866a2376 fix: resolve codacy high severity findings 2026-05-07 18:43:59 +02:00
github-actions[bot]
c571b06293 Merge branch 'main' into main 2026-05-07 16:00:03 +00:00
Luca Rossi
b9ce903991 Updated readme 2026-05-07 17:59:31 +02:00
Ivan Klimchuk
14bf0dc32f fix: resolve pnpm patchedDependencies routing for tests 2026-05-07 18:44:28 +03:00
Ivan Klimchuk
238b4df55f feat: add Belarusian basic and latin locales 2026-05-07 18:44:27 +03:00
lucaronin
3e01c87f64 test: cover note history edit stability 2026-05-07 17:39:35 +02:00
Luca Rossi
f816c9a175 Added Codacy badge 2026-05-07 16:40:49 +02:00
lucaronin
1714b13583 fix: avoid mermaid html label clipping 2026-05-07 16:29:11 +02:00
lucaronin
fb55c57536 fix: show reference titles in breadcrumb 2026-05-07 15:48:29 +02:00
lucaronin
99e509f553 fix: keep whiteboard dialogs usable 2026-05-07 15:27:41 +02:00
lucaronin
09b8a28e39 fix: restore generated release history page 2026-05-07 15:00:28 +02:00
Luca Rossi
e0766dc678 Updated release notes 2026-05-07 14:53:47 +02:00
lucaronin
eed307f3d5 docs: add release notes for v2026-05-07 2026-05-07 14:41:37 +02:00
lucaronin
6ab5c2aa46 fix: keep saved view property sorts visible 2026-05-07 14:04:44 +02:00
lucaronin
9113f44789 test: stabilize type-derived quick open smoke 2026-05-07 13:47:18 +02:00
lucaronin
45643c59a8 fix: improve dark wikilink suggestion highlight 2026-05-07 13:38:29 +02:00
lucaronin
6d6b19f894 fix: use canonical download URL 2026-05-07 13:02:51 +02:00
lucaronin
ea4830ac30 fix: hard navigate to download page 2026-05-07 12:57:25 +02:00
lucaronin
cd3cff733b ci: preserve download assets on docs deploy 2026-05-07 12:38:26 +02:00
lucaronin
a441a6bd6a docs: harden release readiness instructions 2026-05-07 11:46:56 +02:00
lucaronin
e24ea96436 docs: adjust landing hero spacing 2026-05-07 11:24:59 +02:00
lucaronin
496a0373ff fix: resolve linux package mcp server path 2026-05-07 10:58:09 +02:00
lucaronin
8f8b9ada47 fix: launch gemini windows shims 2026-05-07 10:42:56 +02:00
lucaronin
efd3ff3132 ci: keep site updates out of app releases 2026-05-07 10:35:00 +02:00
oksusucha
881f860504 refactor: extract shared binary discovery into cli_agent_runtime
Move common shell lookup logic (PATH check, login shell probe,
first_existing_path) into cli_agent_runtime as find_cli_binary()
and check_cli_availability(). Rewrite kiro_discovery to use these
shared utilities, reducing duplication from 100 lines to ~20.

Other discovery modules (pi, gemini, opencode) can adopt the same
pattern in a follow-up.
2026-05-07 17:15:27 +09:00
oksusucha
596d2b93bb refactor: reduce kiro_cli complexity by extracting helper functions
Split run_agent_stream into focused helpers:
- spawn_kiro_process: command setup and spawn
- write_prompt_async: threaded stdin write
- generate_session_id: unique ID generation
- stream_stdout: line-by-line output processing
- collect_stderr: stderr collection

Reduces per-function cyclomatic complexity from ~18 to ~6.
2026-05-07 17:13:16 +09:00
oksusucha
16ce8b105c feat: add Kiro CLI as AI agent (rebuilt on upstream architecture)
- Add kiro_discovery.rs: binary detection (PATH, login shell, known paths)
- Add kiro_cli.rs: streaming via stdin, ANSI stripping, MCP config merge
- Register Kiro in ai_agents.rs enum, status, and dispatcher
- Add 'kiro' to settings normalization
- Add Kiro to frontend agent definitions and helpers
- Update test for agent cycling

Kiro uses plain text streaming (not JSON) so it has its own stream
handler rather than using cli_agent_runtime::run_ai_agent_json_stream.
MCP config merges into existing .kiro/settings/mcp.json to preserve
user-configured servers. Prompt is passed via stdin to avoid E2BIG.
2026-05-07 17:08:44 +09:00
lucaronin
abc6f7d9b1 docs: deploy public site with GitHub Pages 2026-05-07 09:29:47 +02:00
lucaronin
a59582c067 fix: normalize agent docs paths for windows 2026-05-07 09:08:11 +02:00
lucaronin
afc9face71 docs: add/update ADR for shared attachment path normalization (guard — from commit c8cc9b1) 2026-05-07 08:07:20 +02:00
lucaronin
7bbff355f3 fix: normalize note open entries 2026-05-07 06:41:50 +02:00
lucaronin
8707c9e4be fix: recover stale BlockNote render ids 2026-05-07 06:22:39 +02:00
lucaronin
a92651e048 fix(whiteboard): guard missing tldraw text measurements 2026-05-07 04:13:46 +02:00
lucaronin
c8cc9b1bf1 refactor: consolidate vault attachment paths 2026-05-07 03:05:10 +02:00
lucaronin
8b39eb92d6 fix(ai): handle command backspace in composer 2026-05-07 02:05:50 +02:00
lucaronin
5a02b9240a fix(editor): open PDF attachments from notes 2026-05-07 00:55:07 +02:00
lucaronin
d211d89ab8 fix: clear stale editor state on vault switch 2026-05-07 00:25:15 +02:00
lucaronin
5c3a49d2ca fix: handle shorthand type sort fields 2026-05-06 23:19:50 +02:00
lucaronin
1eeda047c2 feat: bundle docs for Tolaria agents 2026-05-06 23:08:42 +02:00
lucaronin
55dcdd4c87 fix: allow local model network access on macos 2026-05-06 18:26:57 +02:00
Luca Rossi
366d240cc4 Updated release notes 2026-05-06 17:07:45 +02:00
lucaronin
e19db5cab7 feat: add table of contents shortcut 2026-05-06 16:45:18 +02:00
lucaronin
74eecfda02 docs: add release notes for v2026-05-06 2026-05-06 16:22:11 +02:00
lucaronin
d093bd44bc fix(git): use macos keychain credentials for remotes 2026-05-06 16:09:33 +02:00
lucaronin
2e7c5cb433 fix: restore native image attachments 2026-05-06 14:47:43 +02:00
lucaronin
41ee40d427 fix(type-actions): handle type filename collisions 2026-05-06 14:30:03 +02:00
lucaronin
5bf59a9d2f fix(note-list): keep status dot steady while typing 2026-05-06 13:42:04 +02:00
lucaronin
ee24982c72 fix(editor): repair malformed numbered list blocks 2026-05-06 13:15:40 +02:00
lucaronin
755a743e93 fix(download): keep public installer page visible 2026-05-06 12:46:12 +02:00
lucaronin
35be37d256 fix(onboarding): keep ai setup dialog scrollable 2026-05-06 12:22:35 +02:00
lucaronin
9c0d0a8566 chore: ratchet codescene threshold 2026-05-06 12:04:45 +02:00
lucaronin
0294bb6fc0 fix(ai): preserve composer focus after replies 2026-05-06 12:04:45 +02:00
lucaronin
dee1c8b1e8 fix: refine frontmatter date picker 2026-05-06 11:52:15 +02:00
lucaronin
e22b4047ed refactor(test): improve app test harness health 2026-05-06 11:17:46 +02:00
lucaronin
691f5eec7c fix: expand editor whitespace selection hit area 2026-05-06 10:49:06 +02:00
lucaronin
7cf2a077ed test: cover editor flush when returning to switched note 2026-05-06 04:08:58 +02:00
lucaronin
b40c1051af feat(properties): speed up distant date edits 2026-05-05 20:44:27 +02:00
lucaronin
7a274d3a83 fix(editor): guard stale whiteboard block writes 2026-05-05 19:54:09 +02:00
lucaronin
634975e505 fix(vault): coalesce background file activity 2026-05-05 19:23:12 +02:00
lucaronin
88f5d04b8c fix(editor): preserve exact copied text 2026-05-05 18:18:56 +02:00
lucaronin
59408bbebd fix: raise CI Node heap for release builds 2026-05-05 17:36:29 +02:00
lucaronin
bc97ade133 fix(editor): support whitespace drag selection 2026-05-05 16:51:56 +02:00
lucaronin
eb6b58eb36 fix: refine breadcrumb overflow actions 2026-05-05 15:46:18 +02:00
lucaronin
520b2c9799 feat(settings): follow system theme 2026-05-05 15:00:24 +02:00
lucaronin
d5ecd567d0 fix(editor): guard stale wikilink click targets 2026-05-05 13:43:48 +02:00
lucaronin
f88b359bb5 fix(editor): render wikilinks inside tables 2026-05-05 13:02:56 +02:00
lucaronin
67ada08e05 fix(editor): ignore stale history diff responses 2026-05-05 12:29:46 +02:00
lucaronin
6ce04eead6 fix(ai): avoid Codex Windows batch shims 2026-05-05 11:45:56 +02:00
lucaronin
b8e3d8193c fix(editor): guard stale table handle state 2026-05-05 11:20:43 +02:00
lucaronin
9c09b5ad78 fix(editor): guard composing enter in list items 2026-05-05 10:15:59 +02:00
lucaronin
a6caa2b069 fix(linux): enable fcitx input for AppImage Wayland 2026-05-05 09:54:39 +02:00
lucaronin
0f231048cf fix(vault): retry transient access denied saves 2026-05-05 09:32:08 +02:00
lucaronin
8a3d00e8c6 docs: add/update ADR for media previews and vault refresh policy (guard — from commit d347cda, 92c0b89) 2026-05-05 09:32:08 +02:00
lucaronin
bfd175224a ci: split frontend and rust quality lanes 2026-05-05 04:06:28 +02:00
lucaronin
0df9c1d707 fix(types): normalize built-in notes type creation 2026-05-05 03:41:41 +02:00
lucaronin
ccfa148014 fix(nav): guard stale entity backlink metadata 2026-05-05 03:19:51 +02:00
lucaronin
dda4943425 test(editor): cover mermaid property refresh 2026-05-05 03:00:47 +02:00
lucaronin
be87c3864c fix(ai): remount composer after native input 2026-05-05 02:43:49 +02:00
lucaronin
982d6476d7 fix(editor): guard stale suggestion query cleanup 2026-05-05 02:19:49 +02:00
lucaronin
92c0b895dc fix(editor): preserve focus during vault refresh 2026-05-05 02:00:28 +02:00
lucaronin
75a8444b41 fix(window): preserve windows properties geometry 2026-05-05 01:43:13 +02:00
lucaronin
d347cda9e3 fix(preview): allow native media playback 2026-05-05 01:08:33 +02:00
lucaronin
f5adbf9cf7 feat(settings): add note display preferences 2026-05-04 22:07:27 +02:00
lucaronin
19059fbf81 fix(frontmatter): preserve crlf inbox organization 2026-05-04 20:35:01 +02:00
lucaronin
1694e3ad07 fix(editor): route clipboard images to attachments 2026-05-04 19:12:14 +02:00
lucaronin
e46613b82b fix: stabilize tldraw whiteboard assets 2026-05-04 18:34:07 +02:00
김덕환
38ea51627d fix(i18n): correct Korean locale mistranslations (#524)
- Inbox: '받은 편지함' → 'Inbox' (이메일 용어 오역. 다른 언어 전부 Inbox 유지)
- Note: '메모' → '노트' (앱 전체 용어 불일치 수정)
- Vault: '보관소/보관함' → 'Vault' (다른 언어 전부 Vault 유지)
- Views: '조회수' → '뷰' (YouTube 조회수 뉘앙스 제거)
- Guidance: '가이드ANCE' → '가이던스' (오타 수정)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-04 18:05:19 +02:00
lucaronin
e5fa40b826 docs: add release notes for v2026-05-04 2026-05-04 16:52:41 +02:00
lucaronin
ff4a78f964 feat: load readable release notes dynamically 2026-05-04 15:07:09 +02:00
lucaronin
9973d8d4e6 fix: bundle tldraw whiteboard assets 2026-05-04 14:57:09 +02:00
lucaronin
e7e33c479d fix: make AI target selection exclusive 2026-05-04 13:30:42 +02:00
lucaronin
69b9da6da6 feat: add readable stable release notes 2026-05-04 13:01:06 +02:00
lucaronin
71ef8325a0 feat: add lazy table of contents panel 2026-05-04 12:48:18 +02:00
lucaronin
582d1d25d6 fix: stabilize embedded diagrams 2026-05-04 11:28:53 +02:00
lucaronin
ebf4545d46 fix: show type-derived instance property placeholders 2026-05-04 10:55:04 +02:00
lucaronin
3c483ba0e6 feat(editor): add table of contents panel 2026-05-04 06:08:09 +02:00
lucaronin
71629763ee chore: ignore local codacy runtime 2026-05-04 05:25:43 +02:00
lucaronin
417e37f1d1 refactor(paths): share note path identity 2026-05-04 04:59:50 +02:00
lucaronin
a3e3192b66 refactor(editor): share durable block codecs 2026-05-04 04:09:14 +02:00
lucaronin
86c503c1f9 refactor(ai): share model provider catalog 2026-05-04 02:59:20 +02:00
lucaronin
21a47b4a77 fix(editor): avoid modern array copy in rich export 2026-05-04 02:07:32 +02:00
lucaronin
64d961bd98 fix(editor): type ime cancel listener 2026-05-04 01:18:28 +02:00
lucaronin
1b218f5250 fix(editor): keep toolbar hidden through ime settle 2026-05-04 01:02:07 +02:00
lucaronin
dfb9f98b7a fix(types): block same-path type collisions 2026-05-03 23:44:24 +02:00
lucaronin
6a033cd2db fix(status): keep git popup above panels 2026-05-03 20:25:50 +02:00
lucaronin
b872b6148c fix(menu): route windows menu actions 2026-05-03 20:00:37 +02:00
lucaronin
c7b6832cb1 fix: harden stale note rename and open flows 2026-05-03 19:51:15 +02:00
lucaronin
05a9aacd1e fix(git): preserve unicode path output 2026-05-03 19:24:27 +02:00
lucaronin
701ebe3f07 fix: hide toolbar during ime composition 2026-05-03 18:36:40 +02:00
lucaronin
f49b8f60dc fix: guard fresh-note heading paste 2026-05-03 18:27:26 +02:00
lucaronin
39615f49f3 fix: guard appimage colrv1 emoji font startup 2026-05-03 18:06:42 +02:00
lucaronin
3d7a0ee936 fix: guard stale checklist toggles 2026-05-03 17:42:04 +02:00
lucaronin
9faee02e04 test: observe hardened autosave requests 2026-05-03 17:25:31 +02:00
lucaronin
dfff3a848b fix: resolve codacy critical security findings 2026-05-03 17:25:31 +02:00
lucaronin
aaf0336733 feat: add direct AI model providers 2026-05-03 16:23:15 +02:00
lucaronin
5b906cb3a4 fix(inspector): show blank frontmatter properties 2026-05-03 15:05:37 +02:00
choi woo-nyong
0a40f12a6a fix: ko-KR 사이드바 Views 그룹 번역을 "조회수"에서 "보기"로 바로잡아 (#514)
sidebar.group.views 키가 "조회수"로 오역되어 있어 기능과 무관한 표현이었다

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-03 12:53:51 +02:00
lucaronin
033759598a fix: improve editor block controls 2026-05-03 12:28:00 +02:00
lucaronin
871b62fbd3 feat: add markdown whiteboards 2026-05-03 12:17:20 +02:00
lucaronin
aa310b25df fix(ai): expand agent vault paths before spawn 2026-05-03 11:57:15 +02:00
lucaronin
dbb12e96fe feat(editor): add code block copy action 2026-05-03 11:34:56 +02:00
lucaronin
c795f118d0 fix(ui): preserve tooltip positioning under zoom 2026-05-03 11:01:49 +02:00
lucaronin
d96d6efd57 fix: support automatic rtl editor direction 2026-05-03 10:51:45 +02:00
lucaronin
67623d848b fix(layout): keep resize handles above panes 2026-05-03 10:24:16 +02:00
lucaronin
45d72297b3 test: cover image asset URLs on tab swap 2026-05-03 04:14:02 +02:00
lucaronin
4f86b97e8d fix(ai): cap agent composer height 2026-05-02 20:41:08 +02:00
lucaronin
b6dd6189a2 fix(linux): locate appimage mcp resources 2026-05-02 20:08:11 +02:00
lucaronin
ba07bafc97 fix: expose codex mcp tools reliably 2026-05-02 19:47:33 +02:00
lucaronin
6d86770abf fix: compact large ai agent context 2026-05-02 19:24:52 +02:00
lucaronin
e23ba0ae27 fix: normalize windows wikilink targets 2026-05-02 19:01:00 +02:00
lucaronin
7a97f24a0e fix: detect codex windows npm shims 2026-05-02 18:41:38 +02:00
lucaronin
20f34ab9d1 fix: preserve ai panel delete shortcuts 2026-05-02 18:12:37 +02:00
lucaronin
19b31cc96a fix: recover when active vault disappears 2026-05-02 17:53:13 +02:00
lucaronin
e5ed863d5d fix: refine sidebar folder rows 2026-05-02 17:21:58 +02:00
lucaronin
4f7f5a31e4 fix(linux): choose matching wayland preload 2026-05-02 17:01:57 +02:00
lucaronin
439e2b7f66 fix: prefetch tauri nsis tools in release ci 2026-05-02 16:45:59 +02:00
lucaronin
048d27243d fix: refine slash command menu 2026-05-02 16:39:43 +02:00
lucaronin
58e129cdbc fix: stabilize editor block dragging 2026-05-02 12:18:59 +02:00
lucaronin
713b486750 fix: render release history tabs as pills 2026-05-02 11:25:39 +02:00
lucaronin
7d66e2f8e1 fix: guard table handle add selection 2026-05-02 11:16:09 +02:00
lucaronin
cd6847d59e feat: refine titlebar navigation controls 2026-05-02 10:56:36 +02:00
lucaronin
74c9841ad7 fix(ui): refine icon picker appearance 2026-05-02 10:16:24 +02:00
lucaronin
ba41854f0e fix: ignore malformed reload entries 2026-05-02 10:00:50 +02:00
lucaronin
6532ee125a fix(vault): prevent gitignored folder scan freezes 2026-05-02 09:28:51 +02:00
lucaronin
c9698b853e fix(notes): serialize immediate note creation writes 2026-05-02 03:56:48 +02:00
lucaronin
db16f2ff5e fix(views): ignore stale saved-view deletes 2026-05-02 03:25:02 +02:00
lucaronin
1e5d83d4a3 refactor(commands): share app command manifest 2026-05-02 02:57:45 +02:00
lucaronin
062de2656e refactor(frontmatter): share canonical key rules 2026-05-02 02:16:13 +02:00
lucaronin
65d14ebf6f fix(editor): theme code blocks in light mode 2026-05-02 01:47:42 +02:00
lucaronin
0b5149ba8d fix(ai): preserve gui path for cli agents 2026-05-02 01:26:34 +02:00
lucaronin
c23a3c1caf fix: add sidebar section bottom spacing 2026-05-02 01:00:06 +02:00
lucaronin
44e2870697 fix(editor): preserve pasted markdown raw toggles 2026-05-02 00:50:21 +02:00
lucaronin
802b6b77d9 fix: align macOS header controls 2026-05-02 00:35:14 +02:00
lucaronin
bb09fcfd6f fix: repair blocknote core patch 2026-05-01 23:45:38 +02:00
lucaronin
c8ac12f3dc feat: cache note content and parsed editor blocks 2026-05-01 23:14:36 +02:00
lucaronin
fbfda2f15c fix(types): render css type colors 2026-05-01 21:02:42 +02:00
lucaronin
c4c7737e83 fix(types): guard root type filename collisions 2026-05-01 20:33:07 +02:00
lucaronin
c24c60b433 fix(editor): guard stale block side menu actions 2026-05-01 19:58:35 +02:00
lucaronin
21532de941 fix: remove legacy startup loading skeleton 2026-05-01 19:11:45 +02:00
lucaronin
f92efd72df fix: keep note windows editable 2026-05-01 18:36:48 +02:00
lucaronin
6e41328f21 fix: tolerate colliding frontmatter properties 2026-05-01 18:09:48 +02:00
lucaronin
5b2b4cd569 fix: recognize type frontmatter case-insensitively 2026-05-01 17:45:14 +02:00
Jarek Krochmalski
494aeb9533 feat: add Polish (pl-PL) translation (#475)
Add complete Polish locale covering all 525 translation keys:
- New pl-PL.json catalog with full Polish translations
- Register pl-PL in APP_LOCALES and LOCALE_DEFINITIONS in i18n.ts
- Add locale.plPL label to en.json and all existing locale catalogs
- Add pl-PL target to lara.yaml for Lara CLI sync
- Add "polish" to language settings search keywords

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-01 15:02:18 +02:00
lucaronin
8272e68934 fix: ignore stale rich editor beforeinput views 2026-05-01 13:33:39 +02:00
lucaronin
7ba20c1e83 fix: guard stale table handle drags 2026-05-01 12:55:23 +02:00
lucaronin
3384b9acd0 fix: make editor left block handle draggable 2026-05-01 12:15:14 +02:00
lucaronin
8b36ff6b00 fix: avoid active editor remount on unrelated vault refresh 2026-05-01 11:40:14 +02:00
lucaronin
acb35cb5df docs: add/update ADR for Tauri frontend readiness watchdog (guard — from commit 1c010dd) 2026-05-01 08:03:02 +02:00
lucaronin
a8bda0a11b fix: allow linux window controls 2026-05-01 07:01:58 +02:00
lucaronin
4acbe17e1e refactor: consolidate sidebar item interactions 2026-05-01 06:34:25 +02:00
lucaronin
09759d204c test: cover traditional chinese wikilinks 2026-05-01 06:11:09 +02:00
lucaronin
7a01ef6952 fix: recover formula note parse failures 2026-05-01 05:44:22 +02:00
lucaronin
1c010ddc22 fix: recover inert macos webview startup 2026-05-01 04:58:44 +02:00
lucaronin
0df5c7882d ci: bound vitest coverage parallelism 2026-05-01 04:32:51 +02:00
lucaronin
2d138f1564 ci: bound frontend coverage parallelism 2026-05-01 04:19:39 +02:00
lucaronin
c9ee81c5dc test: cover locale-stable note subtitles 2026-05-01 04:06:02 +02:00
lucaronin
04fa74e8d8 fix: keep mcp setup dialog scrollable 2026-05-01 03:47:13 +02:00
lucaronin
b42ed35596 refactor: consolidate blocknote stale guards 2026-05-01 03:18:19 +02:00
lucaronin
75e1a82ff0 test: cover windows type creation 2026-05-01 02:42:20 +02:00
lucaronin
2af2156526 test: cover windows typed note creation 2026-05-01 02:24:07 +02:00
lucaronin
568ada8992 fix: sanitize appimage git subprocess env 2026-05-01 02:01:49 +02:00
lucaronin
0987946cff fix: surface codex final replies 2026-05-01 01:29:46 +02:00
lucaronin
81b03f05b6 chore: ratchet codescene thresholds 2026-05-01 00:59:32 +02:00
lucaronin
ee71a00c6f feat: add paste without formatting command 2026-05-01 00:59:32 +02:00
lucaronin
adcaa8a387 fix: align ai agent permission modes 2026-04-30 21:18:15 +02:00
lucaronin
4a03f1675e fix: parse crlf multibyte headings safely 2026-04-30 20:18:31 +02:00
lucaronin
50c304fce5 fix: reduce autosave pressure while typing 2026-04-30 19:50:44 +02:00
lucaronin
aa4d4fd12e fix: revert sidebar navigation arrows 2026-04-30 19:33:04 +02:00
lucaronin
b31f0b57da fix: keep macos startup responsive 2026-04-30 18:58:48 +02:00
lucaronin
d3a577b488 fix: copy mcp config through native clipboard 2026-04-30 18:22:14 +02:00
lucaronin
fbb0927401 fix: preserve mermaid svg styles under tauri csp 2026-04-30 17:48:19 +02:00
lucaronin
687d6403a3 fix: remove rename view menu action 2026-04-30 17:28:19 +02:00
lucaronin
1738dd2405 fix: persist claude power user shell access 2026-04-30 16:45:10 +02:00
lucaronin
8c286a4856 fix: stabilize linux appimage webkit rendering 2026-04-30 16:17:37 +02:00
lucaronin
0331b40ae2 fix: persist layout panel widths 2026-04-30 15:53:32 +02:00
lucaronin
d53c3fa7e5 fix: detect linuxbrew ai agent binaries 2026-04-30 15:01:35 +02:00
lucaronin
3c14d60be7 fix: stream gemini cli replies 2026-04-30 14:16:27 +02:00
lucaronin
765a16a4e8 fix: ignore stale BlockNote side menu blocks 2026-04-30 14:02:48 +02:00
lucaronin
886ded9575 fix: canonicalize simplified chinese ui language 2026-04-30 13:34:46 +02:00
lucaronin
831f6eb72b fix: guard uninitialized BlockNote suggestion updates 2026-04-30 13:16:55 +02:00
lucaronin
45efb08b23 fix: persist supported ui language settings 2026-04-30 12:58:26 +02:00
lucaronin
fdc7d8fd84 fix: guard note suggestions during reload 2026-04-30 12:38:23 +02:00
lucaronin
690ace2d1f fix: resolve linux mcp server resources 2026-04-30 12:22:29 +02:00
lucaronin
a267a00716 test: cover native menu event dispatch 2026-04-30 11:57:23 +02:00
lucaronin
536cab3491 fix: align view customization with type actions 2026-04-30 11:39:25 +02:00
lucaronin
e49af5855b fix: ignore stale vault refreshes after note switch 2026-04-30 10:54:16 +02:00
lucaronin
9012cd901b test: include multibyte search smoke in pre-push 2026-04-30 10:37:39 +02:00
lucaronin
0d6e3853f2 fix: make search snippets unicode-safe 2026-04-30 10:23:54 +02:00
lucaronin
8754b1bc77 docs: add ADRs for vault-root navigation and safe analytics events (guard — from commits ec83e9b, 18b4f9d) 2026-04-30 08:03:04 +02:00
lucaronin
ec5751711f feat: improve type sidebar actions 2026-04-30 07:48:34 +02:00
lucaronin
0995c90cc0 fix: show pointer cursor on interactive controls 2026-04-30 07:14:05 +02:00
lucaronin
dede400296 fix: surface desktop menu update feedback 2026-04-30 06:46:45 +02:00
lucaronin
65ff44eb50 feat: open inline images in lightbox 2026-04-30 06:24:38 +02:00
lucaronin
2c06c8dcc2 fix: disable unsafe shiki highlighting on old webkit 2026-04-30 06:06:36 +02:00
lucaronin
bc56eec35e fix: block frontmatter writes for attachments 2026-04-30 05:37:58 +02:00
lucaronin
388d389ba4 fix: disable unsafe editor link opens 2026-04-30 05:21:42 +02:00
lucaronin
a1a4cb50d0 feat: add sidebar navigation arrows 2026-04-30 04:52:28 +02:00
lucaronin
a7ed9adaae fix: ignore stale code block controls 2026-04-30 04:33:19 +02:00
lucaronin
f8f08f4160 feat: reuse type appearance controls for views 2026-04-30 04:14:47 +02:00
lucaronin
c0afae2296 fix: repair blocknote patch metadata 2026-04-30 03:47:26 +02:00
lucaronin
176f983dc9 fix: render opencode text parts after tools 2026-04-30 03:37:31 +02:00
lucaronin
01a87ed478 fix: ignore stale suggestion menu close 2026-04-30 03:13:15 +02:00
lucaronin
18b4f9d078 feat: retrofit safe product analytics events 2026-04-30 02:58:36 +02:00
lucaronin
fd3afc2493 feat: configure all notes file visibility 2026-04-30 02:17:49 +02:00
lucaronin
0ad207e318 fix: reject unsafe editor serialization 2026-04-30 01:47:21 +02:00
lucaronin
2e40945c2d fix: guard blocknote popover references 2026-04-30 01:20:12 +02:00
lucaronin
54d53b882f fix: dedupe native menu state sync 2026-04-30 00:58:07 +02:00
lucaronin
848d718373 fix: repair empty editor block swaps 2026-04-30 00:42:46 +02:00
lucaronin
ec83e9b839 feat: show vault root in folder tree 2026-04-30 00:18:33 +02:00
lucaronin
ce859aa0e7 fix: stabilize inbox organize navigation 2026-04-29 23:40:28 +02:00
lucaronin
1f4889fe66 fix: keep vault loading shell visible 2026-04-29 23:12:33 +02:00
lucaronin
2a2ea1b8ef fix: uncap wide editor content 2026-04-29 22:41:05 +02:00
lucaronin
b2a50d9870 fix: remove wide note max width 2026-04-29 22:24:54 +02:00
d 🔹
cca72a9d6a fix: pass 'en-US' to word-count toLocaleString in note subtitles (#438)
Subtitle helpers used the system locale to format word counts, while
adjacent date formatting in the same file already pins 'en-US'. On
locales like de-DE this produced '1.200 words' alongside English copy
('words', 'Created') and English-formatted dates, and broke
`noteListHelpers extra coverage` plus two SearchPanel cases on any
machine with a non-comma thousands separator.

Pass 'en-US' explicitly in both formatSubtitle and formatSearchSubtitle
so the number separator matches the surrounding English text and the
existing date convention in the file.

Closes #429

Co-authored-by: voidborne-d <voidborne@agentmail.to>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 22:08:56 +02:00
Alexandre ETEOCLE
ef0e85b053 ci: add rpm bundle to linux release workflows
Adds RPM generation, validation, intermediate artifact upload, and final GitHub Release publish patterns for Linux alpha and stable workflows.
2026-04-29 22:04:34 +02:00
github-actions[bot]
e7d74a7880 Merge branch 'main' into main 2026-04-29 19:58:07 +00:00
lucaronin
2624d32b50 fix: keep large vault startup responsive 2026-04-29 21:47:29 +02:00
github-actions[bot]
8cb5d0fd88 Merge branch 'main' into main 2026-04-29 19:42:54 +00:00
Luca Rossi
9c42da5745 Revise README for platform support and clarity
Updated README to reflect support for macOS, Windows, and Linux. Enhanced clarity in download instructions and removed outdated Gemini CLI setup section.
2026-04-29 21:42:21 +02:00
github-actions[bot]
cebcd17db0 Merge branch 'main' into main 2026-04-29 19:40:29 +00:00
Muescha
454acb5ded doc: README add installation via Homebrew (#338)
Updated installation instructions via Homebrew ( https://formulae.brew.sh/cask/tolaria ) and added a section for getting started with Tolaria.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 21:40:01 +02:00
github-actions[bot]
4276c1c3ac Merge branch 'main' into main 2026-04-29 19:39:54 +00:00
lucaronin
2a691f68db fix: keep vault asset previews available after switches 2026-04-29 21:33:20 +02:00
lucaronin
c4de154410 test: stabilize wikilink smoke insertion 2026-04-29 21:29:17 +02:00
lucaronin
670aae9f2e fix: create type documents at vault root 2026-04-29 21:29:17 +02:00
github-actions[bot]
fa3cf3f7f3 Merge branch 'main' into main 2026-04-29 19:27:18 +00:00
hhungxun
f3b4e5e61d fix: preload Wayland client for Linux AppImage (#405)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 21:26:50 +02:00
github-actions[bot]
8d761751e7 Merge branch 'main' into main 2026-04-29 19:21:47 +00:00
Khanh Thanh
4c4f01af43 Add Vietnamese language support and update locale files (#421)
- Added 'vi' (Vietnamese) to the list of supported app locales in i18n.ts.
- Updated locale definitions to include Vietnamese translations for various keys.
- Modified existing locale files (de-DE, en, es-419, es-ES, fr-FR, it-IT, ja-JP, ko-KR, pt-BR, pt-PT, ru-RU, zh-CN) to include Vietnamese translations for Vietnamese and ensure consistency.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 21:21:24 +02:00
github-actions[bot]
eabab20453 Merge branch 'main' into main 2026-04-29 19:18:49 +00:00
rubinette
79d7eb9795 feat: add comprehensive Traditional Chinese (zh-TW) locale support (#419)
- Add Traditional Chinese (zh-TW) locale support throughout the app
- Add and register a comprehensive zh-TW translation file
- Update tests to cover normalization and handling of zh-TW and its aliases
- Refine locale registry to include zh-TW definition and improve language keyword handling
- Extend locale-related keywords and display options in en.json for zh-TW locale

Signed-off-by: rubinette <rubinette@hey.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 21:18:17 +02:00
github-actions[bot]
fdd6a64c72 Merge branch 'main' into main 2026-04-29 18:55:44 +00:00
lucaronin
d1ed826714 fix: suppress watcher refresh after new note writes 2026-04-29 20:45:07 +02:00
github-actions[bot]
2db83d0810 Merge branch 'main' into main 2026-04-29 18:23:25 +00:00
lucaronin
2e8286d2b3 fix: normalize Windows verbatim note paths 2026-04-29 20:10:43 +02:00
github-actions[bot]
cdb303d98c Merge branch 'main' into main 2026-04-29 18:05:55 +00:00
lucaronin
cb9ebaadad fix: keep manual selection after inbox organize 2026-04-29 19:53:33 +02:00
github-actions[bot]
fe2a35b09e Merge branch 'main' into main 2026-04-29 17:40:28 +00:00
lucaronin
c295c9f2b5 feat: add note width modes 2026-04-29 19:26:24 +02:00
github-actions[bot]
816145ea0f Merge branch 'main' into main 2026-04-29 16:32:08 +00:00
lucaronin
a4b11089c0 fix: debounce rich editor serialization 2026-04-29 18:19:35 +02:00
github-actions[bot]
5d3661995c Merge branch 'main' into main 2026-04-29 15:25:20 +00:00
lucaronin
6f29542528 fix: render mermaid diagrams from notes 2026-04-29 17:14:28 +02:00
github-actions[bot]
c06b673e62 Merge branch 'main' into main 2026-04-29 12:34:15 +00:00
lucaronin
dcd0d73848 fix: open vault files externally 2026-04-29 14:25:15 +02:00
github-actions[bot]
45e7416267 Merge branch 'main' into main 2026-04-29 12:24:43 +00:00
lucaronin
0e8c8fb61a docs: add product analytics workflow 2026-04-29 14:24:12 +02:00
github-actions[bot]
743b259fb5 Merge branch 'main' into main 2026-04-29 11:59:15 +00:00
lucaronin
42bdd559ef fix: move mcp config copy to settings 2026-04-29 13:51:28 +02:00
github-actions[bot]
154e6f68be Merge branch 'main' into main 2026-04-29 11:46:39 +00:00
lucaronin
3a6c97272d fix: allow pdf asset previews in csp 2026-04-29 13:32:29 +02:00
github-actions[bot]
053404afee Merge branch 'main' into main 2026-04-29 11:29:05 +00:00
lucaronin
f53a3746d3 fix: localize recent UI copy 2026-04-29 13:25:22 +02:00
lucaronin
23145a05f5 feat: preview pdf files in app 2026-04-29 13:14:44 +02:00
github-actions[bot]
459eae1334 Merge branch 'main' into main 2026-04-29 10:22:51 +00:00
lucaronin
5b86b0b50d fix: validate active note cache refresh 2026-04-29 12:09:58 +02:00
lucaronin
dc1b1783b6 docs: add localization developer workflow 2026-04-29 12:09:58 +02:00
lucaronin
3dc520abea feat: add safe note open cache 2026-04-29 12:09:58 +02:00
github-actions[bot]
22b366d6f9 Merge branch 'main' into main 2026-04-29 10:00:02 +00:00
lucaronin
792b3009dd fix: keep saved view reorder UI native 2026-04-29 11:51:22 +02:00
github-actions[bot]
9540a4e9dc Merge branch 'main' into main 2026-04-29 09:28:10 +00:00
lucaronin
6ee7f219f5 feat: add gemini cli agent support 2026-04-29 11:16:52 +02:00
github-actions[bot]
de1cad448b Merge branch 'main' into main 2026-04-29 08:54:27 +00:00
lucaronin
3db1ed9dc5 fix: split ai permission tooltips 2026-04-29 10:44:20 +02:00
github-actions[bot]
1133b8ee6f Merge branch 'main' into main 2026-04-29 08:42:58 +00:00
lucaronin
7a7ef5d9ca fix: improve ai permission toggle contrast 2026-04-29 10:35:19 +02:00
github-actions[bot]
ae4dbefe83 Merge branch 'main' into main 2026-04-29 08:31:25 +00:00
Luca Rossi
4c42cf4d52 Updated release download link 2026-04-29 10:30:53 +02:00
github-actions[bot]
2f1ffeffcb Merge branch 'main' into main 2026-04-29 08:17:58 +00:00
lucaronin
405b711e16 revert: restore editor side menu ordering 2026-04-29 10:06:03 +02:00
github-actions[bot]
a3a89ef483 Merge branch 'main' into main 2026-04-29 06:19:49 +00:00
lucaronin
83705e26f5 docs: add ADRs for gitignored visibility and saved view ordering (guard — from commits d2c15b8, fd92ff5) 2026-04-29 08:03:17 +02:00
github-actions[bot]
6c533b7055 Merge branch 'main' into main 2026-04-29 05:56:55 +00:00
lucaronin
434927160c fix: stabilize radix overlay presence 2026-04-29 07:45:02 +02:00
github-actions[bot]
d54467cbfe Merge branch 'main' into main 2026-04-29 05:09:38 +00:00
lucaronin
ec2e04a464 test: wait for date picker smoke note load 2026-04-29 06:55:07 +02:00
github-actions[bot]
359103b0d9 Merge branch 'main' into main 2026-04-29 04:50:54 +00:00
lucaronin
a17ec689a3 fix: tolerate stale vault watcher listeners 2026-04-29 06:36:39 +02:00
github-actions[bot]
2302594c5c Merge branch 'main' into main 2026-04-29 04:21:53 +00:00
lucaronin
eda4d380e0 fix: let disk decide type file conflicts 2026-04-29 06:09:31 +02:00
github-actions[bot]
e226804947 Merge branch 'main' into main 2026-04-29 03:41:19 +00:00
lucaronin
f9b155ba6e fix: repair malformed editor block entries 2026-04-29 05:27:39 +02:00
github-actions[bot]
165697f678 Merge branch 'main' into main 2026-04-29 03:17:43 +00:00
lucaronin
4a62008b1f fix: clamp stale raw editor selections 2026-04-29 05:05:15 +02:00
github-actions[bot]
a6a8146975 Merge branch 'main' into main 2026-04-29 03:00:21 +00:00
lucaronin
a08cee7965 refactor: share cli agent runtime scaffold 2026-04-29 04:47:16 +02:00
github-actions[bot]
ec03ffc7a9 Merge branch 'main' into main 2026-04-29 02:47:15 +00:00
lucaronin
a7c480af62 fix: normalize stale vault entry metadata 2026-04-29 04:33:18 +02:00
github-actions[bot]
beeecb1845 Merge branch 'main' into main 2026-04-29 02:11:15 +00:00
lucaronin
f4790d5763 fix: detect Windows npm ai agent shims 2026-04-29 04:01:53 +02:00
lucaronin
ed52a9e800 fix: preserve frontmatter date picker days 2026-04-29 03:56:44 +02:00
lucaronin
2fcd808f09 fix: detect Windows npm ai agent shims 2026-04-29 03:53:54 +02:00
github-actions[bot]
f7a54593ac Merge branch 'main' into main 2026-04-29 01:43:08 +00:00
lucaronin
9d15286f7d fix: stop orphaned mcp server processes 2026-04-29 03:29:16 +02:00
github-actions[bot]
6209b0baa5 Merge branch 'main' into main 2026-04-29 01:16:17 +00:00
lucaronin
537bd0f3ec fix: normalize Windows vault image paths 2026-04-29 03:02:57 +02:00
github-actions[bot]
8739605572 Merge branch 'main' into main 2026-04-29 00:48:11 +00:00
github-actions[bot]
f18a309469 Merge branch 'main' into main 2026-04-29 00:36:54 +00:00
lucaronin
648241604f feat: add theme mode command actions 2026-04-29 02:35:52 +02:00
lucaronin
c6f3d9d945 fix: create type files in type folder 2026-04-29 02:24:01 +02:00
github-actions[bot]
aa1d54f4c9 Merge branch 'main' into main 2026-04-28 23:54:31 +00:00
lucaronin
65e54f108a fix: render saved view colors 2026-04-29 01:40:16 +02:00
github-actions[bot]
f816be5bbe Merge branch 'main' into main 2026-04-28 23:28:37 +00:00
lucaronin
58f8a774b5 chore: ratchet codescene thresholds 2026-04-29 01:26:33 +02:00
lucaronin
0bf280d3ec fix: satisfy ai permission mode clippy 2026-04-29 01:26:33 +02:00
lucaronin
d2d4746d5c feat: add vault ai agent permission modes 2026-04-29 01:26:33 +02:00
github-actions[bot]
101423c977 Merge branch 'main' into main 2026-04-28 22:27:31 +00:00
lucaronin
91db7c0098 fix: pause saves without active vault 2026-04-29 00:19:47 +02:00
github-actions[bot]
e66c81d896 Merge branch 'main' into main 2026-04-28 22:04:32 +00:00
lucaronin
87908d982f fix: allow guidance open path 2026-04-28 23:56:08 +02:00
github-actions[bot]
e399d4c1fe Merge branch 'main' into main 2026-04-28 21:50:18 +00:00
lucaronin
40544fc551 fix: detect nvm ai agent binaries 2026-04-28 23:39:48 +02:00
github-actions[bot]
63c5c5e6b1 Merge branch 'main' into main 2026-04-28 21:39:27 +00:00
lucaronin
f34fdb0289 fix: guard stale suggestion menu actions 2026-04-28 23:29:09 +02:00
github-actions[bot]
d39fa1aa9d Merge branch 'main' into main 2026-04-28 21:09:09 +00:00
lucaronin
e601a42f41 fix: surface ai agent final replies 2026-04-28 23:00:06 +02:00
lucaronin
de627c2478 fix: normalize missing vault metadata 2026-04-28 22:52:07 +02:00
Oleg Kossoy
52aae9ed96 fix: tolerate non-JS pnpm binary in coverage runner
`run-vitest-coverage.mjs` reused `process.env.npm_execpath` with
`process.execPath` (node) for a fast path that skips the pnpm wrapper.
That assumes `npm_execpath` is a JS file, but standalone pnpm installs
ship a native Mach-O / ELF binary, so node tries to load the binary as
a CJS module and crashes with `SyntaxError: Invalid or unexpected
token`, taking the pre-push hook down with it.

Only take the fast path when the path actually looks like a JS module
(`.js`/`.mjs`/`.cjs`); otherwise fall back to invoking `pnpm` from
PATH, which is what the script already does when `npm_execpath` is
unset.
2026-04-28 23:49:40 +03:00
Oleg Kossoy
f72b7d3361 fix: ignore .git symlink targets in vault watcher
Vaults stored on iCloud / Dropbox commonly use a `.git -> .git.nosync`
symlink so the cloud sync engine ignores git internals. Background
`git fetch` calls (auto-pull, remote-status, gitstatusd) wrote to the
real `.git.nosync/` directory, which the watcher's `.git` component
filter did not match — every fetch fired a `vault-changed` event and
triggered `reloadVault`, flickering the "Reload vault" status badge
multiple times per second.

Resolve the real git directory at watcher start (symlink, regular dir,
or `gitdir:` pointer for worktrees/submodules) and reject any path
inside it. Also filter `.gitstatus.*` cache dirs and `*.icloud`
placeholders, which are noisy on the same setups.
2026-04-28 23:49:40 +03:00
lucaronin
46d9aaed48 fix: dedupe editor toolbar state closes 2026-04-28 21:57:26 +02:00
lucaronin
6b96b5caf6 fix: reset editor selection before note swaps 2026-04-28 21:31:54 +02:00
lucaronin
595c48ca59 fix: apply settings theme changes immediately 2026-04-28 21:16:30 +02:00
lucaronin
fd92ff5e9b feat: allow manual saved view ordering 2026-04-28 20:41:07 +02:00
lucaronin
a39b346a75 fix: detect mise node for mcp setup 2026-04-28 20:28:56 +02:00
lucaronin
deac10caa2 fix: detect nvm pi installs 2026-04-28 19:54:39 +02:00
lucaronin
0e1e0ff47a fix: preserve ai composer line breaks 2026-04-28 19:32:08 +02:00
lucaronin
a645edbc19 fix: repair malformed editor block ids 2026-04-28 18:56:36 +02:00
lucaronin
48f1fb0b50 feat: add gemini cli external ai setup 2026-04-28 18:30:14 +02:00
lucaronin
1aede09834 fix: preserve wikilinks inside markdown tables 2026-04-28 18:00:15 +02:00
lucaronin
d51b76f992 fix: restrict sentry releases to stable builds 2026-04-28 17:17:23 +02:00
lucaronin
8e66fb4c19 fix: preserve table math markdown 2026-04-28 16:47:52 +02:00
lucaronin
a2843a0cf3 fix: persist immediate note creation before open 2026-04-28 15:01:32 +02:00
lucaronin
38fb8a7370 feat: copy mcp config from app 2026-04-28 14:03:10 +02:00
lucaronin
be5a8bb300 fix: make editor left handle draggable 2026-04-28 12:31:13 +02:00
lucaronin
ce040c75fd test: cover table hover stability 2026-04-28 10:46:06 +02:00
lucaronin
ced89a42bd fix: render mermaid diagrams with lightbox 2026-04-28 10:32:37 +02:00
lucaronin
20a960551f fix: update fallback vault git email 2026-04-28 10:08:18 +02:00
lucaronin
2e331315e8 fix: refine Italian locale labels 2026-04-28 09:43:44 +02:00
lucaronin
d2c15b8469 feat: hide gitignored vault content 2026-04-28 09:07:21 +02:00
lucaronin
2fe6c5b580 fix: respect selected ai agent in right panel 2026-04-28 07:36:22 +02:00
lucaronin
2a224f78f8 refactor: simplify note mutation flushing 2026-04-28 06:25:39 +02:00
lucaronin
b85d54bb5d fix: preserve ai agent final result text 2026-04-28 06:17:33 +02:00
lucaronin
9c05e17d37 fix: sanitize custom view filenames 2026-04-28 05:39:41 +02:00
lucaronin
cf6ad71fff ci: de-duplicate coverage-backed test lane 2026-04-28 04:53:39 +02:00
lucaronin
db82aee172 refactor: consolidate ai agent session flow 2026-04-28 04:20:40 +02:00
lucaronin
c72d833624 feat: add pi ai agent support 2026-04-28 03:41:04 +02:00
lucaronin
a3ca78fced fix: guard git init parent folders 2026-04-28 02:38:32 +02:00
lucaronin
e912ab9a9f fix: use selected vault for mcp bridge 2026-04-28 02:26:14 +02:00
lucaronin
49717dc671 feat: add opencode ai agent option 2026-04-28 01:26:21 +02:00
lucaronin
d9a0a04ddb chore: ratchet codescene thresholds 2026-04-28 01:21:59 +02:00
lucaronin
0561fe68b1 fix: restore editor block handle reordering 2026-04-28 01:09:47 +02:00
lucaronin
2f078fab11 feat: watch active vault filesystem changes 2026-04-27 23:54:50 +02:00
lucaronin
98fde6571a feat: render mermaid note diagrams 2026-04-27 22:14:23 +02:00
lucaronin
a5c4b8fa20 fix: hide ai agent subprocess windows 2026-04-27 21:21:00 +02:00
lucaronin
1301653b81 fix: avoid tooltip dropdown popper nesting 2026-04-27 20:58:31 +02:00
lucaronin
ee90b05159 fix: guard inline input types 2026-04-27 20:36:59 +02:00
lucaronin
b6c11b1468 fix: close claude print-mode stdin 2026-04-27 19:25:53 +02:00
lucaronin
2fbdafa6f2 fix: set git identity before remote connect 2026-04-27 18:59:23 +02:00
lucaronin
39f44b86aa fix: copy fenced code blocks exactly 2026-04-27 18:31:51 +02:00
lucaronin
cb3274cdbc fix: harden windows mcp setup 2026-04-27 17:51:25 +02:00
lucaronin
76d8bba332 fix: remove hidden Claude permission bypass 2026-04-27 16:46:32 +02:00
lucaronin
86853cf337 fix: allow Codex to edit active vault 2026-04-27 16:07:05 +02:00
lucaronin
9a292d2476 fix: map releases to Sentry 2026-04-27 15:27:31 +02:00
lucaronin
4a22b02810 test: harden locale catalog validation 2026-04-27 14:48:49 +02:00
github-actions[bot]
e4270cda3b Merge branch 'main' into main 2026-04-27 12:45:03 +00:00
lucaronin
9cb0de37a5 fix: parse crlf frontmatter 2026-04-27 14:31:51 +02:00
github-actions[bot]
2a794cb373 Merge branch 'main' into main 2026-04-27 11:37:50 +00:00
lucaronin
324af6b271 fix: normalize folder filtering across path separators 2026-04-27 13:27:20 +02:00
Disheng Qiu
926db0eeb5 feat: add lara-powered app localization 2026-04-27 13:06:59 +02:00
lucaronin
b1a97834c9 fix: keep raw editor full width 2026-04-27 11:54:15 +02:00
lucaronin
da13cb38ac fix: satisfy editor find command contract 2026-04-27 10:57:58 +02:00
lucaronin
dbf8ba5f40 feat: add editor find and replace 2026-04-27 10:57:58 +02:00
lucaronin
3e8a6e5d7c fix: render inline math while typing 2026-04-27 10:44:35 +02:00
lucaronin
8141644729 fix: revert note width modes 2026-04-27 10:13:55 +02:00
lucaronin
0d88c75718 Merge branches 'main', 'main', 'main' and 'main' of https://github.com/refactoringhq/tolaria 2026-04-27 09:42:36 +02:00
lucaronin
f8721f2a1b feat: add note width modes 2026-04-27 05:34:11 +02:00
lucaronin
fb39c6679a fix: handle invalid Windows save paths 2026-04-27 04:04:04 +02:00
lucaronin
f1bed131bf fix: refresh type state after type changes 2026-04-27 03:25:24 +02:00
lucaronin
257cf6ed02 fix: keep theme tooltip inside window 2026-04-27 02:20:28 +02:00
lucaronin
908daafaa1 feat: add basic file actions 2026-04-27 01:45:16 +02:00
lucaronin
d15437face test: cover consecutive IME composer input 2026-04-27 01:05:34 +02:00
lucaronin
30281f879d chore: ratchet codescene thresholds 2026-04-27 00:49:39 +02:00
lucaronin
4ef6edfb10 feat: preview vault images in app 2026-04-27 00:36:24 +02:00
lucaronin
918419e371 fix: respect macos control text bindings 2026-04-26 23:22:53 +02:00
lucaronin
2704002973 fix: remove note drag and drop flows 2026-04-26 22:51:08 +02:00
lucaronin
9265b8f117 fix: stabilize AI composer text editing 2026-04-26 22:11:54 +02:00
lucaronin
943fa854e1 fix: satisfy localization build 2026-04-26 19:39:56 +02:00
lucaronin
a2f8ba72a2 fix: complete app localization pass 2026-04-26 19:37:21 +02:00
lucaronin
1d546dde3d fix: allow note drops to insert wikilinks 2026-04-26 18:15:22 +02:00
lucaronin
63877e61ec fix: store window state in logical points 2026-04-26 17:54:41 +02:00
lucaronin
8896f1d451 fix: restore saved window size after launch 2026-04-26 17:54:41 +02:00
lucaronin
f523099854 feat: add vault loading skeleton 2026-04-26 17:35:09 +02:00
lucaronin
15f38f096f feat: support non-git vaults 2026-04-26 17:08:14 +02:00
lucaronin
cdd2ddec6c feat: persist main window placement 2026-04-26 15:45:34 +02:00
lucaronin
292d3739df fix: prevent editor handling note wikilink drops 2026-04-26 14:37:11 +02:00
lucaronin
1136c1948e fix: clarify macos release artifact names 2026-04-26 14:29:47 +02:00
lucaronin
8406042db1 fix: support native folder drops in AI inputs 2026-04-26 13:59:39 +02:00
lucaronin
f07cbbe0eb test: reflect cloned getting started vault in smoke 2026-04-26 12:28:01 +02:00
lucaronin
768da9f955 fix: prevent bottom bar wrap at narrow widths 2026-04-26 12:12:05 +02:00
lucaronin
f58e9d8930 fix: allow editor runtime styles under CSP 2026-04-26 11:31:41 +02:00
lucaronin
133242eff5 fix: keep compact status bar on one row 2026-04-26 10:33:16 +02:00
lucaronin
aebd2bea4a fix: refresh language settings draft 2026-04-26 09:46:11 +02:00
lucaronin
60238fbe78 fix: persist native language settings 2026-04-26 09:17:28 +02:00
lucaronin
5336236e79 test: cover native command wrappers 2026-04-26 08:39:56 +02:00
lucaronin
f9719f11b5 feat: add app localization foundation 2026-04-26 08:18:47 +02:00
lucaronin
e4083ded1a fix: detect claude cli on windows 2026-04-26 07:34:27 +02:00
lucaronin
65a89f102a fix: detect codex from macos shell paths 2026-04-26 06:48:35 +02:00
lucaronin
5b34ec2980 feat: publish intel mac builds 2026-04-26 06:07:33 +02:00
lucaronin
e579979829 fix: preserve composed ai input 2026-04-26 05:24:05 +02:00
lucaronin
c7edc71a97 feat: add note layout preference 2026-04-26 04:41:18 +02:00
lucaronin
5fd8f8fb40 feat: add markdown math support 2026-04-26 03:52:10 +02:00
lucaronin
694419d442 fix: disable native text suggestions 2026-04-26 03:01:10 +02:00
lucaronin
e82fb2a5c7 test: cover table cell paste regression 2026-04-26 02:58:46 +02:00
lucaronin
39234dcf52 fix: block unsafe editor links 2026-04-26 02:16:20 +02:00
lucaronin
36e8a62284 fix: isolate blocknote side-menu clicks 2026-04-26 01:20:11 +02:00
lucaronin
d622b91b81 docs: ratchet codescene thresholds 2026-04-26 00:31:55 +02:00
lucaronin
5a546b9a8b feat: add generic MCP config support 2026-04-26 00:15:22 +02:00
lucaronin
b8c1a094b6 fix: resolve ime selection typing 2026-04-25 23:50:04 +02:00
lucaronin
c2c56cfca5 fix: harden inline wikilink ime handling 2026-04-25 23:46:31 +02:00
lucaronin
449a62e31f fix: address unicode slug type check 2026-04-25 23:11:50 +02:00
lucaronin
7d6fd5cc51 fix: preserve unicode note title slugs 2026-04-25 23:08:30 +02:00
lucaronin
75ed6460ac feat: drop notes into editors as wikilinks 2026-04-25 22:31:57 +02:00
lucaronin
a3096c596f fix: reflow status bar at narrow widths 2026-04-25 21:59:10 +02:00
lucaronin
6105c7527c refactor: fix hook dependency lint warnings 2026-04-25 21:26:25 +02:00
lucaronin
92eee0d470 fix: keep editor selection stable around media blocks 2026-04-25 21:21:41 +02:00
lucaronin
f14fda09d5 fix: accept finder folder drops in ai composer 2026-04-25 20:41:56 +02:00
lucaronin
31d12d8cc8 test: stabilize smoke web server 2026-04-25 19:51:41 +02:00
lucaronin
6a002f0dc0 fix: guard missing inline input types 2026-04-25 19:23:00 +02:00
lucaronin
9d30d1165f fix: harden linux appimage startup 2026-04-25 18:54:38 +02:00
lucaronin
b709acd58a fix: type delete confirmation shortcuts 2026-04-25 18:23:37 +02:00
lucaronin
080e3498a7 fix: submit delete confirmation on enter 2026-04-25 18:20:32 +02:00
lucaronin
51010ca578 fix: show manual update feedback 2026-04-25 17:15:19 +02:00
lucaronin
15bc3a4701 fix: disable command palette text correction 2026-04-25 16:49:48 +02:00
lucaronin
3ebfa642fa fix: allow runtime stylesheet injection 2026-04-25 16:06:17 +02:00
lucaronin
8207b87c33 fix: include linux signatures in stable release 2026-04-25 14:39:19 +02:00
lucaronin
602a099ac3 fix: polish contribute dialog copy 2026-04-25 13:54:39 +02:00
lucaronin
2315122c4a fix: clear stale editor selection on note switch 2026-04-25 13:27:33 +02:00
lucaronin
ab4fcdbaae ci: include linux alpha updater signatures 2026-04-25 12:45:31 +02:00
lucaronin
a56cb8c287 fix: allow raw editor styles under CSP 2026-04-25 12:03:54 +02:00
lucaronin
b93e6d491a ci: test linux alpha release artifacts 2026-04-25 11:59:21 +02:00
lucaronin
3152ff2c6e fix: allow optional pending diff loaders 2026-04-25 11:46:59 +02:00
lucaronin
dd018cf98b fix: queue diff opening until tab activation 2026-04-25 11:46:59 +02:00
lucaronin
d0d9d90096 test: stabilize H1 wikilink smoke 2026-04-25 11:20:14 +02:00
lucaronin
944efada94 fix: load note windows without vault scan 2026-04-25 10:53:09 +02:00
lucaronin
ef4ad256e3 feat: add Refactoring support card 2026-04-25 10:31:26 +02:00
lucaronin
c427cd2492 fix: guard native file drops 2026-04-25 10:07:43 +02:00
lucaronin
151e993ab7 chore: ratchet codescene thresholds 2026-04-25 02:29:08 +02:00
lucaronin
60554d0b96 fix: handle IME composition in AI composer 2026-04-25 02:29:08 +02:00
lucaronin
4929f11b6d fix: harden windows release and git subprocesses 2026-04-25 00:58:49 +02:00
lucaronin
3f4e5b585f fix: refresh image asset scope after attachment writes 2026-04-25 00:19:57 +02:00
lucaronin
cd49c81bd3 feat: add status bar theme toggle 2026-04-24 23:58:26 +02:00
lucaronin
b9f3dc9e1a fix: refine contribute panel actions 2026-04-24 23:44:02 +02:00
lucaronin
122aba7878 fix: load current note in new window 2026-04-24 23:29:44 +02:00
lucaronin
05896bde19 fix: restore editor external link toolbar actions 2026-04-24 23:25:22 +02:00
lucaronin
87a933f39c fix: replace silent catch blocks with warnings 2026-04-24 22:44:36 +02:00
lucaronin
54c0efa3e4 feat: add dark mode foundation 2026-04-24 22:28:07 +02:00
lucaronin
bdde0c8943 fix: bound note prefetch cache by size 2026-04-24 21:53:47 +02:00
lucaronin
a474303eb9 fix: preserve back history after cmd-click wikilinks 2026-04-24 21:15:27 +02:00
lucaronin
4701485ee5 fix: keep empty title clicks in h1 2026-04-24 20:13:34 +02:00
lucaronin
135a8a0adf fix: handle nested release artifacts 2026-04-24 19:48:56 +02:00
lucaronin
0f5d7d5340 fix: restore folder boundary validation 2026-04-24 19:23:55 +02:00
lucaronin
d6b3c0aef3 feat: add windows desktop release support 2026-04-24 19:23:55 +02:00
lucaronin
f896c01829 fix: publish linux stable release artifacts 2026-04-24 18:53:43 +02:00
lucaronin
c3cff0c461 feat: replace feedback with contribute modal 2026-04-24 17:45:43 +02:00
lucaronin
8c7f9238cb fix: gate macos shortcut constants for linux clippy 2026-04-24 16:45:05 +02:00
lucaronin
38acebba7c feat: add linux desktop support 2026-04-24 16:25:36 +02:00
lucaronin
622977aeb8 fix: restore clone modal invoke typing 2026-04-24 15:18:50 +02:00
lucaronin
9ce1c6f854 refactor: restore clone modal code health 2026-04-24 15:13:24 +02:00
lucaronin
46865638cd fix: harden clone git repo flow 2026-04-24 14:56:17 +02:00
lucaronin
564ee0a387 test: cover native command wrappers 2026-04-24 14:44:01 +02:00
lucaronin
11655c0283 fix: harden inbox auto-advance merge 2026-04-24 14:35:54 +02:00
github-actions[bot]
c24fd30266 Merge branch 'main' into main 2026-04-24 12:04:53 +00:00
lucaronin
daa1e3e985 fix: keep ai composer responsive after image paste 2026-04-24 13:52:58 +02:00
github-actions[bot]
44c7f43e57 Merge branch 'main' into main 2026-04-24 11:43:53 +00:00
lucaronin
f2c83be4b3 fix: format MCP config tests 2026-04-24 13:32:30 +02:00
lucaronin
6104402bfb fix: write Claude Code MCP config to .claude.json 2026-04-24 13:22:33 +02:00
github-actions[bot]
b3744e3bf5 Merge branch 'main' into main 2026-04-24 10:43:45 +00:00
lucaronin
67c8598b87 fix: keep clone flows off the UI thread 2026-04-24 12:34:00 +02:00
github-actions[bot]
52620c80dc Merge branch 'main' into main 2026-04-24 10:27:33 +00:00
lucaronin
7e386ff70c docs: move contributing guide to repo root 2026-04-24 12:27:17 +02:00
github-actions[bot]
aba5550b8e Merge branch 'main' into main 2026-04-24 10:07:41 +00:00
Luca Rossi
309b816e6d Add custom funding URL to FUNDING.yml 2026-04-24 12:07:27 +02:00
github-actions[bot]
f552d8abff Merge branch 'main' into main 2026-04-24 10:02:39 +00:00
lucaronin
9f6779e9f1 docs: add contributing guide 2026-04-24 11:46:35 +02:00
github-actions[bot]
bddf5106bb Merge branch 'main' into main 2026-04-24 06:31:23 +00:00
lucaronin
ba5c165016 test: harden macos titlebar coverage 2026-04-24 08:20:30 +02:00
github-actions[bot]
39415c350f Merge branch 'main' into main 2026-04-24 06:14:23 +00:00
Brian Lee
499e481df8 Merge branches 'main', 'main', 'main' and 'main' of https://github.com/refactoringhq/tolaria 2026-04-24 15:07:00 +09:00
Brian Lee
843cedd14e fix: align auto-advance test bridge types 2026-04-24 15:06:13 +09:00
Brian Lee
05156ca793 feat: add inbox auto-advance setting 2026-04-24 15:04:08 +09:00
lucaronin
2308b269c8 docs: add ADRs for cache concurrency and git signing fallback (guard — from commits 42bb6c4, 6dbcc33) 2026-04-24 08:03:58 +02:00
lucaronin
b7f482bf27 fix: respect macos header double click action 2026-04-24 07:47:53 +02:00
lucaronin
33d7404d6c fix: keep blocknote tables editable after focus changes 2026-04-24 07:02:40 +02:00
lucaronin
0ecac2ab79 fix: allow unicode type filenames 2026-04-24 04:01:43 +02:00
lucaronin
7c9f2b0dda fix: guard editor selection churn around inline actions 2026-04-24 03:25:50 +02:00
lucaronin
24e317cfa6 fix: handle unreadable non-utf8 files gracefully 2026-04-24 03:02:03 +02:00
lucaronin
66c183fa61 fix: keep IME composition stable in editors 2026-04-24 02:50:12 +02:00
lucaronin
dacf9bb19b fix: guard note loading without active vault 2026-04-24 02:16:49 +02:00
lucaronin
3c92c200ab fix: sync onboarding vault handoff 2026-04-24 01:45:11 +02:00
lucaronin
61c3a3e21b docs: ratchet codescene thresholds 2026-04-24 01:39:28 +02:00
lucaronin
2e7c94d159 fix: register created onboarding vaults 2026-04-24 01:26:22 +02:00
lucaronin
98ade7411a fix: avoid duplicate onboarding vault writes 2026-04-24 01:23:16 +02:00
lucaronin
18776341f8 fix: persist onboarding vault selections 2026-04-24 01:08:13 +02:00
lucaronin
57bbcdbaef fix: format cache hardening helpers 2026-04-24 00:00:10 +02:00
lucaronin
42bb6c4b18 fix: harden vault cache updates 2026-04-24 00:00:10 +02:00
lucaronin
070cd3ac6d test: stabilize telemetry onboarding smoke 2026-04-23 23:11:46 +02:00
lucaronin
accc6270d3 fix: restore vault images and default folders 2026-04-23 23:11:46 +02:00
lucaronin
941f12aa1b test: clear vitest cache before coverage 2026-04-23 22:45:22 +02:00
lucaronin
4710a2d0a9 fix: reuse local smoke server by default 2026-04-23 22:27:38 +02:00
lucaronin
65a421215a fix: satisfy arrow ligature type checks 2026-04-23 22:14:20 +02:00
lucaronin
4ed0289989 Revert "test: simplify main entrypoint setup"
This reverts commit 4ad32faa7b.
2026-04-23 22:14:11 +02:00
lucaronin
4ad32faa7b test: simplify main entrypoint setup 2026-04-23 22:10:02 +02:00
lucaronin
c9623d5ca8 feat: add arrow ligatures in both editors 2026-04-23 22:06:01 +02:00
lucaronin
77286457bf fix: anchor settings default agent dropdown 2026-04-23 20:09:49 +02:00
lucaronin
5b8aa8da0d fix: guard pull refresh during unsaved editor edits 2026-04-23 19:34:15 +02:00
lucaronin
21b6d8984a fix: support symlinked vault notes 2026-04-23 18:36:45 +02:00
lucaronin
11fc123492 fix: guard folder picker after update install 2026-04-23 18:16:43 +02:00
lucaronin
d98fa94e14 fix: retry commits when signing helper is missing 2026-04-23 17:27:53 +02:00
lucaronin
add3c4075f fix: thread vault image path through tab swaps 2026-04-23 16:35:27 +02:00
lucaronin
fb0d550eb2 fix: make starter vault images portable 2026-04-23 16:31:36 +02:00
lucaronin
6adf7fd53b fix: capture react root errors in sentry 2026-04-23 15:34:39 +02:00
lucaronin
3b4d9599d2 fix: finish empty Claude Code runs 2026-04-23 14:15:15 +02:00
lucaronin
bb1b18686f fix: stabilize smoke dev server watch 2026-04-23 13:45:24 +02:00
lucaronin
6dbcc334bf fix: create repos despite commit signing 2026-04-23 13:26:01 +02:00
lucaronin
4944365ed7 fix: detect local Claude Code installs 2026-04-23 12:24:26 +02:00
lucaronin
c4c6bb6c57 fix: restore folder row toggle 2026-04-23 09:27:17 +02:00
lucaronin
35dd2dd47b docs: add/update ADR for note retargeting (guard — from commit 2c9361d7) 2026-04-23 08:07:56 +02:00
lucaronin
89141bae14 chore: ratchet codescene thresholds 2026-04-23 00:53:45 +02:00
lucaronin
2b9294f884 style: format note move backend 2026-04-23 00:41:04 +02:00
lucaronin
a756af54ec fix: keep note drop targets active during drag 2026-04-23 00:27:09 +02:00
lucaronin
b01b156dfb fix: align folder rows with sidebar items 2026-04-23 00:21:20 +02:00
lucaronin
2c9361d704 feat: retarget notes to types and folders 2026-04-22 23:56:00 +02:00
lucaronin
cea71a4fd9 Merge branch 'main' of https://github.com/refactoringhq/tolaria 2026-04-22 22:54:40 +02:00
lucaronin
8b73ef5836 feat: add sidebar folder rename and delete actions 2026-04-22 22:49:16 +02:00
lucaronin
a6a727a4c0 fix: align inspector create-type typing 2026-04-22 22:42:04 +02:00
lucaronin
bf13eed3ab fix: handle filename collisions in create flows 2026-04-22 22:37:01 +02:00
lucaronin
a27a70e552 fix: allow PostHog and Google Fonts in app CSP 2026-04-22 22:24:45 +02:00
lucaronin
ca92bf4f77 fix: restore keyboard shortcut typing 2026-04-22 21:22:35 +02:00
lucaronin
9fda0a67ec fix: make note renames crash-safe 2026-04-22 21:18:38 +02:00
lucaronin
0ffb7c65a9 Ignore history shortcuts while editing 2026-04-22 21:06:27 +02:00
lucaronin
633d9f1496 Add security policy and private reporting guidance 2026-04-22 20:46:24 +02:00
lucaronin
ba3d413b94 fix: recover from missing note paths 2026-04-22 20:18:26 +02:00
lucaronin
f3286922ad fix: satisfy clippy for MCP status 2026-04-22 19:28:58 +02:00
lucaronin
b0b476c9e5 fix: harden external AI tool setup 2026-04-22 19:18:52 +02:00
lucaronin
dac696b752 fix: guard toolbar popover anchors 2026-04-22 17:57:30 +02:00
lucaronin
cda87ee6c0 feat: add sidebar folder rename and delete actions 2026-04-22 17:16:56 +02:00
lucaronin
33324a5c79 fix: harden mcp bridge and vault boundaries 2026-04-22 17:14:42 +02:00
lucaronin
f28626bbfe fix: finish claude agent streams 2026-04-22 15:59:12 +02:00
lucaronin
1bc2e3d1e0 docs: add ADRs for startup and linkify lifecycles (guard — from commits 59268acd, bf442646) 2026-04-22 15:09:45 +02:00
lucaronin
8e6bdab3e6 docs: ratchet codescene thresholds 2026-04-22 00:31:54 +02:00
lucaronin
d52362ad9b fix: repair tiptap patch integrity 2026-04-22 00:21:46 +02:00
lucaronin
bf442646f5 fix: silence duplicate linkify init warnings 2026-04-22 00:05:32 +02:00
lucaronin
1b3a444368 fix: show welcome on fresh install without missing vault 2026-04-21 23:34:07 +02:00
lucaronin
c4da7f1f2f fix: narrow raw mode sync override 2026-04-21 19:04:02 +02:00
lucaronin
c5fe291a9a fix: sync inspector edits into raw editor 2026-04-21 19:04:02 +02:00
lucaronin
59268acd04 fix: guard empty vault path during startup 2026-04-21 19:04:01 +02:00
lucaronin
3e4e2ebdc6 refactor: simplify breadcrumb and note-list search ui 2026-04-21 19:03:11 +02:00
Luca Rossi
05375258a2 Rename 'Documentation' section to 'Tech Docs' 2026-04-21 18:06:30 +02:00
Luca Rossi
0c0354bfb8 Updated README badges 2026-04-21 18:05:54 +02:00
lucaronin
6023e95d5b fix: serialize vitest coverage files 2026-04-21 17:38:44 +02:00
lucaronin
88d5694353 fix: retry flaky vitest coverage teardown 2026-04-21 17:35:00 +02:00
lucaronin
db6b38843b fix: keep vitest coverage shards off workspace 2026-04-21 17:21:35 +02:00
lucaronin
ad9901bbf5 fix: stabilize coverage suite reruns 2026-04-21 17:08:43 +02:00
lucaronin
b785c53ef9 test: increase strategic coverage 2026-04-21 16:54:52 +02:00
lucaronin
8ae1ade220 test: cover live sidebar type picker sync 2026-04-21 15:16:39 +02:00
lucaronin
35b035643e fix: adapt save reload callback 2026-04-21 15:09:27 +02:00
lucaronin
d3d663af0b fix: reload vault entries on tauri startup 2026-04-21 15:02:42 +02:00
lucaronin
8f624350d8 test: cover journal vault scan 2026-04-21 12:37:46 +02:00
lucaronin
0e54173834 test: cover explicit journal visibility 2026-04-21 12:27:06 +02:00
lucaronin
a6f1524a78 ci: reject placeholder telemetry secrets 2026-04-21 11:06:16 +02:00
lucaronin
4cd5baeb55 fix: sync sidebar types with live vault state 2026-04-21 10:45:39 +02:00
lucaronin
fb1265e088 test: stabilize note list search timing 2026-04-21 10:14:18 +02:00
lucaronin
ca89662231 fix: sync raw editor frontmatter state 2026-04-21 10:06:54 +02:00
lucaronin
97f285e3cb ci: upload coverage to codecov 2026-04-21 10:06:54 +02:00
Luca Rossi
4bd4ef43dc Added release badge in README 2026-04-21 09:43:40 +02:00
Luca Rossi
be29880a3e Added walkthroughs to the README 2026-04-21 09:22:51 +02:00
lucaronin
7be5519f61 fix: sort alpha releases chronologically 2026-04-21 09:10:16 +02:00
lucaronin
8cefbe949a fix: refine derived inverse relationship labels 2026-04-21 09:08:25 +02:00
lucaronin
dd0a55b8e8 docs: add ADR for external vault refresh after pulls and agent edits (guard — from commit 0020ade6) 2026-04-21 08:03:02 +02:00
lucaronin
610276b1bc docs: ratchet codescene thresholds 2026-04-21 00:56:59 +02:00
lucaronin
e6c968e37e fix: satisfy note list keyboard build 2026-04-21 00:50:44 +02:00
lucaronin
367a66f32a fix: debounce note list search 2026-04-21 00:47:45 +02:00
lucaronin
6a4046915c fix: harden startup telemetry urls 2026-04-20 23:38:41 +02:00
lucaronin
ec8b637c84 fix: hide uncloned getting started vault 2026-04-20 23:04:34 +02:00
lucaronin
3ee42fb856 test: align create-note smoke placeholder 2026-04-20 22:13:19 +02:00
lucaronin
eacf05a9f8 fix: reload notes after agent edits 2026-04-20 22:04:38 +02:00
lucaronin
a199fad803 fix: widen editor focus block typing 2026-04-20 18:38:51 +02:00
lucaronin
e956c806e5 fix: keep editor sync from clobbering live inserts 2026-04-20 18:35:50 +02:00
lucaronin
7a6e3863ad fix: reopen active note after pull updates 2026-04-20 16:04:11 +02:00
lucaronin
e0e2dc4063 test: add pull refresh regression 2026-04-20 15:53:56 +02:00
lucaronin
0020ade6d7 fix: tighten pulled note refresh flow 2026-04-20 14:43:52 +02:00
lucaronin
503e482c7d fix: normalize inverse relationship labels 2026-04-20 12:49:57 +02:00
lucaronin
0d0e6af4c9 fix: keep vault menu remove action in row 2026-04-20 12:19:10 +02:00
lucaronin
dbaf361878 test: fix starter-vault app shell coverage 2026-04-20 11:32:44 +02:00
lucaronin
d3741c82f5 test: fix starter-vault shell regression 2026-04-20 11:20:27 +02:00
lucaronin
b90d2f8612 fix: reorder welcome onboarding actions 2026-04-20 11:04:29 +02:00
lucaronin
9536efee0e fix: persist AI panel chats across reopen 2026-04-20 10:28:48 +02:00
lucaronin
c86a6ab9c5 docs: add/update ADR for starter vault local-first remote flow (guard — from commit 374b4f66) 2026-04-20 10:28:00 +02:00
Luca Rossi
1d7c027700 Updated Tolaria Icon 2026-04-20 10:25:46 +02:00
lucaronin
74e50c64bb fix: hide stale getting started vault entries 2026-04-20 01:02:08 +02:00
lucaronin
8b97ef711f feat: refresh welcome screen branding 2026-04-20 00:45:14 +02:00
lucaronin
08855cee92 fix: defer git history during note switching 2026-04-20 00:09:52 +02:00
lucaronin
03042bb49c fix: normalize shipped telemetry secrets 2026-04-19 23:27:57 +02:00
lucaronin
cee4bef179 fix: harden shipped telemetry config 2026-04-19 23:16:34 +02:00
lucaronin
f223d4f654 refactor: curate demo-vault-v2 fixture 2026-04-19 22:26:35 +02:00
lucaronin
2f693c4476 fix: dedupe new type palette action 2026-04-19 21:38:26 +02:00
lucaronin
44f0c7cb74 feat: open favorites in neighborhood mode 2026-04-19 21:27:32 +02:00
lucaronin
ea14f1a389 fix: use rendered release note html 2026-04-19 20:53:39 +02:00
lucaronin
db21ae4708 feat: improve release history page 2026-04-19 20:37:53 +02:00
lucaronin
8019c4fb2d fix: defer missing vault startup state 2026-04-19 20:08:10 +02:00
lucaronin
0d552e1370 fix: satisfy note-open perf build checks 2026-04-19 19:34:05 +02:00
lucaronin
977f5593fa fix: stop note navigation from scaling with vault size 2026-04-19 19:30:58 +02:00
lucaronin
261a5befc7 chore: ratchet codescene thresholds 2026-04-19 18:56:47 +02:00
lucaronin
8d3cce7b49 fix: satisfy neighborhood history build checks 2026-04-19 18:50:08 +02:00
lucaronin
e0594820f0 fix: unwind neighborhood history on escape 2026-04-19 18:46:10 +02:00
lucaronin
b98d41212e fix: stop seeding title during property init 2026-04-19 18:18:35 +02:00
lucaronin
098dc421cb test: add isolated vitest coverage runner 2026-04-19 18:09:58 +02:00
lucaronin
326ccb08e1 test: stabilize vitest coverage artifacts 2026-04-19 18:03:58 +02:00
lucaronin
c2bbdb0085 test: stabilize git default branch in helpers 2026-04-19 17:52:58 +02:00
lucaronin
039f9e447f fix: cache inspector link lookups 2026-04-19 17:35:08 +02:00
lucaronin
8581a1b1e6 fix: organize seeded claude shim files 2026-04-19 17:08:45 +02:00
Luca Rossi
2c5cb593d3 Removed old Project spec reverence 2026-04-19 17:05:30 +02:00
Luca Rossi
348b627029 Updated README 2026-04-19 16:44:50 +02:00
lucaronin
f5cc5ffb4f fix: satisfy clippy and stabilize app startup test 2026-04-19 16:28:02 +02:00
lucaronin
374b4f6686 feat: keep getting started vaults local by default 2026-04-19 16:18:35 +02:00
lucaronin
36c20f34f4 fix: hide unavailable getting started vault 2026-04-19 15:21:59 +02:00
Luca Rossi
8551d819bd Deleted old unused font 2026-04-19 15:16:43 +02:00
lucaronin
d5497d7252 Match getting started subtitle copy 2026-04-19 14:30:07 +02:00
lucaronin
046afe0e67 Simplify Getting Started vault subtitle 2026-04-19 14:23:05 +02:00
lucaronin
cb1cc4b394 fix: include inverse neighborhood groups 2026-04-19 14:13:10 +02:00
lucaronin
00cb938766 fix: add cmd o quick-open alias 2026-04-19 13:47:02 +02:00
lucaronin
80c13272f3 fix: keep neighborhood reverse matches path-aware 2026-04-19 12:53:07 +02:00
lucaronin
9534449307 fix: hide absent neighborhood groups 2026-04-19 12:26:31 +02:00
lucaronin
2d5361b2e9 fix: keep keyboard note switching snappy 2026-04-19 11:49:48 +02:00
lucaronin
b22da65d44 fix: keep note switches instant 2026-04-19 11:29:04 +02:00
lucaronin
862af864d3 fix: preserve image selection while hovering toolbar 2026-04-19 10:48:45 +02:00
lucaronin
9ecefc90d5 feat: add stable landing page download link 2026-04-19 04:44:17 +02:00
lucaronin
9693fe24c9 fix: restore type filter icon colors 2026-04-19 04:15:12 +02:00
lucaronin
92ea32c0b8 feat: add neighborhood browsing mode 2026-04-19 03:37:33 +02:00
lucaronin
2e2bd2f3b3 fix: preserve native welcome vault creation 2026-04-19 02:37:29 +02:00
lucaronin
3556d934ee test: format vault command assertions 2026-04-19 01:55:39 +02:00
lucaronin
feef938174 fix: align fresh vault seed files 2026-04-19 01:33:53 +02:00
lucaronin
67a9930ea1 feat: add empty vault creation flow 2026-04-19 01:06:40 +02:00
lucaronin
aed4d05f1d docs: ratchet codescene thresholds 2026-04-19 00:25:35 +02:00
lucaronin
cac7f12c6d fix: stop duplicate native quick prompt paste 2026-04-19 00:18:42 +02:00
lucaronin
72b5b52140 feat: add view note-list controls 2026-04-18 20:31:25 +02:00
lucaronin
7deeb49751 feat: add number property type 2026-04-18 19:40:00 +02:00
lucaronin
8c0a2e7ec8 fix: share onboarding drag shell 2026-04-18 18:50:30 +02:00
lucaronin
c4e66dc74f fix: make onboarding window draggable 2026-04-18 18:09:37 +02:00
lucaronin
c0bb722db5 fix: reuse note content cache for rapid note switches 2026-04-18 17:30:49 +02:00
lucaronin
bc11c869aa fix: stabilize ai mode paste editing 2026-04-18 16:52:36 +02:00
lucaronin
502ecf0572 test: stabilize wikilink smoke coverage 2026-04-18 16:18:43 +02:00
lucaronin
8bbbba98b7 fix: correct vault path type guard 2026-04-18 16:02:19 +02:00
lucaronin
86adf860b5 fix: restore bottom bar vault switching 2026-04-18 15:59:34 +02:00
lucaronin
cec1627a81 fix: preserve image toolbar hover bridge 2026-04-18 15:12:39 +02:00
lucaronin
b00183419e fix: show Note metadata in autocomplete 2026-04-18 13:37:15 +02:00
lucaronin
ed9ceff1d2 fix: align Notes counts with explicit type 2026-04-18 13:07:17 +02:00
lucaronin
e18491bcd3 fix: preserve untitled rename session continuity 2026-04-18 12:17:24 +02:00
lucaronin
fd0cfc7fb6 fix: exclude attachments from all notes totals 2026-04-18 11:53:06 +02:00
lucaronin
6541ce5755 test: stabilize wikilink smoke timing 2026-04-18 11:19:41 +02:00
lucaronin
4456801526 fix: keep h1 typing responsive 2026-04-18 11:06:55 +02:00
lucaronin
db7aaf1385 fix: restore note type during vault repair 2026-04-18 10:38:50 +02:00
lucaronin
54b2616fa4 fix: restore manual commit dialog when autogit is off 2026-04-18 09:58:49 +02:00
lucaronin
6a90e9f770 fix: correct editor layout diff callback type 2026-04-18 09:28:18 +02:00
lucaronin
b79c78a5ba fix: save raw note changes before switching notes 2026-04-18 09:28:18 +02:00
Luca Rossi
48a8882970 Reorder CodeScene badges in README 2026-04-18 09:16:06 +02:00
Luca Rossi
98276702a2 Added codescene badges 2026-04-18 09:14:59 +02:00
lucaronin
857d55a7cd fix: stop cmd+s from renaming note titles 2026-04-18 08:25:43 +02:00
lucaronin
b78dfad384 fix: resume onboarding after telemetry on fresh start 2026-04-18 03:10:41 +02:00
lucaronin
9ac51bab9a test: pin app test active vault 2026-04-18 02:35:41 +02:00
lucaronin
1fee994863 test: cover mock vault config path 2026-04-18 02:21:48 +02:00
lucaronin
764bbf0e4c test: scope inbox sidebar assertion 2026-04-18 02:10:54 +02:00
lucaronin
ff05ba0430 chore: ratchet codescene thresholds 2026-04-18 01:56:12 +02:00
lucaronin
347df47c62 fix: stabilize smoke vault harness 2026-04-18 01:50:29 +02:00
lucaronin
7536169508 fix: resume onboarding after telemetry consent 2026-04-18 00:21:52 +02:00
lucaronin
34de384738 fix: preserve yml views in raw editor 2026-04-17 23:32:36 +02:00
lucaronin
954ee42f0b fix: restore seeded vault guidance compatibility 2026-04-17 22:55:34 +02:00
lucaronin
f67aafe442 fix: guard optional filename marks 2026-04-17 22:23:38 +02:00
lucaronin
b2122ac24c fix: stop auto-linking plain filenames 2026-04-17 22:20:48 +02:00
lucaronin
6f8810146e Sync getting-started AGENTS guidance 2026-04-17 19:15:05 +02:00
lucaronin
929ba9241e Refresh getting-started view guidance 2026-04-17 19:08:59 +02:00
lucaronin
c5e84c8218 fix: keep image toolbar controls open 2026-04-17 18:29:37 +02:00
lucaronin
fb55ed2f6d fix: remove unsupported block colors menu 2026-04-17 17:48:12 +02:00
lucaronin
91a0247187 fix: stop editor toggles from changing modified order 2026-04-17 17:12:25 +02:00
lucaronin
ce78abfd57 Restore icon property in inspector 2026-04-17 15:33:17 +02:00
lucaronin
6202892512 Shorten AI agent footer badge 2026-04-17 14:04:12 +02:00
lucaronin
2eb21356ef Polish settings panel and tooltip alignment 2026-04-17 10:53:56 +02:00
lucaronin
4cbf359c7b docs: add/update ADRs for git checkpoints and H1 rename setting (guard — from commit 323ef19) 2026-04-17 08:04:36 +02:00
lucaronin
4e7069174e test: refresh smoke selectors for toolbar actions 2026-04-17 02:49:51 +02:00
lucaronin
22cefe4912 feat: add action tooltips to note controls 2026-04-17 02:34:41 +02:00
lucaronin
ce26c57e99 feat: expand note list search content 2026-04-17 01:46:04 +02:00
lucaronin
8238134435 chore: ratchet codescene thresholds 2026-04-17 00:54:16 +02:00
lucaronin
10fe1239bd fix: restore inspector build types 2026-04-17 00:47:28 +02:00
lucaronin
a84f3c93ca feat: warn on missing note types 2026-04-17 00:44:14 +02:00
lucaronin
072591ad59 test: restore serialized smoke menu bridge 2026-04-16 23:47:20 +02:00
lucaronin
323ef1913b feat: add AutoGit idle and inactive checkpoints 2026-04-16 23:38:30 +02:00
lucaronin
2b85f4e45f fix: preserve valid bold markdown in rich editor 2026-04-16 22:31:19 +02:00
lucaronin
3bcfedda5b refactor: split vault mod tests by responsibility 2026-04-16 21:59:54 +02:00
lucaronin
3e45fd898a fix: limit markdown list highlighting to markers 2026-04-16 21:13:57 +02:00
lucaronin
6334d10540 💅 polish inspector, note list, and history UI 2026-04-16 17:03:50 +02:00
lucaronin
5eeb37e32d fix: make pulse rows edge to edge 2026-04-16 16:18:45 +02:00
lucaronin
193b7c2a44 fix: align sidebar count column 2026-04-16 15:45:28 +02:00
lucaronin
a36760efe6 fix: satisfy rust clippy gate 2026-04-16 15:09:24 +02:00
lucaronin
edd5a1ce8f feat: align release version naming 2026-04-16 14:43:08 +02:00
lucaronin
991ee8689e refactor: simplify note item composition 2026-04-16 13:58:17 +02:00
lucaronin
e111700203 fix: restore relationship panel layout 2026-04-16 13:04:35 +02:00
lucaronin
2772064b2a fix: prefer local mtimes in modified sort 2026-04-16 12:44:12 +02:00
lucaronin
c0d16e4b4d feat: add h1 auto-rename setting 2026-04-16 12:18:11 +02:00
lucaronin
711d49b3bb fix: align property panel rows 2026-04-16 11:53:29 +02:00
lucaronin
53b3f5a14b fix: type editor formatting block selector 2026-04-16 11:28:50 +02:00
lucaronin
d1dbada9e4 fix: restore markdown-safe editor formatting controls 2026-04-16 11:24:37 +02:00
lucaronin
4e66f09eef fix: make note-list property pickers searchable 2026-04-16 10:25:56 +02:00
lucaronin
f5b1d763c1 docs: ratchet codescene thresholds 2026-04-16 09:51:59 +02:00
lucaronin
905754e0c5 fix: reduce editor-only window width floor 2026-04-16 09:38:34 +02:00
lucaronin
f665f089dc fix: narrow note list entity selection 2026-04-16 09:11:22 +02:00
lucaronin
57a87e43b3 fix: make keyboard note navigation predictable 2026-04-16 09:04:13 +02:00
lucaronin
1b3a9e3ecf fix: keep raw frontmatter out of markdown headings 2026-04-16 07:49:31 +02:00
lucaronin
f8bd3e1b4a feat: show property kind icons 2026-04-16 07:27:11 +02:00
lucaronin
1b10aa9660 test: cover mcp config edge cases 2026-04-16 07:03:08 +02:00
lucaronin
514c1f9ef7 fix: satisfy rust quality gates 2026-04-16 06:42:58 +02:00
lucaronin
e9c1ae2b8a fix: canonicalize hidden system metadata 2026-04-16 06:21:34 +02:00
lucaronin
e8e2004aa8 fix: make the pulse header draggable 2026-04-16 05:41:41 +02:00
lucaronin
a448945ed5 fix: format window constraint command imports 2026-04-16 05:07:33 +02:00
lucaronin
d469f220e7 fix: make main window min width follow visible panes 2026-04-16 05:03:01 +02:00
lucaronin
d60c47ee27 feat: standardize relationship property labels 2026-04-16 03:45:34 +02:00
lucaronin
087e9f13c8 fix: rename getting started starter repo 2026-04-16 03:08:29 +02:00
lucaronin
ed93b4b234 fix: stop defaulting new note status 2026-04-16 02:42:43 +02:00
lucaronin
b8dfc79e80 fix: align note list model event types 2026-04-16 02:21:55 +02:00
lucaronin
bbf98d6112 feat: customize all-notes note list columns 2026-04-16 02:18:43 +02:00
lucaronin
f3ee593dee test: include multi-selection smoke in curated suite 2026-04-16 01:46:24 +02:00
lucaronin
2bd5d64497 fix: include multi-selection keyboard action type 2026-04-16 01:39:35 +02:00
lucaronin
9c0bda55f6 fix: apply note shortcuts to full multi-selection 2026-04-16 01:36:44 +02:00
lucaronin
2c0c464eac chore: ratchet codescene thresholds 2026-04-16 00:45:17 +02:00
lucaronin
82192f23be feat: simplify bulk action bar 2026-04-16 00:38:43 +02:00
lucaronin
310e159894 fix: remove status bar note count 2026-04-16 00:01:26 +02:00
lucaronin
77a7d9d519 fix: preserve editor mode position 2026-04-15 23:33:30 +02:00
lucaronin
37f099ab1e fix: keep editor formatting markdown-safe 2026-04-15 22:42:40 +02:00
lucaronin
8f72c4839d fix: preserve zoomed block handle hover 2026-04-15 19:42:25 +02:00
lucaronin
a1331453ad fix: keep untitled rename non-blocking 2026-04-15 18:59:51 +02:00
lucaronin
8518e89f03 fix: prevent duplicate untitled saves during rename 2026-04-15 18:33:44 +02:00
lucaronin
a5cde9e2d6 fix: open pulse rows in commit diff mode 2026-04-15 17:57:38 +02:00
lucaronin
2ad3d1cae1 fix: inject telemetry envs into release builds 2026-04-15 16:59:31 +02:00
lucaronin
7eaf57d040 fix: make getting started clone destination explicit 2026-04-15 14:55:56 +02:00
lucaronin
e142b4c8f9 docs: add/update ADR for root AI guidance files (guard — from commit ec3f90e) 2026-04-15 10:23:24 +02:00
lucaronin
8f04ed59a4 fix: remap buffered saves after untitled rename 2026-04-14 23:48:30 +02:00
lucaronin
8a5763959b fix: keep untitled rename non-blocking 2026-04-14 21:15:01 +02:00
lucaronin
4ab44fbe1a fix: preserve untitled rename cursor 2026-04-14 21:10:51 +02:00
lucaronin
2720c14e22 fix: refresh stale vault agent guidance 2026-04-14 20:45:52 +02:00
lucaronin
8d3bfc2ca7 chore: ratchet codescene thresholds 2026-04-14 20:12:56 +02:00
lucaronin
9ccd0a7221 fix: remove all notes status pills 2026-04-14 20:07:54 +02:00
lucaronin
1388e7ddbe fix: align sidebar view count chips 2026-04-14 19:45:32 +02:00
lucaronin
cd76c0d65d feat: add searchable type combobox 2026-04-14 18:48:56 +02:00
lucaronin
d9fd615d4c fix: open note windows from direct note paths 2026-04-14 18:17:45 +02:00
lucaronin
1f4ae56c1c fix: preserve aliased link labels 2026-04-14 17:34:22 +02:00
lucaronin
084d3d9c71 fix: preserve exact linked note titles 2026-04-14 17:32:47 +02:00
lucaronin
9041246a32 test: stabilize CI vault defaults 2026-04-14 17:04:23 +02:00
lucaronin
6a8fe11027 fix: flush title rename saves before path changes 2026-04-14 16:49:18 +02:00
lucaronin
0078cc257a fix: harden rename saves and async deletes 2026-04-14 15:40:46 +02:00
lucaronin
9f46f22667 fix: skip alpha dmg packaging on CI 2026-04-14 15:39:33 +02:00
lucaronin
2b734da4cb fix: clear cached tauri bundle artifacts 2026-04-14 15:30:10 +02:00
lucaronin
73c619892b fix: rotate tauri target cache keys 2026-04-14 15:18:05 +02:00
lucaronin
8f1b3c75b2 feat: show property type icons in rows 2026-04-14 15:14:46 +02:00
lucaronin
39da825443 fix: standardize property chip sizing 2026-04-14 15:01:59 +02:00
lucaronin
7b86e3a524 fix: align properties panel value columns 2026-04-14 14:40:31 +02:00
lucaronin
ab92ea178f fix: align vault guidance status typing 2026-04-14 14:20:30 +02:00
lucaronin
ec3f90e865 feat: add vault ai guidance restore flow 2026-04-14 14:16:55 +02:00
lucaronin
2356afdc42 style: format vault command wrappers 2026-04-14 12:56:16 +02:00
lucaronin
9154a8153f fix: refresh default vault agents guidance 2026-04-14 12:52:32 +02:00
lucaronin
c0fbb1c40e refactor: remove obsolete scripts tooling 2026-04-14 12:28:37 +02:00
lucaronin
76fe4611c2 test: stabilize untitled auto-rename smoke 2026-04-14 12:16:50 +02:00
lucaronin
62154535c0 feat: add ai agent status bar switcher 2026-04-14 11:55:48 +02:00
lucaronin
cff9583a46 fix: support caret boundary on current target 2026-04-14 11:37:44 +02:00
lucaronin
70252b6324 fix: anchor ai mode caret after wikilinks 2026-04-14 11:35:38 +02:00
lucaronin
60da17e13b fix: restore blocknote slash menu styling 2026-04-14 11:14:49 +02:00
lucaronin
0c9bfb14c3 fix: format async delete command 2026-04-14 10:57:32 +02:00
lucaronin
48d2246f0b fix: make note deletion non-blocking 2026-04-14 10:53:08 +02:00
lucaronin
d879457f89 docs: add ADR for ratcheted CodeScene thresholds (guard — from commit 311dcc64) 2026-04-14 10:11:05 +02:00
lucaronin
311dcc6413 chore: ratchet codescene thresholds after ADR guard checks 2026-04-14 10:03:12 +02:00
lucaronin
f61e7d2ab3 docs: update ADR for selectable CLI AI agents (guard — from commit d540d76f) 2026-04-14 09:57:20 +02:00
lucaronin
1ac6bd887b fix: format alpha build label parser 2026-04-13 23:52:13 +02:00
lucaronin
f31f827a4f fix: restore alpha build label 2026-04-13 23:48:19 +02:00
lucaronin
a1ffe7a074 fix: align note list header icons 2026-04-13 23:33:46 +02:00
lucaronin
1b45394d0a fix: show filenames in changes view 2026-04-13 23:18:53 +02:00
lucaronin
95f0153c1b fix: restore aligned property panel values 2026-04-13 23:01:37 +02:00
lucaronin
a034a79db2 fix: align blocknote editor context 2026-04-13 22:45:25 +02:00
lucaronin
1f148ebe32 fix: restore vault type command export 2026-04-13 22:23:31 +02:00
lucaronin
b7cb3c2130 fix: remove legacy journal command surfaces 2026-04-13 22:21:42 +02:00
lucaronin
f50d9127d1 fix: restore editor code block highlighting 2026-04-13 22:00:50 +02:00
lucaronin
2a353d5749 fix: keep caret after command palette ai trigger 2026-04-13 21:07:10 +02:00
lucaronin
c2f9b7c20d fix: restore ai smoke compatibility 2026-04-13 19:47:38 +02:00
lucaronin
d540d76f73 feat: support selectable cli ai agents 2026-04-13 19:37:59 +02:00
lucaronin
cdfc0c9135 docs: clarify demo vault automation hygiene 2026-04-13 17:59:23 +02:00
lucaronin
5b29e061fe fix: refresh renamed note metadata 2026-04-13 17:44:41 +02:00
lucaronin
5102923c9c fix: repair release workflow parsing 2026-04-13 17:24:03 +02:00
lucaronin
87be231f9f test: format git status assertions 2026-04-13 15:30:17 +02:00
lucaronin
557a45189d fix: show diff stats in changes list 2026-04-13 15:22:21 +02:00
lucaronin
2107764550 fix: cap properties panel label column width 2026-04-13 15:01:07 +02:00
lucaronin
0a52ae6c1d fix: prevent stale raw content after note switch 2026-04-13 14:23:20 +02:00
lucaronin
487e8d0352 fix: preserve hard breaks when toggling editors 2026-04-13 14:04:25 +02:00
lucaronin
d4aacf1bea fix: focus command palette ai mode immediately 2026-04-13 13:42:46 +02:00
lucaronin
71df363bb6 fix: satisfy command palette ai push checks 2026-04-13 13:29:06 +02:00
lucaronin
91ab5be914 fix: stabilize command palette ai mode editing 2026-04-13 13:26:38 +02:00
lucaronin
9623203756 fix: restore blocknote code block theme 2026-04-13 12:59:51 +02:00
lucaronin
5897782a7e fix: stabilize new note title focus smoke 2026-04-13 12:46:22 +02:00
lucaronin
b4abc05f0e fix: harden legacy release channel fallback 2026-04-13 12:35:02 +02:00
lucaronin
8f408d9ef0 fix: guard blocknote table handles on raw return 2026-04-13 12:19:36 +02:00
lucaronin
1d778826e8 fix: restore empty editor drag region 2026-04-13 11:52:55 +02:00
lucaronin
5120d75903 fix: update getting_started.rs AGENTS template to use type: instead of is_a: 2026-04-13 08:37:40 +02:00
lucaronin
5d7426797f docs: add ADR-0060 and ADR-0061 for network-aware UI gating and AI prompt bridge (guard — from commits 739e1b3, 7933fe1) 2026-04-13 08:09:28 +02:00
lucaronin
ed54cef69e chore: update codescene thresholds after ADR additions 2026-04-13 08:05:30 +02:00
lucaronin
df6f956cde docs: add ADR-0058 and ADR-0059 for Claude Code onboarding gate and local-only commits (guard — from commits fd9de04, a13e36a) 2026-04-13 08:01:24 +02:00
lucaronin
61aae139c7 fix: remove stale daily note menu test id 2026-04-12 23:17:17 +02:00
lucaronin
14dc07084d fix: remove daily note command surfaces 2026-04-12 23:14:03 +02:00
lucaronin
73c1ecf8a9 fix: use muted editor code surfaces 2026-04-12 22:51:32 +02:00
lucaronin
de91167a28 fix: align note list header actions 2026-04-12 22:29:36 +02:00
lucaronin
a4397d48de fix: split alpha and stable release channels 2026-04-12 22:14:53 +02:00
lucaronin
fbbd53d204 test: restore smoke flows after Claude onboarding 2026-04-12 20:10:01 +02:00
lucaronin
fd9de04275 feat: add first-launch Claude Code onboarding 2026-04-12 19:56:10 +02:00
lucaronin
a13e36a504 feat: support local-only git commits without remotes 2026-04-12 19:38:25 +02:00
lucaronin
73a63085c7 fix: tighten vault switcher typing 2026-04-12 19:19:52 +02:00
lucaronin
739e1b3c20 feat: handle offline onboarding gracefully 2026-04-12 19:17:49 +02:00
lucaronin
e2745d96eb refactor: remove github auth integration 2026-04-12 17:08:07 +02:00
lucaronin
a2c84d3ce8 test: align AI smoke selector with inline input 2026-04-12 13:20:26 +02:00
lucaronin
afbea1367c fix: narrow inline wikilink selection lookup 2026-04-12 13:15:51 +02:00
lucaronin
7933fe1bfb feat: add inline AI prompts to command palette 2026-04-12 13:14:18 +02:00
lucaronin
d16aa29d18 docs: test notes must be deleted from disk after QA; update QA script paths to tolaria-qa 2026-04-12 12:31:35 +02:00
lucaronin
6188395141 fix: align desktop test bridge types 2026-04-12 12:29:13 +02:00
lucaronin
1bf5a7d6e4 fix: preserve resized tables across raw mode toggles 2026-04-12 11:45:59 +02:00
lucaronin
27e371c7fe docs: restore README content lost during Laputa→Tolaria rename 2026-04-12 10:40:50 +02:00
lucaronin
867e97b199 fix: left-align property values in inspector 2026-04-12 02:03:54 +02:00
lucaronin
2441518cee fix: restore breadcrumb center drag region 2026-04-12 01:52:31 +02:00
lucaronin
010bc32ee1 style: format Rust rename updates 2026-04-12 01:41:26 +02:00
lucaronin
361898b187 refactor: rename app branding from Laputa to Tolaria 2026-04-12 01:35:34 +02:00
lucaronin
5b5f949c74 fix: initialize new notes with empty h1 focus 2026-04-12 00:58:11 +02:00
3071 changed files with 227470 additions and 55792 deletions

17
.codacy.yaml Normal file
View File

@@ -0,0 +1,17 @@
---
exclude_paths:
- "coverage/**"
- "dist/**"
- "e2e/**"
- "node_modules/**"
- "scripts/**"
- "src/test/**"
- "src-tauri/gen/**"
- "src-tauri/resources/agent-docs/**"
- "src-tauri/target/**"
- "target/**"
- "test-results/**"
- "tests/**"
- "**/*.test.ts"
- "**/*.test.tsx"
- "vite.config.ts"

View File

@@ -1,2 +1,2 @@
HOTSPOT_THRESHOLD=9.7
AVERAGE_THRESHOLD=9.36
HOTSPOT_THRESHOLD=10.0
AVERAGE_THRESHOLD=9.95

View File

@@ -1,5 +1,10 @@
# Copy to .env.local and fill in real values
# These are never committed — .env.local is gitignored
# Copy to .env.local and fill in real values.
# These are never committed — .env.local is gitignored.
# Release workflows must mirror these values into GitHub Actions secrets:
# - VITE_SENTRY_DSN
# - SENTRY_DSN (same value as VITE_SENTRY_DSN for Rust-side crash reporting)
# - VITE_POSTHOG_KEY
# - VITE_POSTHOG_HOST
# Sentry DSN (https://sentry.io → Project → Settings → Client Keys)
VITE_SENTRY_DSN=
@@ -7,3 +12,7 @@ VITE_SENTRY_DSN=
# PostHog (https://posthog.com → Project → Settings → Project API Key)
VITE_POSTHOG_KEY=
VITE_POSTHOG_HOST=https://eu.i.posthog.com
# Lara CLI (https://github.com/translated/lara-cli)
LARA_ACCESS_KEY_ID=
LARA_ACCESS_KEY_SECRET=

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
custom: https://refactoring.fm/

2
.github/HOOKS.md vendored
View File

@@ -49,7 +49,7 @@ If the remote CodeScene scores are better than the current thresholds, the hook
## Legacy Files
The legacy `pre-commit` and `post-commit` files under `.github/hooks/` are archival only. Do not copy them into `.git/hooks`; use Husky and `.husky/` instead. `install-hooks.sh` remains as a reinstall helper that runs Husky.
The legacy `pre-commit` file under `.github/hooks/` is archival only. Do not copy it into `.git/hooks`; use Husky and `.husky/` instead. The old design `post-commit` auto-implementation hook was removed because it depended on obsolete one-off scripts. `install-hooks.sh` remains as a reinstall helper that runs Husky.
## Troubleshooting

26
.github/SETUP.md vendored
View File

@@ -14,6 +14,26 @@ Nel repository GitHub (Settings → Secrets and variables → Actions → New re
**CODESCENE_PROJECT_ID**
Trova l'ID del progetto nella dashboard CodeScene (URL: `https://codescene.io/projects/<PROJECT_ID>/...`)
**VITE_SENTRY_DSN**
```
<frontend Sentry DSN used by shipped Tolaria builds>
```
**SENTRY_DSN**
```
<same DSN as VITE_SENTRY_DSN, passed to the Rust/Tauri build for native crash reporting>
```
**VITE_POSTHOG_KEY**
```
<PostHog project API key used by shipped Tolaria builds>
```
**VITE_POSTHOG_HOST**
```
https://eu.i.posthog.com
```
### 2. Enable GitHub Actions
- Vai su Settings → Actions → General
@@ -65,6 +85,12 @@ cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check
- **Fail se code health diminuisce**
- Confronta HEAD vs base branch
### 📡 Telemetry In Release Builds
- `release.yml` e `release-stable.yml` devono ricevere `VITE_SENTRY_DSN`, `SENTRY_DSN`, `VITE_POSTHOG_KEY`, `VITE_POSTHOG_HOST`
- `VITE_SENTRY_DSN` inizializza il frontend Sentry bundle
- `SENTRY_DSN` inizializza Sentry nel binary Rust/Tauri
- `VITE_POSTHOG_KEY` / `VITE_POSTHOG_HOST` permettono ai build distribuiti di inizializzare PostHog quando l'utente abilita analytics
### 📝 Documentation
- **Warning se modifichi `src/` o `src-tauri/` ma non aggiorni `docs/`**
- Non blocca il merge, solo un reminder

View File

@@ -1,72 +0,0 @@
#!/bin/bash
# Post-commit hook: Auto-implement design changes via Claude Code
# Copy to .git/hooks/post-commit and make executable
set -e
# Check if ui-design.pen was modified in this commit
if ! git diff --name-only HEAD~1 HEAD | grep -q "ui-design.pen"; then
exit 0
fi
echo ""
echo "🎨 Design file changed - analyzing..."
# Run analyzer
ANALYZER_OUTPUT=$(node scripts/design-diff-analyzer.js --json 2>&1)
if [ $? -eq 0 ]; then
echo "✅ No implementation tasks needed (content-only changes)"
exit 0
fi
# Extract tasks from JSON
TASKS_JSON=$(echo "$ANALYZER_OUTPUT" | sed -n '/---JSON---/,$ p' | tail -n +2)
if [ -z "$TASKS_JSON" ]; then
echo "✅ No significant design changes"
exit 0
fi
echo "$ANALYZER_OUTPUT" | sed '/---JSON---/,$ d'
# Generate Claude Code prompt
PROMPT=$(cat <<EOF
The design file ui-design.pen was just updated. Analyze the changes and implement them.
Design changes detected:
$ANALYZER_OUTPUT
Tasks:
1. Review the git diff of ui-design.pen (HEAD vs HEAD~1)
2. Identify what changed (colors, typography, spacing, layout)
3. Update the corresponding code:
- Colors/Typography/Spacing → update src/theme.json
- Layout changes → update React components
- New elements → create new components
4. Test visually in Chrome (pnpm dev, open localhost:5173)
5. Run tests: pnpm test
6. Commit changes with descriptive message referencing the design update
When completely finished, run:
openclaw system event --text "✅ Design changes implemented and tested" --mode now
EOF
)
echo ""
echo "🚀 Spawning Claude Code to implement changes..."
echo ""
# Spawn Claude Code via OpenClaw
openclaw sessions spawn \
--task "$PROMPT" \
--label "design-auto-implement" \
--cleanup delete \
--timeout-seconds 600 \
--agent-id main
echo ""
echo "✅ Claude Code spawned - you'll be notified when implementation is complete"
echo ""
exit 0

View File

@@ -0,0 +1,115 @@
param(
[string]$ConfigPath = "src-tauri/tauri.windows-signing.conf.json"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Read-FirstEnv {
param([string[]]$Names)
foreach ($Name in $Names) {
$Value = [Environment]::GetEnvironmentVariable($Name)
if (-not [string]::IsNullOrWhiteSpace($Value)) {
return $Value.Trim()
}
}
throw "Set one of these environment variables: $($Names -join ', ')"
}
function Read-OptionalEnv {
param(
[string[]]$Names,
[string]$DefaultValue
)
foreach ($Name in $Names) {
$Value = [Environment]::GetEnvironmentVariable($Name)
if (-not [string]::IsNullOrWhiteSpace($Value)) {
return $Value.Trim()
}
}
return $DefaultValue
}
function Normalize-Thumbprint {
param([string]$Thumbprint)
return ($Thumbprint -replace "\s", "").ToUpperInvariant()
}
function Convert-CertificateSecretToBytes {
param([string]$CertificateSecret)
$Base64Lines = $CertificateSecret -split "\r?\n" |
Where-Object { $_ -notmatch "^-+BEGIN " -and $_ -notmatch "^-+END " }
$CertificateBase64 = ($Base64Lines -join "") -replace "\s", ""
try {
return [Convert]::FromBase64String($CertificateBase64)
} catch {
throw "Windows code-signing certificate must be base64-encoded PFX data."
}
}
$CertificateSecret = Read-FirstEnv @("WINDOWS_CODE_SIGNING_CERTIFICATE", "WINDOWS_CERTIFICATE")
$CertificatePassword = Read-FirstEnv @("WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD", "WINDOWS_CERTIFICATE_PASSWORD")
$ConfiguredThumbprint = Read-OptionalEnv @("WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT", "WINDOWS_CERTIFICATE_THUMBPRINT") ""
$DigestAlgorithm = Read-OptionalEnv @("WINDOWS_CODE_SIGNING_DIGEST_ALGORITHM") "sha256"
$TimestampUrl = Read-OptionalEnv @("WINDOWS_CODE_SIGNING_TIMESTAMP_URL", "WINDOWS_TIMESTAMP_URL") "http://timestamp.digicert.com"
$TempRoot = Join-Path ([IO.Path]::GetTempPath()) "tolaria-windows-signing"
if (-not [string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) {
$TempRoot = Join-Path $env:RUNNER_TEMP "tolaria-windows-signing"
}
New-Item -ItemType Directory -Force -Path $TempRoot | Out-Null
$PfxPath = Join-Path $TempRoot "certificate.pfx"
[IO.File]::WriteAllBytes($PfxPath, (Convert-CertificateSecretToBytes $CertificateSecret))
$SecurePassword = ConvertTo-SecureString -String $CertificatePassword -Force -AsPlainText
$ImportedCertificates = @(Import-PfxCertificate -FilePath $PfxPath -CertStoreLocation Cert:\CurrentUser\My -Password $SecurePassword)
Remove-Item -Force -ErrorAction SilentlyContinue $PfxPath
$ImportedCertificate = $ImportedCertificates | Where-Object { $_.HasPrivateKey } | Select-Object -First 1
if ($null -eq $ImportedCertificate) {
throw "The imported Windows code-signing certificate does not include a private key."
}
if ([string]::IsNullOrWhiteSpace($ConfiguredThumbprint)) {
$CertificateThumbprint = Normalize-Thumbprint $ImportedCertificate.Thumbprint
} else {
$CertificateThumbprint = Normalize-Thumbprint $ConfiguredThumbprint
}
$StoreCertificate = Get-ChildItem Cert:\CurrentUser\My |
Where-Object { (Normalize-Thumbprint $_.Thumbprint) -eq $CertificateThumbprint } |
Select-Object -First 1
if ($null -eq $StoreCertificate) {
throw "The requested Windows code-signing certificate thumbprint was not found in Cert:\CurrentUser\My."
}
$Config = @{
bundle = @{
windows = @{
certificateThumbprint = $CertificateThumbprint
digestAlgorithm = $DigestAlgorithm
timestampUrl = $TimestampUrl
}
}
}
$ResolvedConfigPath = Resolve-Path -Path (Split-Path -Parent $ConfigPath) -ErrorAction SilentlyContinue
if ($null -eq $ResolvedConfigPath) {
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $ConfigPath) | Out-Null
}
$Config | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -Encoding utf8NoBOM
if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) {
"WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT=$CertificateThumbprint" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
}
Write-Host "Prepared Windows Authenticode signing config at $ConfigPath."

137
.github/scripts/prefetch-tauri-nsis.ps1 vendored Normal file
View File

@@ -0,0 +1,137 @@
$ErrorActionPreference = "Stop"
$nsisUrl = "https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip"
$nsisSha1 = "EF7FF767E5CBD9EDD22ADD3A32C9B8F4500BB10D"
$tauriUtilsUrl = "https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.5.3/nsis_tauri_utils.dll"
$tauriUtilsSha1 = "75197FEE3C6A814FE035788D1C34EAD39349B860"
$tauriUtilsRelativePath = "Plugins\x86-unicode\additional\nsis_tauri_utils.dll"
$nsisRequiredFiles = @(
"makensis.exe",
"Bin\makensis.exe",
"Stubs\lzma-x86-unicode",
"Stubs\lzma_solid-x86-unicode",
"Include\MUI2.nsh",
"Include\FileFunc.nsh",
"Include\x64.nsh",
"Include\nsDialogs.nsh",
"Include\WinMessages.nsh",
"Include\Win\COM.nsh",
"Include\Win\Propkey.nsh",
"Include\Win\RestartManager.nsh"
)
function Get-UpperSha1 {
param([Parameter(Mandatory = $true)][string]$Path)
return (Get-FileHash -Algorithm SHA1 -LiteralPath $Path).Hash.ToUpperInvariant()
}
function Test-FileSha1 {
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)][string]$ExpectedSha1
)
return (Test-Path -LiteralPath $Path) -and ((Get-UpperSha1 -Path $Path) -eq $ExpectedSha1)
}
function Save-VerifiedDownload {
param(
[Parameter(Mandatory = $true)][string]$Uri,
[Parameter(Mandatory = $true)][string]$OutFile,
[Parameter(Mandatory = $true)][string]$ExpectedSha1
)
$parent = Split-Path -Parent $OutFile
New-Item -ItemType Directory -Force -Path $parent | Out-Null
$tempFile = "$OutFile.download"
for ($attempt = 1; $attempt -le 5; $attempt++) {
try {
Remove-Item -Force -ErrorAction SilentlyContinue -LiteralPath $tempFile
Invoke-WebRequest -Uri $Uri -OutFile $tempFile -TimeoutSec 120
$actualSha1 = Get-UpperSha1 -Path $tempFile
if ($actualSha1 -ne $ExpectedSha1) {
throw "SHA1 mismatch for $Uri. Expected $ExpectedSha1, got $actualSha1."
}
Move-Item -Force -LiteralPath $tempFile -Destination $OutFile
return
} catch {
Remove-Item -Force -ErrorAction SilentlyContinue -LiteralPath $tempFile
if ($attempt -eq 5) {
throw
}
$delaySeconds = [Math]::Min(30, 5 * $attempt)
Write-Warning "Download attempt ${attempt} failed: $($_.Exception.Message). Retrying in ${delaySeconds}s."
Start-Sleep -Seconds $delaySeconds
}
}
}
function Find-MissingFile {
param(
[Parameter(Mandatory = $true)][string]$Root,
[Parameter(Mandatory = $true)][string[]]$RelativePaths
)
foreach ($relativePath in $RelativePaths) {
if (-not (Test-Path -LiteralPath (Join-Path $Root $relativePath))) {
return $relativePath
}
}
return $null
}
if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {
throw "LOCALAPPDATA is required to resolve Tauri's Windows tool cache."
}
$tauriToolsPath = Join-Path $env:LOCALAPPDATA "tauri"
$nsisPath = Join-Path $tauriToolsPath "NSIS"
$downloadRoot = if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) {
[System.IO.Path]::GetTempPath()
} else {
$env:RUNNER_TEMP
}
New-Item -ItemType Directory -Force -Path $tauriToolsPath | Out-Null
$missingNsisFile = Find-MissingFile -Root $nsisPath -RelativePaths $nsisRequiredFiles
if ($missingNsisFile) {
Write-Host "Tauri NSIS cache is missing $missingNsisFile; downloading NSIS 3.11."
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -LiteralPath $nsisPath
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -LiteralPath (Join-Path $tauriToolsPath "nsis-3.11")
$zipPath = Join-Path $downloadRoot "nsis-3.11.zip"
Save-VerifiedDownload -Uri $nsisUrl -OutFile $zipPath -ExpectedSha1 $nsisSha1
Expand-Archive -Force -LiteralPath $zipPath -DestinationPath $tauriToolsPath
$extractedNsisPath = Join-Path $tauriToolsPath "nsis-3.11"
if (-not (Test-Path -LiteralPath $extractedNsisPath)) {
throw "Downloaded NSIS archive did not contain the expected nsis-3.11 directory."
}
Move-Item -Force -LiteralPath $extractedNsisPath -Destination $nsisPath
} else {
Write-Host "Tauri NSIS cache already contains NSIS 3.11."
}
$tauriUtilsPath = Join-Path $nsisPath $tauriUtilsRelativePath
if (-not (Test-FileSha1 -Path $tauriUtilsPath -ExpectedSha1 $tauriUtilsSha1)) {
Write-Host "Downloading Tauri NSIS utility plugin."
Save-VerifiedDownload -Uri $tauriUtilsUrl -OutFile $tauriUtilsPath -ExpectedSha1 $tauriUtilsSha1
} else {
Write-Host "Tauri NSIS utility plugin is already cached."
}
$missingFile = Find-MissingFile -Root $nsisPath -RelativePaths ($nsisRequiredFiles + @($tauriUtilsRelativePath))
if ($missingFile) {
throw "Tauri NSIS toolchain is incomplete after prefetch; missing $missingFile."
}
Write-Host "Tauri NSIS toolchain ready at $nsisPath."

View File

@@ -10,6 +10,7 @@ Il workflow `ci.yml` esegue i seguenti check automatici:
### 2. Test Coverage
- Frontend: vitest con coverage reporting
- Upload automatico su Codecov dai report LCOV frontend + Rust
- Threshold configurabile in `vitest.config.ts`
### 3. Code Health (CodeScene)
@@ -40,6 +41,23 @@ CODESCENE_PROJECT_ID=<your-project-id>
Il PAT di CodeScene è lo stesso che usi localmente (~/.codescene/token).
Il project ID lo trovi nella dashboard CodeScene.
### Codecov Setup
- Installa/attiva il repo in Codecov una volta sola tramite GitHub App / import del repository.
- Nessun `CODECOV_TOKEN` richiesto in GitHub Actions: `ci.yml` usa OIDC (`id-token: write` + `use_oidc: true`).
- Il workflow carica `coverage/lcov.info` (Vitest) e `coverage/rust.lcov` (cargo-llvm-cov).
### Telemetry Secrets For Release Builds
Aggiungi anche questi secrets per i workflow `release.yml` e `release-stable.yml`:
```
VITE_SENTRY_DSN=<frontend sentry dsn>
SENTRY_DSN=<same dsn for rust/native crash reporting>
VITE_POSTHOG_KEY=<posthog project api key>
VITE_POSTHOG_HOST=https://eu.i.posthog.com
```
Senza questi valori, i build distribuiti possono mantenere i toggle telemetry nelle Settings ma non inizializzare davvero PostHog/Sentry.
### Coverage Thresholds
Configura in `vitest.config.ts`:
@@ -85,8 +103,11 @@ codescene delta-analysis --base-revision origin/main
## Workflow Triggers
- **Push**: su `main` e branch `experiment/*`
- **Push**: su `main`
- **Pull Request**: verso `main`
- **Manuale**: `workflow_dispatch`
Nota: l'upload a Codecov gira su push a `main` e sulle PR dello stesso repository. Le PR da fork saltano l'upload per evitare problemi di permessi OIDC.
## Status Checks

View File

@@ -3,10 +3,23 @@ name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
id-token: write
env:
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
# Keep large production frontend builds below CI runner memory limits.
NODE_OPTIONS: --max-old-space-size=4096
jobs:
test:
name: Tests & Quality Checks
frontend-quality:
name: Frontend Tests & Quality Checks
runs-on: macos-15
steps:
@@ -15,7 +28,7 @@ jobs:
fetch-depth: 0 # Full history for CodeScene
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
@@ -25,58 +38,44 @@ jobs:
node-version: '22'
cache: 'pnpm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy, llvm-tools-preview
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Keep frontend and Rust quality gates in separate macOS jobs so the
# expensive Rust target cache restore no longer blocks the frontend lane.
# ── 0. Build check (catches type errors and bundler failures) ─────────
- name: TypeScript type check
run: pnpm exec tsc --noEmit
- name: Vite build check
run: pnpm build
# TypeScript is checked explicitly above; run Vite directly here to avoid
# paying for the package build script's duplicate `tsc -b` pass.
run: pnpm exec vite build
# ── 1. Tests ──────────────────────────────────────────────────────────
- name: Run frontend tests
run: pnpm test
- name: Docs build check
run: pnpm docs:build
# ── 1. Coverage-backed tests ──────────────────────────────────────────
# The coverage command runs the canonical frontend test suite.
- name: Bundle MCP server resources (required by Tauri build)
run: node scripts/bundle-mcp-server.mjs
- name: Run Rust tests
run: cargo test --manifest-path=src-tauri/Cargo.toml
# ── 2. Coverage (enforced — fails build if thresholds not met) ────────
- name: Frontend coverage (≥70% lines/functions/branches/statements)
# ── 2. Tests + coverage (enforced — fails build if thresholds not met) ─
- name: Frontend tests + coverage (≥70% lines/functions/branches/statements)
run: pnpm test:coverage
# Thresholds configured in vite.config.ts — exits non-zero if coverage drops
- name: Rust coverage (≥85% lines)
run: |
cargo llvm-cov \
--manifest-path src-tauri/Cargo.toml \
--ignore-filename-regex 'lib\.rs|main\.rs|menu\.rs' \
--fail-under-lines 85
# cargo-llvm-cov exits non-zero if line coverage drops below 85%
# lib.rs/main.rs/menu.rs are Tauri boilerplate -- not meaningfully unit-testable.
- name: Upload frontend coverage to Codecov
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe
with:
use_oidc: true
fail_ci_if_error: true
disable_search: true
files: ./coverage/lcov.info
flags: frontend
verbose: true
# OIDC avoids long-lived CODECOV_TOKEN secrets.
# ── 3. Code Health (CodeScene — Hotspot + Average Code Health gates) ──
# Enforces minimum floors on BOTH hotspot and average code health.
@@ -138,8 +137,124 @@ jobs:
- name: Lint frontend
run: pnpm lint
rust-quality:
name: Rust Tests & Quality Checks
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
components: rustfmt, clippy, llvm-tools-preview
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@e5de28abeb52d916c5e5875d54b21a9e738b61ec
- name: Rust tests + coverage (≥85% lines)
run: |
mkdir -p coverage
cargo llvm-cov \
--manifest-path src-tauri/Cargo.toml \
--ignore-filename-regex 'lib\.rs|main\.rs|menu\.rs' \
--lcov \
--output-path coverage/rust.lcov \
--fail-under-lines 85
# cargo-llvm-cov exits non-zero if line coverage drops below 85%
# lib.rs/main.rs/menu.rs are Tauri boilerplate -- not meaningfully unit-testable.
- name: Upload Rust coverage to Codecov
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe
with:
use_oidc: true
fail_ci_if_error: true
disable_search: true
files: ./coverage/rust.lcov
flags: rust
verbose: true
# OIDC avoids long-lived CODECOV_TOKEN secrets.
- name: Clippy (Rust)
run: cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings
- name: Format check (Rust)
run: cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check
linux-build:
name: Linux build verification
# Keep the normal push CI lane under the 10-minute target. The release
# workflows already perform the full Linux/AppImage build after main
# pushes, so this slower compatibility check stays available for PRs and
# manual diagnostics without blocking every direct push.
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Tauri Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libsoup-3.0-dev \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
libfuse2 \
librsvg2-dev \
patchelf \
build-essential \
file
- name: Setup pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
components: clippy
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Frontend build
run: pnpm build
- name: Cargo check
run: cargo check --manifest-path=src-tauri/Cargo.toml
- name: Clippy
run: cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings

104
.github/workflows/deploy-docs.yml vendored Normal file
View File

@@ -0,0 +1,104 @@
name: Deploy docs
on:
push:
branches: [main]
paths:
- ".github/workflows/deploy-docs.yml"
- "package.json"
- "pnpm-lock.yaml"
- "scripts/build-agent-docs.mjs"
- "site/**"
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
env:
NODE_OPTIONS: --max-old-space-size=4096
jobs:
build:
name: Build VitePress site
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: latest
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build docs and download pages
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pnpm docs:build
DIST="site/.vitepress/dist"
mkdir -p "$DIST/alpha" "$DIST/stable" "$DIST/download" "$DIST/releases" "$DIST/stable/download"
gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > "$DIST/releases.json"
STABLE_TAG="$(gh release list --repo ${{ github.repository }} --limit 100 --json tagName,isDraft,isPrerelease --jq '[.[] | select(.isDraft == false and .isPrerelease == false)][0].tagName // ""')"
if [ -n "$STABLE_TAG" ]; then
gh release download --repo ${{ github.repository }} "$STABLE_TAG" --pattern "stable-latest.json" --output "$DIST/stable/latest.json" || echo '{}' > "$DIST/stable/latest.json"
else
echo '{}' > "$DIST/stable/latest.json"
fi
ALPHA_TAG="$(gh release list --repo ${{ github.repository }} --limit 100 --json tagName,isDraft,isPrerelease --jq '[.[] | select(.isDraft == false and .isPrerelease == true)][0].tagName // ""')"
if [ -n "$ALPHA_TAG" ]; then
gh release download --repo ${{ github.repository }} "$ALPHA_TAG" --pattern "alpha-latest.json" --output "$DIST/alpha/latest.json" || echo '{}' > "$DIST/alpha/latest.json"
else
echo '{}' > "$DIST/alpha/latest.json"
fi
bun scripts/build-release-download-page.ts --latest-json "$DIST/stable/latest.json" --releases-json "$DIST/releases.json" --output-file "$DIST/download/index.html"
bun scripts/build-release-history-page.ts --releases-json "$DIST/releases.json" --output-file "$DIST/releases/index.html"
cp "$DIST/download/index.html" "$DIST/stable/download/index.html"
cp "$DIST/alpha/latest.json" "$DIST/latest.json"
cp "$DIST/alpha/latest.json" "$DIST/latest-canary.json"
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
with:
path: site/.vitepress/dist
deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-24.04
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -0,0 +1,567 @@
name: Release build artifacts
on:
workflow_call:
inputs:
version:
required: true
type: string
macos_bundles:
required: false
type: string
default: ""
upload_macos_dmg:
required: true
type: boolean
require_windows_authenticode:
required: false
type: boolean
default: true
env:
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
# The production Vite bundle can exceed Node's default ~2GB heap on
# macOS arm64 runners while Tauri runs beforeBuildCommand.
NODE_OPTIONS: --max-old-space-size=4096
jobs:
build:
name: Build (${{ matrix.arch }})
runs-on: macos-15
strategy:
fail-fast: true
matrix:
include:
- arch: aarch64
target: aarch64-apple-darwin
- arch: x86_64
target: x86_64-apple-darwin
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Setup Bun (required for bundle-qmd.sh)
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: latest
- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
targets: ${{ matrix.target }}
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/${{ matrix.target }}/release/bundle
- name: Set version
run: |
VERSION="${{ inputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Import Apple Developer certificate into keychain
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Validate telemetry env
env:
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
python3 <<'PY'
import os
import re
import sys
from urllib.parse import urlparse
DISALLOWED_PLACEHOLDERS = {
"",
"-",
"_",
"false",
"true",
"null",
"undefined",
"none",
"disabled",
}
def normalize(name: str) -> str:
value = os.getenv(name, "").strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1].strip()
return value
def normalize_http_like(value: str) -> str:
if "://" in value:
return value
return f"https://{value}"
def normalize_hostname(hostname: str) -> str:
normalized = hostname.strip().rstrip('.').lower()
if normalized.startswith('[') and normalized.endswith(']'):
normalized = normalized[1:-1]
return normalized
def is_ip_address(hostname: str) -> bool:
if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname):
return all(0 <= int(part) <= 255 for part in hostname.split('.'))
return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None
def is_allowed_hostname(hostname: str) -> bool:
normalized = normalize_hostname(hostname)
if not normalized or normalized in DISALLOWED_PLACEHOLDERS:
return False
if normalized == 'localhost':
return True
return '.' in normalized or is_ip_address(normalized)
def is_http_url(value: str) -> bool:
parsed = urlparse(normalize_http_like(value))
return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "")
values = {
name: normalize(name)
for name in (
"VITE_SENTRY_DSN",
"SENTRY_DSN",
"VITE_POSTHOG_KEY",
"VITE_POSTHOG_HOST",
)
}
errors = []
for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"):
value = values[name]
if value.lower() in DISALLOWED_PLACEHOLDERS:
errors.append(f"{name} must be set to a real value, not a placeholder")
elif not is_http_url(value):
errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host")
if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS:
errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder")
if errors:
print("Telemetry env validation failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
raise SystemExit(1)
print("Telemetry env validation passed.")
PY
- name: Build Tauri app (with signing + notarization)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
VITE_SENTRY_RELEASE: ${{ inputs.version }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
MACOS_BUNDLES="${{ inputs.macos_bundles }}"
if [ -n "$MACOS_BUNDLES" ]; then
pnpm tauri build --target ${{ matrix.target }} --bundles "$MACOS_BUNDLES"
else
pnpm tauri build --target ${{ matrix.target }}
fi
- name: Upload .dmg
if: ${{ inputs.upload_macos_dmg }}
uses: actions/upload-artifact@v4
with:
name: dmg-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
retention-days: 1
- name: Upload updater artifacts (.tar.gz + .sig)
uses: actions/upload-artifact@v4
with:
name: updater-${{ matrix.arch }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
retention-days: 1
build-linux:
name: Build (linux-x86_64)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Tauri Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libsoup-3.0-dev \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
fcitx5-frontend-gtk3 \
libfuse2 \
librsvg2-dev \
curl \
wget \
patchelf \
build-essential \
file \
rpm
- name: Setup pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
targets: x86_64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle
- name: Set version
run: |
VERSION="${{ inputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Build Tauri app (Linux bundles)
env:
APPIMAGE_EXTRACT_AND_RUN: 1
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
VITE_SENTRY_RELEASE: ${{ inputs.version }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage
- name: Validate Linux bundles
run: |
shopt -s nullglob
appimages=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
)
installers=(
"${appimages[@]}"
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
)
signatures=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
)
if [ ${#appimages[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage bundle."
exit 1
fi
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage, deb or rpm bundle."
exit 1
fi
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Linux build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Linux bundles
uses: actions/upload-artifact@v4
with:
name: linux-x86_64-bundles
path: |
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
if-no-files-found: error
retention-days: 1
build-windows:
name: Build (windows-x86_64)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
targets: x86_64-pc-windows-msvc
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~\.cargo\registry
~\.cargo\git
src-tauri\target
key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Cache Tauri Windows tools
uses: actions/cache@v4
with:
path: ~\AppData\Local\tauri
key: ${{ runner.os }}-tauri-tools-nsis-3.11-nsis-tauri-utils-0.5.3
- name: Prefetch Tauri NSIS toolchain
shell: pwsh
run: ./.github/scripts/prefetch-tauri-nsis.ps1
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached Windows bundle artifacts
shell: pwsh
run: |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
- name: Set version
shell: pwsh
run: |
$version = "${{ inputs.version }}"
$tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json
$tauri.version = $version
$tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json"
(Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml"
- name: Validate Windows release env
id: windows-signing
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
WINDOWS_CODE_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE }}
WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
REQUIRE_WINDOWS_AUTHENTICODE: ${{ inputs.require_windows_authenticode }}
run: |
for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do
if [ -z "${!name}" ]; then
echo "::error::$name is required to build signed Windows updater artifacts."
exit 1
fi
done
has_certificate=false
has_password=false
if [ -n "$WINDOWS_CODE_SIGNING_CERTIFICATE" ] || [ -n "$WINDOWS_CERTIFICATE" ]; then
has_certificate=true
fi
if [ -n "$WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD" ] || [ -n "$WINDOWS_CERTIFICATE_PASSWORD" ]; then
has_password=true
fi
if [ "$has_certificate" != "$has_password" ]; then
echo "::error::Windows Authenticode signing is partially configured. Set both certificate and password secrets, or remove both for unsigned alpha Windows artifacts."
exit 1
fi
if [ "$has_certificate" = "true" ]; then
echo "authenticode_available=true" >> "$GITHUB_OUTPUT"
elif [ "$REQUIRE_WINDOWS_AUTHENTICODE" = "true" ]; then
echo "::error::WINDOWS_CODE_SIGNING_CERTIFICATE or WINDOWS_CERTIFICATE is required to Authenticode-sign Windows installers."
exit 1
else
echo "::warning::Windows Authenticode certificate secrets are not configured. Building alpha Windows artifacts without Authenticode signatures; Tauri updater signatures are still required."
echo "authenticode_available=false" >> "$GITHUB_OUTPUT"
fi
- name: Prepare Windows Authenticode signing
if: ${{ steps.windows-signing.outputs.authenticode_available == 'true' }}
shell: pwsh
env:
WINDOWS_CODE_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE }}
WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD }}
WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT }}
WINDOWS_CODE_SIGNING_TIMESTAMP_URL: ${{ secrets.WINDOWS_CODE_SIGNING_TIMESTAMP_URL }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
WINDOWS_TIMESTAMP_URL: ${{ secrets.WINDOWS_TIMESTAMP_URL }}
run: ./.github/scripts/configure-windows-authenticode.ps1
- name: Build Tauri app (Windows bundles)
shell: pwsh
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
VITE_SENTRY_RELEASE: ${{ inputs.version }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
if ("${{ steps.windows-signing.outputs.authenticode_available }}" -eq "true") {
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis --config src-tauri/tauri.windows-signing.conf.json
} else {
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis
}
- name: Validate Windows Authenticode signatures
if: ${{ steps.windows-signing.outputs.authenticode_available == 'true' }}
shell: pwsh
run: |
$expectedThumbprint = $env:WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT
if ([string]::IsNullOrWhiteSpace($expectedThumbprint)) {
throw "WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT was not exported by the signing setup step."
}
$expectedThumbprint = ($expectedThumbprint -replace "\s", "").ToUpperInvariant()
$paths = @()
$paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "*.exe" -File -ErrorAction SilentlyContinue
$paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis" -Filter "*.exe" -File -ErrorAction SilentlyContinue
$paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi" -Filter "*.msi" -File -ErrorAction SilentlyContinue
$paths = @($paths | Sort-Object FullName -Unique)
if ($paths.Count -eq 0) {
throw "No Windows executable or installer artifacts found to verify."
}
foreach ($path in $paths) {
$signature = Get-AuthenticodeSignature -FilePath $path.FullName
if ($signature.Status -ne "Valid") {
throw "Invalid Authenticode signature for $($path.FullName): $($signature.Status)"
}
if ($null -eq $signature.SignerCertificate) {
throw "Missing signer certificate for $($path.FullName)."
}
$actualThumbprint = ($signature.SignerCertificate.Thumbprint -replace "\s", "").ToUpperInvariant()
if ($actualThumbprint -ne $expectedThumbprint) {
throw "Unexpected signer thumbprint for $($path.FullName): $actualThumbprint"
}
Write-Host "Authenticode signature OK: $($path.FullName)"
}
- name: Validate Windows bundles
shell: bash
run: |
shopt -s nullglob
installers=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
)
signatures=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig
)
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Windows build produced no installable NSIS or MSI bundle."
exit 1
fi
for installer in "${installers[@]}"; do
if [[ "$(basename "$installer")" != *"${{ inputs.version }}"* ]]; then
echo "::error::Windows build produced an installer for a different version: $(basename "$installer")"
exit 1
fi
done
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Windows build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Windows bundles
uses: actions/upload-artifact@v4
with:
name: windows-x86_64-bundles
path: |
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig
if-no-files-found: error
retention-days: 1

View File

@@ -1,286 +0,0 @@
name: Release (Canary)
on:
push:
branches:
- canary
concurrency:
group: release-canary-${{ github.ref }}
cancel-in-progress: true
jobs:
# ─────────────────────────────────────────────────────────────
# Phase 1: Compute the canary version string
# ─────────────────────────────────────────────────────────────
version:
name: Compute version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
tag: ${{ steps.ver.outputs.tag }}
steps:
- id: ver
run: |
VERSION="0.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}-canary"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
echo "### Canary version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────
# Phase 2: Build each architecture in parallel
# ─────────────────────────────────────────────────────────────
build:
name: Build (${{ matrix.arch }})
needs: version
runs-on: macos-15
strategy:
fail-fast: true
matrix:
include:
- arch: aarch64
target: aarch64-apple-darwin
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Bun (required for bundle-qmd.sh)
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Import Apple Developer certificate into keychain
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Build Tauri app (with signing + notarization)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
pnpm tauri build --target ${{ matrix.target }}
- name: Upload .dmg
uses: actions/upload-artifact@v4
with:
name: dmg-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
retention-days: 1
- name: Upload updater artifacts (.tar.gz + .sig)
uses: actions/upload-artifact@v4
with:
name: updater-${{ matrix.arch }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
retention-days: 1
# ─────────────────────────────────────────────────────────────
# Phase 3: Publish GitHub Release (prerelease)
# ─────────────────────────────────────────────────────────────
release:
name: GitHub Release (canary)
needs: [version, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Generate release notes
run: |
PREV_TAG=$(git tag --sort=-version:refname | grep canary | head -n 1 || echo "")
if [ -z "$PREV_TAG" ]; then
NOTES=$(git log --oneline --no-merges -20)
else
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD")
fi
{
echo "## What's Changed (Canary)"
echo ""
echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
echo ""
echo "---"
echo "**Canary build — pre-release, may be unstable**"
echo ""
echo "**Requires Apple Silicon (M1/M2/M3)**"
echo ""
echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*"
} > release_notes.md
- name: Build latest-canary.json
run: |
VERSION="${{ needs.version.outputs.version }}"
TAG="${{ needs.version.outputs.tag }}"
REPO="refactoringhq/laputa-app"
ARM_SIG=$(cat updater-aarch64/*.app.tar.gz.sig)
ARM_TARBALL=$(ls updater-aarch64/*.app.tar.gz | xargs basename)
cat > latest-canary.json << EOF
{
"version": "${VERSION}",
"notes": "Canary build. See https://refactoringhq.github.io/laputa-app/ for release notes.",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"darwin-aarch64": {
"signature": "${ARM_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}"
}
}
}
EOF
echo "latest-canary.json:"; cat latest-canary.json
- name: Publish GitHub Release (prerelease)
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.version.outputs.tag }}
name: Laputa ${{ needs.version.outputs.version }} (Canary)
body_path: release_notes.md
draft: false
prerelease: true
files: |
dmg-aarch64/*.dmg
updater-aarch64/*.app.tar.gz
updater-aarch64/*.app.tar.gz.sig
latest-canary.json
# ─────────────────────────────────────────────────────────────
# Phase 4: Update GitHub Pages (preserve stable latest.json)
# ─────────────────────────────────────────────────────────────
pages:
name: Update release history page
needs: [version, release]
runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: github-pages
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- name: Build release history page
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p _site
gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json
# Download stable latest.json from existing GH Pages (preserve it)
curl -fsSL "https://refactoringhq.github.io/laputa-app/latest.json" -o _site/latest.json || echo '{}' > _site/latest.json
# Copy canary latest.json from this release
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "latest-canary.json" --output _site/latest-canary.json || true
cat > _site/index.html << 'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laputa — Release History</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #F7F6F3; color: #37352F; line-height: 1.6; padding: 2rem; max-width: 720px; margin: 0 auto; }
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.5rem; }
.subtitle { color: #787774; margin-bottom: 2rem; }
.release { background: #fff; border: 1px solid #E9E9E7; border-radius: 8px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; }
.release h2 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.25rem; }
.release .meta { font-size: 0.8125rem; color: #787774; margin-bottom: 0.75rem; }
.release .body { font-size: 0.875rem; white-space: pre-wrap; }
.release .downloads { margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap; }
.release .downloads a { display: inline-block; padding: 0.375rem 0.75rem; background: #155DFF; color: #fff; border-radius: 6px; text-decoration: none; font-size: 0.8125rem; font-weight: 500; }
.release .downloads a:hover { background: #1248CC; }
.canary { border-left: 3px solid #f59e0b; }
.empty { color: #787774; text-align: center; padding: 3rem; }
</style>
</head>
<body>
<h1>Laputa Release History</h1>
<p class="subtitle">Auto-updated on every release</p>
<div id="releases"></div>
<script>
fetch('releases.json').then(r=>r.json()).then(releases=>{
const el=document.getElementById('releases');
if(!releases.length){el.innerHTML='<p class="empty">No releases yet.</p>';return;}
releases.forEach(r=>{
const date=new Date(r.published_at).toLocaleDateString('en-US',{year:'numeric',month:'long',day:'numeric'});
const dmgs=(r.assets||[]).filter(a=>a.name.endsWith('.dmg'));
const links=dmgs.map(a=>'<a href="'+a.browser_download_url+'">'+a.name+'</a>').join('');
const body=(r.body||'').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const div=document.createElement('div');
div.className='release'+(r.prerelease?' canary':'');
div.innerHTML='<h2>'+(r.name||r.tag_name)+'</h2><div class="meta">'+date+' · '+r.tag_name+(r.prerelease?' · <strong>Canary</strong>':'')+'</div><div class="body">'+body+'</div>'+(links?'<div class="downloads">'+links+'</div>':'');
el.appendChild(div);
});
});
</script>
</body>
</html>
HTMLEOF
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_site
commit_message: "Update release history for canary ${{ needs.version.outputs.tag }}"

303
.github/workflows/release-stable.yml vendored Normal file
View File

@@ -0,0 +1,303 @@
name: Release (Stable)
on:
push:
tags:
- 'stable-v*'
- 'v20*'
concurrency:
group: release-stable-${{ github.ref }}
cancel-in-progress: true
jobs:
# ─────────────────────────────────────────────────────────────
# Phase 1: Compute the stable version string once
# ─────────────────────────────────────────────────────────────
version:
name: Compute stable version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
display_version: ${{ steps.ver.outputs.display_version }}
tag: ${{ steps.ver.outputs.tag }}
steps:
- id: ver
shell: bash
run: |
python3 <<'PY' > version.env
import os
import re
from datetime import date
tag = os.environ["GITHUB_REF_NAME"]
legacy_match = re.fullmatch(r"stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})", tag)
date_match = re.fullmatch(r"v(\d{4})-(\d{2})-(\d{2})", tag)
if date_match:
year, month, day = map(int, date_match.groups())
date(year, month, day)
version = f"{year}.{month}.{day}"
display_version = tag
elif legacy_match:
year, month, day = map(int, legacy_match.groups())
date(year, month, day)
version = f"{year}.{month}.{day}"
display_version = version
else:
raise SystemExit(f"Stable tags must use vYYYY-MM-DD or stable-vYYYY.M.D, got {tag}")
print(f"version={version}")
print(f"display_version={display_version}")
print(f"tag={tag}")
PY
cat version.env >> "$GITHUB_OUTPUT"
DISPLAY_VERSION=$(grep '^display_version=' version.env | cut -d= -f2-)
echo "### Stable version: \`$DISPLAY_VERSION\`" >> "$GITHUB_STEP_SUMMARY"
# -------------------------------------------------------------
# Phase 2: Build shared release artifacts
# -------------------------------------------------------------
build-artifacts:
name: Build release artifacts
needs: version
uses: ./.github/workflows/release-build-artifacts.yml
with:
version: ${{ needs.version.outputs.version }}
macos_bundles: ""
upload_macos_dmg: true
# One-time stable promotion exceptions while Windows certificate secrets
# are still being provisioned. Future stable tags must keep Authenticode.
require_windows_authenticode: ${{ !contains(fromJson('["v2026-06-01","v2026-06-06"]'), needs.version.outputs.tag) }}
secrets: inherit
# ─────────────────────────────────────────────────────────────
# Phase 3: Publish GitHub Release
# ─────────────────────────────────────────────────────────────
release:
name: GitHub Release (stable)
needs: [version, build-artifacts]
runs-on: ubuntu-latest
permissions:
contents: write
pages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Normalize macOS release artifact names
run: |
normalize_macos_artifacts() {
local arch="$1"
local normalized_updater="$2"
local normalized_dmg="$3"
local updater_dir="updater-${arch}"
local updater_file
updater_file=$(find "$updater_dir" -maxdepth 1 -name "*.app.tar.gz" -print -quit)
if [ -z "$updater_file" ]; then
echo "::error::Missing macOS updater artifact in ${updater_dir}" >&2
return 1
fi
local sig_file="${updater_file}.sig"
if [ ! -f "$sig_file" ]; then
echo "::error::Missing macOS updater signature for ${updater_file}" >&2
return 1
fi
local normalized_sig="${normalized_updater}.sig"
if [ "$updater_file" != "$normalized_updater" ]; then
mv "$updater_file" "$normalized_updater"
fi
if [ "$sig_file" != "$normalized_sig" ]; then
mv "$sig_file" "$normalized_sig"
fi
local dmg_dir="dmg-${arch}"
local dmg_file
dmg_file=$(find "$dmg_dir" -maxdepth 1 -name "*.dmg" -print -quit)
if [ -z "$dmg_file" ]; then
echo "::error::Missing macOS DMG artifact in ${dmg_dir}" >&2
return 1
fi
if [ "$dmg_file" != "$normalized_dmg" ]; then
mv "$dmg_file" "$normalized_dmg"
fi
}
normalize_macos_artifacts aarch64 \
"updater-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.app.tar.gz" \
"dmg-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.dmg"
normalize_macos_artifacts x86_64 \
"updater-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.app.tar.gz" \
"dmg-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.dmg"
- name: Generate release notes
run: |
NOTES_FILE="release-notes/${{ needs.version.outputs.tag }}.md"
if [ -f "$NOTES_FILE" ]; then
cat "$NOTES_FILE" > release_notes.md
else
PREV_TAG=$(git for-each-ref --sort=-creatordate --format='%(refname:short)' refs/tags/v20* refs/tags/stable-v* | grep -vx "${{ needs.version.outputs.tag }}" | head -n 1 || echo "")
if [ -z "$PREV_TAG" ]; then
NOTES=$(git log --oneline --no-merges -20)
else
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..${{ needs.version.outputs.tag }}")
fi
{
echo "## What's Changed"
echo ""
echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
} > release_notes.md
fi
{
echo ""
echo "---"
echo "**Stable release — manually promoted from \`main\`**"
echo ""
echo "**Includes macOS (Apple Silicon and Intel), Windows x64, and Linux x64 bundles**"
echo ""
echo "*Built from \`$(git rev-parse --short ${{ needs.version.outputs.tag }})\` on $(date -u +%Y-%m-%d)*"
} >> release_notes.md
- name: Build stable-latest.json
run: |
VERSION="${{ needs.version.outputs.version }}"
TAG="${{ needs.version.outputs.tag }}"
REPO="${GITHUB_REPOSITORY}"
REPO_NAME="${REPO#*/}"
PAGES_URL="https://refactoringhq.github.io/${REPO_NAME}/"
find_required() {
local patterns=("$@")
for pattern in "${patterns[@]}"; do
set -- $pattern
if [ -e "$1" ]; then
printf '%s\n' "$1"
return 0
fi
done
echo "::error::Missing required artifact matching one of: ${patterns[*]}" >&2
return 1
}
ARM_SIG_FILE=$(find_required "updater-aarch64/*.app.tar.gz.sig")
ARM_UPDATER_FILE="${ARM_SIG_FILE%.sig}"
ARM_SIG=$(cat "$ARM_SIG_FILE")
ARM_TARBALL=$(basename "$ARM_UPDATER_FILE")
ARM_DMG=$(basename "$(find_required "dmg-aarch64/*.dmg")")
INTEL_SIG_FILE=$(find_required "updater-x86_64/*.app.tar.gz.sig")
INTEL_UPDATER_FILE="${INTEL_SIG_FILE%.sig}"
INTEL_SIG=$(cat "$INTEL_SIG_FILE")
INTEL_TARBALL=$(basename "$INTEL_UPDATER_FILE")
INTEL_DMG=$(basename "$(find_required "dmg-x86_64/*.dmg")")
LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig")
LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}"
LINUX_SIG=$(cat "$LINUX_SIG_FILE")
LINUX_UPDATER=$(basename "$LINUX_UPDATER_FILE")
LINUX_DOWNLOAD=$(basename "$(find_required "linux-x86_64-bundles/*/*.AppImage" "linux-x86_64-bundles/*/*.deb" "linux-x86_64-bundles/*/*.AppImage.tar.gz" "linux-x86_64-bundles/*.AppImage" "linux-x86_64-bundles/*.deb" "linux-x86_64-bundles/*.AppImage.tar.gz")")
WINDOWS_SIG_FILE=$(find_required "windows-x86_64-bundles/*/*-setup.exe.sig" "windows-x86_64-bundles/*/*.msi.sig" "windows-x86_64-bundles/*/*.nsis.zip.sig" "windows-x86_64-bundles/*/*.msi.zip.sig" "windows-x86_64-bundles/*-setup.exe.sig" "windows-x86_64-bundles/*.msi.sig" "windows-x86_64-bundles/*.nsis.zip.sig" "windows-x86_64-bundles/*.msi.zip.sig")
WINDOWS_UPDATER_FILE="${WINDOWS_SIG_FILE%.sig}"
WINDOWS_SIG=$(cat "$WINDOWS_SIG_FILE")
WINDOWS_UPDATER=$(basename "$WINDOWS_UPDATER_FILE")
WINDOWS_DOWNLOAD=$(basename "$(find_required "windows-x86_64-bundles/*/*-setup.exe" "windows-x86_64-bundles/*/*.msi" "windows-x86_64-bundles/*/*.nsis.zip" "windows-x86_64-bundles/*/*.msi.zip" "windows-x86_64-bundles/*-setup.exe" "windows-x86_64-bundles/*.msi" "windows-x86_64-bundles/*.nsis.zip" "windows-x86_64-bundles/*.msi.zip")")
cat > stable-latest.json << EOF
{
"version": "${VERSION}",
"notes": "Stable release. See ${PAGES_URL} for full release notes.",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"darwin-aarch64": {
"signature": "${ARM_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}",
"dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_DMG}"
},
"darwin-x86_64": {
"signature": "${INTEL_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_TARBALL}",
"dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_DMG}"
},
"linux-x86_64": {
"signature": "${LINUX_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_DOWNLOAD}"
},
"windows-x86_64": {
"signature": "${WINDOWS_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_DOWNLOAD}"
}
}
}
EOF
echo "stable-latest.json:"; cat stable-latest.json
- name: Publish GitHub Release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
with:
tag_name: ${{ needs.version.outputs.tag }}
name: Tolaria ${{ needs.version.outputs.display_version }}
body_path: release_notes.md
draft: false
prerelease: false
files: |
dmg-aarch64/*.dmg
updater-aarch64/*.app.tar.gz
updater-aarch64/*.app.tar.gz.sig
dmg-x86_64/*.dmg
updater-x86_64/*.app.tar.gz
updater-x86_64/*.app.tar.gz.sig
linux-x86_64-bundles/*.deb
linux-x86_64-bundles/*.deb.sig
linux-x86_64-bundles/*.rpm
linux-x86_64-bundles/*.AppImage
linux-x86_64-bundles/*.AppImage.sig
linux-x86_64-bundles/*.AppImage.tar.gz
linux-x86_64-bundles/*.AppImage.tar.gz.sig
linux-x86_64-bundles/*/*.deb
linux-x86_64-bundles/*/*.deb.sig
linux-x86_64-bundles/*/*.rpm
linux-x86_64-bundles/*/*.AppImage
linux-x86_64-bundles/*/*.AppImage.sig
linux-x86_64-bundles/*/*.AppImage.tar.gz
linux-x86_64-bundles/*/*.AppImage.tar.gz.sig
windows-x86_64-bundles/*.exe
windows-x86_64-bundles/*.exe.sig
windows-x86_64-bundles/*.msi
windows-x86_64-bundles/*.msi.sig
windows-x86_64-bundles/*.zip
windows-x86_64-bundles/*.zip.sig
windows-x86_64-bundles/*/*.exe
windows-x86_64-bundles/*/*.exe.sig
windows-x86_64-bundles/*/*.msi
windows-x86_64-bundles/*/*.msi.sig
windows-x86_64-bundles/*/*.zip
windows-x86_64-bundles/*/*.zip.sig
stable-latest.json
# ─────────────────────────────────────────────────────────────
# Phase 4: Trigger the main-branch GitHub Pages deployment
# ─────────────────────────────────────────────────────────────
pages:
name: Update docs and release pages
needs: [version, release]
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Dispatch docs deployment from main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh workflow run deploy-docs.yml --repo ${{ github.repository }} --ref main
echo "Triggered deploy-docs.yml on main after publishing ${{ needs.version.outputs.tag }}."

View File

@@ -1,51 +1,361 @@
name: Release
name: Release (Alpha)
on:
push:
branches:
- main
paths-ignore:
- ".husky/**"
- ".github/workflows/deploy-docs.yml"
- ".github/workflows/release.yml"
- "site/**"
concurrency:
group: release-${{ github.ref }}
group: release-alpha-${{ github.ref }}
cancel-in-progress: true
jobs:
# ─────────────────────────────────────────────────────────────
# Phase 1: Compute the version string once
# Phase 1: Compute the alpha version string once
# Alpha builds use calendar semver and stay newer than the latest stable tag.
# ─────────────────────────────────────────────────────────────
version:
name: Compute version
name: Compute alpha version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
display_version: ${{ steps.ver.outputs.display_version }}
tag: ${{ steps.ver.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: ver
shell: bash
run: |
VERSION="0.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
echo "### Version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY"
python3 <<'PY' > version.env
import re
import subprocess
from datetime import datetime, timedelta, timezone
def lines(command: list[str]) -> list[str]:
output = subprocess.check_output(command, text=True).strip()
return [line for line in output.splitlines() if line]
alpha_pattern = re.compile(r"^alpha-v(\d{4}\.\d{1,2}\.\d{1,2})-alpha\.(\d+)$")
def parse_alpha_tag(tag: str) -> tuple[str, int] | None:
match = alpha_pattern.fullmatch(tag)
if not match:
return None
calendar_version, sequence = match.groups()
return calendar_version, int(sequence)
def alpha_version(calendar_version: str, sequence: int) -> str:
return f"{calendar_version}-alpha.{sequence}"
def alpha_tag(calendar_version: str, sequence: int) -> str:
return f"alpha-v{calendar_version}-alpha.{sequence:04d}"
existing_tags = [
tag for tag in lines(["git", "tag", "--points-at", "HEAD"])
if tag.startswith("alpha-v")
]
if existing_tags:
tag = existing_tags[0]
parsed = parse_alpha_tag(tag)
version = alpha_version(*parsed) if parsed is not None else tag.removeprefix("alpha-v")
else:
today = datetime.now(timezone.utc).date()
stable_date = None
stable_patterns = (
re.compile(r"^v(\d{4})-(\d{2})-(\d{2})$"),
re.compile(r"^stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})$"),
)
stable_tags = lines([
"git", "for-each-ref", "--sort=-creatordate", "--format=%(refname:short)",
"refs/tags/v20*", "refs/tags/stable-v*",
])
for stable_tag in stable_tags:
match = next((pattern.fullmatch(stable_tag) for pattern in stable_patterns if pattern.fullmatch(stable_tag)), None)
if not match:
continue
year, month, day = map(int, match.groups())
try:
stable_date = datetime(year, month, day, tzinfo=timezone.utc).date()
except ValueError:
continue
break
alpha_date = today if stable_date is None or today > stable_date else stable_date + timedelta(days=1)
calendar_version = f"{alpha_date.year}.{alpha_date.month}.{alpha_date.day}"
sequence = len(lines(["git", "tag", "--list", f"alpha-v{calendar_version}-alpha.*"])) + 1
version = alpha_version(calendar_version, sequence)
tag = alpha_tag(calendar_version, sequence)
display_match = re.fullmatch(r"(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)", version)
display_version = (
f"Alpha {int(display_match.group(1))}.{int(display_match.group(2))}.{int(display_match.group(3))}.{int(display_match.group(4))}"
if display_match
else version
)
print(f"version={version}")
print(f"display_version={display_version}")
print(f"tag={tag}")
PY
cat version.env >> "$GITHUB_OUTPUT"
VERSION=$(grep '^version=' version.env | cut -d= -f2-)
DISPLAY_VERSION=$(grep '^display_version=' version.env | cut -d= -f2-)
echo "### Alpha version: \`$DISPLAY_VERSION\` (\`$VERSION\`)" >> "$GITHUB_STEP_SUMMARY"
# -------------------------------------------------------------
# Phase 2: Build shared release artifacts
# -------------------------------------------------------------
build-artifacts:
name: Build release artifacts
needs: version
uses: ./.github/workflows/release-build-artifacts.yml
with:
version: ${{ needs.version.outputs.version }}
macos_bundles: app
upload_macos_dmg: false
require_windows_authenticode: false
secrets: inherit
# ─────────────────────────────────────────────────────────────
# Phase 2: Build each architecture in parallel
# tauri build handles signing automatically via env vars
# Phase 3: Publish GitHub Release
# No lipo/re-signing — use the per-arch artifacts directly
# ─────────────────────────────────────────────────────────────
build:
name: Build (${{ matrix.arch }})
needs: version
runs-on: macos-15
strategy:
fail-fast: true
matrix:
include:
- arch: aarch64
target: aarch64-apple-darwin
release:
name: GitHub Release (alpha)
needs: [version, build-artifacts]
runs-on: ubuntu-latest
permissions:
contents: write
pages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Normalize macOS updater artifact names
run: |
normalize_updater() {
local arch="$1"
local normalized_updater="$2"
local artifact_dir="updater-${arch}"
local updater_file
updater_file=$(find "$artifact_dir" -maxdepth 1 -name "*.app.tar.gz" -print -quit)
if [ -z "$updater_file" ]; then
echo "::error::Missing macOS updater artifact in ${artifact_dir}" >&2
return 1
fi
local sig_file="${updater_file}.sig"
if [ ! -f "$sig_file" ]; then
echo "::error::Missing macOS updater signature for ${updater_file}" >&2
return 1
fi
local normalized_sig="${normalized_updater}.sig"
if [ "$updater_file" != "$normalized_updater" ]; then
mv "$updater_file" "$normalized_updater"
fi
if [ "$sig_file" != "$normalized_sig" ]; then
mv "$sig_file" "$normalized_sig"
fi
}
normalize_updater aarch64 "updater-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.app.tar.gz"
normalize_updater x86_64 "updater-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.app.tar.gz"
- name: Generate release notes
run: |
PREV_TAG=$(python3 <<'PY'
import re
import subprocess
current_tag = '${{ needs.version.outputs.tag }}'
pattern = re.compile(r'^alpha-v(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)$')
output = subprocess.check_output(['git', 'tag', '--list', 'alpha-v*'], text=True).strip()
tags = [line for line in output.splitlines() if line and line != current_tag]
parsed_tags = []
for tag in tags:
match = pattern.fullmatch(tag)
if not match:
continue
year, month, day, sequence = map(int, match.groups())
parsed_tags.append(((year, month, day, sequence), tag))
print(max(parsed_tags)[1] if parsed_tags else '')
PY
)
if [ -z "$PREV_TAG" ]; then
NOTES=$(git log --oneline --no-merges -20)
else
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD")
fi
{
echo "## What's Changed (Alpha)"
echo ""
echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
echo ""
echo "---"
echo "**Alpha build — updated on every push to \`main\`**"
echo ""
echo "**Includes macOS (Apple Silicon and Intel), Linux x64, and Windows x64 bundles**"
echo ""
echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*"
} > release_notes.md
- name: Build alpha-latest.json
run: |
VERSION="${{ needs.version.outputs.version }}"
TAG="${{ needs.version.outputs.tag }}"
REPO="${GITHUB_REPOSITORY}"
REPO_NAME="${REPO#*/}"
PAGES_URL="https://refactoringhq.github.io/${REPO_NAME}/"
find_required() {
for pattern in "$@"; do
set -- $pattern
if [ -e "$1" ]; then
printf '%s\n' "$1"
return 0
fi
done
return 1
}
ARM_SIG_FILE=$(find_required "updater-aarch64/*.app.tar.gz.sig")
ARM_UPDATER_FILE="${ARM_SIG_FILE%.sig}"
ARM_SIG=$(cat "$ARM_SIG_FILE")
ARM_UPDATER=$(basename "$ARM_UPDATER_FILE")
INTEL_SIG_FILE=$(find_required "updater-x86_64/*.app.tar.gz.sig")
INTEL_UPDATER_FILE="${INTEL_SIG_FILE%.sig}"
INTEL_SIG=$(cat "$INTEL_SIG_FILE")
INTEL_UPDATER=$(basename "$INTEL_UPDATER_FILE")
LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig")
LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}"
LINUX_SIG=$(cat "$LINUX_SIG_FILE")
LINUX_UPDATER=$(basename "$LINUX_UPDATER_FILE")
LINUX_DOWNLOAD=$(basename "$(find_required "linux-x86_64-bundles/*/*.AppImage" "linux-x86_64-bundles/*/*.deb" "linux-x86_64-bundles/*/*.AppImage.tar.gz" "linux-x86_64-bundles/*.AppImage" "linux-x86_64-bundles/*.deb" "linux-x86_64-bundles/*.AppImage.tar.gz")")
WINDOWS_SIG_FILE=$(find_required "windows-x86_64-bundles/*/*-setup.exe.sig" "windows-x86_64-bundles/*/*.msi.sig" "windows-x86_64-bundles/*/*.nsis.zip.sig" "windows-x86_64-bundles/*/*.msi.zip.sig" "windows-x86_64-bundles/*-setup.exe.sig" "windows-x86_64-bundles/*.msi.sig" "windows-x86_64-bundles/*.nsis.zip.sig" "windows-x86_64-bundles/*.msi.zip.sig")
WINDOWS_UPDATER_FILE="${WINDOWS_SIG_FILE%.sig}"
WINDOWS_SIG=$(cat "$WINDOWS_SIG_FILE")
WINDOWS_UPDATER=$(basename "$WINDOWS_UPDATER_FILE")
WINDOWS_DOWNLOAD=$(basename "$(find_required "windows-x86_64-bundles/*/*-setup.exe" "windows-x86_64-bundles/*/*.msi" "windows-x86_64-bundles/*/*.nsis.zip" "windows-x86_64-bundles/*/*.msi.zip" "windows-x86_64-bundles/*-setup.exe" "windows-x86_64-bundles/*.msi" "windows-x86_64-bundles/*.nsis.zip" "windows-x86_64-bundles/*.msi.zip")")
cat > alpha-latest.json << EOF
{
"version": "${VERSION}",
"notes": "Alpha build. See ${PAGES_URL} for full release notes.",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"darwin-aarch64": {
"signature": "${ARM_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}"
},
"darwin-x86_64": {
"signature": "${INTEL_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_UPDATER}"
},
"linux-x86_64": {
"signature": "${LINUX_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_DOWNLOAD}"
},
"windows-x86_64": {
"signature": "${WINDOWS_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_DOWNLOAD}"
}
}
}
EOF
echo "alpha-latest.json:"; cat alpha-latest.json
- name: Publish GitHub Release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
with:
tag_name: ${{ needs.version.outputs.tag }}
name: Tolaria ${{ needs.version.outputs.display_version }}
body_path: release_notes.md
draft: false
prerelease: true
files: |
updater-aarch64/*.app.tar.gz
updater-aarch64/*.app.tar.gz.sig
updater-x86_64/*.app.tar.gz
updater-x86_64/*.app.tar.gz.sig
linux-x86_64-bundles/*.deb
linux-x86_64-bundles/*.deb.sig
linux-x86_64-bundles/*.rpm
linux-x86_64-bundles/*.AppImage
linux-x86_64-bundles/*.AppImage.sig
linux-x86_64-bundles/*.AppImage.tar.gz
linux-x86_64-bundles/*.AppImage.tar.gz.sig
linux-x86_64-bundles/*/*.deb
linux-x86_64-bundles/*/*.deb.sig
linux-x86_64-bundles/*/*.rpm
linux-x86_64-bundles/*/*.AppImage
linux-x86_64-bundles/*/*.AppImage.sig
linux-x86_64-bundles/*/*.AppImage.tar.gz
linux-x86_64-bundles/*/*.AppImage.tar.gz.sig
windows-x86_64-bundles/*.exe
windows-x86_64-bundles/*.exe.sig
windows-x86_64-bundles/*.msi
windows-x86_64-bundles/*.msi.sig
windows-x86_64-bundles/*.zip
windows-x86_64-bundles/*.zip.sig
windows-x86_64-bundles/*/*.exe
windows-x86_64-bundles/*/*.exe.sig
windows-x86_64-bundles/*/*.msi
windows-x86_64-bundles/*/*.msi.sig
windows-x86_64-bundles/*/*.zip
windows-x86_64-bundles/*/*.zip.sig
alpha-latest.json
# ─────────────────────────────────────────────────────────────
# Phase 4: Update GitHub Pages with docs, release history, and download assets
# ─────────────────────────────────────────────────────────────
pages:
name: Update docs and release pages
needs: [version, release]
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
concurrency:
group: github-pages
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
with:
version: 10
@@ -55,232 +365,47 @@ jobs:
node-version: '22'
cache: 'pnpm'
- name: Setup Bun (required for bundle-qmd.sh)
uses: oven-sh/setup-bun@v2
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: latest
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-
- name: Install frontend dependencies
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Import Apple Developer certificate into keychain
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
# Decode and import the certificate so codesign can use it in beforeBuildCommand
CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Build Tauri app (with signing + notarization)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
pnpm tauri build --target ${{ matrix.target }}
- name: Upload .dmg
uses: actions/upload-artifact@v4
with:
name: dmg-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
retention-days: 1
- name: Upload updater artifacts (.tar.gz + .sig)
uses: actions/upload-artifact@v4
with:
name: updater-${{ matrix.arch }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
retention-days: 1
# ─────────────────────────────────────────────────────────────
# Phase 3: Publish GitHub Release
# No lipo/re-signing — use the per-arch artifacts directly
# ─────────────────────────────────────────────────────────────
release:
name: GitHub Release
needs: [version, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Generate release notes
run: |
PREV_TAG=$(git tag --sort=-version:refname | head -n 1 || echo "")
if [ -z "$PREV_TAG" ]; then
NOTES=$(git log --oneline --no-merges -20)
else
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD")
fi
{
echo "## What's Changed"
echo ""
echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
echo ""
echo "---"
echo "**Requires Apple Silicon (M1/M2/M3)**"
echo ""
echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*"
} > release_notes.md
- name: Build latest.json
run: |
VERSION="${{ needs.version.outputs.version }}"
TAG="${{ needs.version.outputs.tag }}"
REPO="refactoringhq/laputa-app"
ARM_SIG=$(cat updater-aarch64/*.app.tar.gz.sig)
ARM_TARBALL=$(ls updater-aarch64/*.app.tar.gz | xargs basename)
cat > latest.json << EOF
{
"version": "${VERSION}",
"notes": "See https://refactoringhq.github.io/laputa-app/ for full release notes.",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"darwin-aarch64": {
"signature": "${ARM_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}"
}
}
}
EOF
echo "latest.json:"; cat latest.json
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.version.outputs.tag }}
name: Laputa ${{ needs.version.outputs.version }}
body_path: release_notes.md
draft: false
prerelease: false
files: |
dmg-aarch64/*.dmg
updater-aarch64/*.app.tar.gz
updater-aarch64/*.app.tar.gz.sig
latest.json
# ─────────────────────────────────────────────────────────────
# Phase 4: Update GitHub Pages with release history
# ─────────────────────────────────────────────────────────────
pages:
name: Update release history page
needs: [version, release]
runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: github-pages
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- name: Build release history page
- name: Build docs and release pages
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p _site
gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json
# Copy latest.json to GitHub Pages for auto-updater endpoint
gh release download --repo ${{ github.repository }} --pattern "latest.json" --output _site/latest.json || true
# Preserve canary latest.json from existing GH Pages
curl -fsSL "https://refactoringhq.github.io/laputa-app/latest-canary.json" -o _site/latest-canary.json || true
cat > _site/index.html << 'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laputa — Release History</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #F7F6F3; color: #37352F; line-height: 1.6; padding: 2rem; max-width: 720px; margin: 0 auto; }
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.5rem; }
.subtitle { color: #787774; margin-bottom: 2rem; }
.release { background: #fff; border: 1px solid #E9E9E7; border-radius: 8px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; }
.release h2 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.25rem; }
.release .meta { font-size: 0.8125rem; color: #787774; margin-bottom: 0.75rem; }
.release .body { font-size: 0.875rem; white-space: pre-wrap; }
.release .downloads { margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap; }
.release .downloads a { display: inline-block; padding: 0.375rem 0.75rem; background: #155DFF; color: #fff; border-radius: 6px; text-decoration: none; font-size: 0.8125rem; font-weight: 500; }
.release .downloads a:hover { background: #1248CC; }
.empty { color: #787774; text-align: center; padding: 3rem; }
</style>
</head>
<body>
<h1>Laputa Release History</h1>
<p class="subtitle">Auto-updated on every release</p>
<div id="releases"></div>
<script>
fetch('releases.json').then(r=>r.json()).then(releases=>{
const el=document.getElementById('releases');
if(!releases.length){el.innerHTML='<p class="empty">No releases yet.</p>';return;}
releases.forEach(r=>{
const date=new Date(r.published_at).toLocaleDateString('en-US',{year:'numeric',month:'long',day:'numeric'});
const dmgs=(r.assets||[]).filter(a=>a.name.endsWith('.dmg'));
const links=dmgs.map(a=>'<a href="'+a.browser_download_url+'">'+a.name+'</a>').join('');
const body=(r.body||'').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const div=document.createElement('div');
div.className='release';
div.innerHTML='<h2>'+(r.name||r.tag_name)+'</h2><div class="meta">'+date+' · '+r.tag_name+'</div><div class="body">'+body+'</div>'+(links?'<div class="downloads">'+links+'</div>':'');
el.appendChild(div);
});
});
</script>
</body>
</html>
HTMLEOF
VITEPRESS_BASE="/" pnpm docs:build
mkdir -p _site/alpha _site/stable _site/release-notes
cp -R site/.vitepress/dist/. _site/
if [ -d release-notes ]; then cp release-notes/*.md _site/release-notes/ 2>/dev/null || true; fi
gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > _site/releases.json
STABLE_TAG=$(gh release list --repo ${{ github.repository }} --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // ""')
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "alpha-latest.json" --output _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json
if [ -n "$STABLE_TAG" ]; then
gh release download --repo ${{ github.repository }} "$STABLE_TAG" --pattern "stable-latest.json" --output _site/stable/latest.json || echo '{}' > _site/stable/latest.json
else
echo '{}' > _site/stable/latest.json
fi
bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html
bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/releases/index.html
mkdir -p _site/download
cp _site/stable/download/index.html _site/download/index.html
cp _site/alpha/latest.json _site/latest.json
cp _site/alpha/latest.json _site/latest-canary.json
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
with:
path: ./_site
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_site
commit_message: "Update release history for ${{ needs.version.outputs.tag }}"
id: deployment
uses: actions/deploy-pages@v4

10
.gitignore vendored
View File

@@ -10,6 +10,9 @@ lerna-debug.log*
node_modules
dist
dist-ssr
site/.vitepress/cache/
site/.vitepress/dist/
_site/
*.local
# Editor directories and files
@@ -32,6 +35,7 @@ dist-ssr
# Demo vault and helper scripts
demo-vault/
generated-fixtures/
select_demo_notes*.py
final_selection.py
@@ -41,7 +45,7 @@ final_selection.py
src-tauri/target
# Generated mcp-server bundle (built by scripts/bundle-mcp-server.mjs)
src-tauri/resources/
src-tauri/resources/mcp-server/
# Python cache
__pycache__/
@@ -69,5 +73,9 @@ CODE-HEALTH-REPORT.md
*.key.pub
# Local environment variables (never commit)
.env
.env.local
.env.*.local
# Local Codacy CLI runtime/config generated by the MCP server
.codacy/

View File

@@ -40,8 +40,36 @@ ensure_node_tooling
echo "🔍 Pre-commit checks..."
STAGED_FILES=$(git diff --cached --name-only)
APP_CHANGED=false
SITE_CHANGED=false
for FILE in $STAGED_FILES; do
case "$FILE" in
site/*)
SITE_CHANGED=true
;;
.github/workflows/*|.husky/*|docs/*|*.md)
;;
*)
APP_CHANGED=true
;;
esac
done
if [ "$APP_CHANGED" = false ]; then
if [ "$SITE_CHANGED" = true ]; then
echo " → docs build..."
pnpm docs:build
else
echo " → app checks skipped (docs/workflow/hooks only)"
fi
echo "✅ Pre-commit passed"
exit 0
fi
# Lint + types (only if TS files staged)
STAGED_TS=$(git diff --cached --name-only | grep -E '\.(ts|tsx)$' || true)
STAGED_TS=$(echo "$STAGED_FILES" | grep -E '\.(ts|tsx)$' || true)
if [ -n "$STAGED_TS" ]; then
echo " → lint + tsc..."
pnpm lint --quiet

View File

@@ -28,6 +28,23 @@
# ─────────────────────────────────────────────────────────────────────────
set -e
ensure_cargo_tooling() {
if command -v cargo >/dev/null 2>&1; then
return 0
fi
if [ -s "$HOME/.cargo/env" ]; then
# shellcheck disable=SC1091
. "$HOME/.cargo/env"
fi
if ! command -v cargo >/dev/null 2>&1; then
echo "❌ cargo must be available before pushing"
echo " Install Rust via https://rustup.rs or ensure ~/.cargo/bin is in PATH."
exit 1
fi
}
ensure_node_tooling() {
if command -v node >/dev/null 2>&1 && command -v pnpm >/dev/null 2>&1; then
return 0
@@ -85,6 +102,7 @@ else
fi
require_main_push
ensure_node_tooling
ensure_cargo_tooling
START_TIME=$(date +%s)
@@ -95,47 +113,83 @@ echo "================================================"
# ── Detect what changed ─────────────────────────────────────────────────
PUSH_TARGET=$(git rev-parse @{push} 2>/dev/null || echo "")
RUST_CHANGED=true
APP_CHANGED=true
SITE_CHANGED=false
if [ -n "$PUSH_TARGET" ]; then
CHANGED=$(git diff --name-only "$PUSH_TARGET"..HEAD)
APP_CHANGED=false
if ! echo "$CHANGED" | grep -qE '^(src-tauri/|Cargo)'; then
RUST_CHANGED=false
fi
for FILE in $CHANGED; do
case "$FILE" in
site/*)
SITE_CHANGED=true
;;
.github/workflows/*|.husky/*|docs/*|*.md)
;;
*)
APP_CHANGED=true
;;
esac
done
fi
# ── 0. TypeScript + Vite build ──────────────────────────────────────────
if [ "$APP_CHANGED" = false ]; then
if [ "$SITE_CHANGED" = true ]; then
echo ""
echo "📚 Docs-only push detected; running docs build..."
pnpm docs:build
echo " ✅ Docs build OK"
else
echo ""
echo "⏭️ App checks skipped (docs/workflow/hooks only)"
fi
ELAPSED=$(($(date +%s) - START_TIME))
echo ""
echo "✅ Pre-push passed in ${ELAPSED}s"
exit 0
fi
# ── 0. Frontend lint ───────────────────────────────────────────────────
echo ""
echo "📦 [0/5] TypeScript + Vite build..."
pnpm exec tsc --noEmit
echo "🔎 [0/6] Frontend lint..."
pnpm lint
echo " ✅ Lint OK"
# ── 1. TypeScript + Vite build ──────────────────────────────────────────
echo ""
echo "📦 [1/6] TypeScript + Vite build..."
pnpm build
echo " ✅ Build OK"
# ── 1. Frontend coverage (≥70%) — includes all unit tests ───────────────
# ── 2. Frontend coverage (≥70%) — includes all unit tests ───────────────
echo ""
echo "📊 [1/5] Frontend tests + coverage (≥70%)..."
echo "📊 [2/6] Frontend tests + coverage (≥70%)..."
pnpm test:coverage --silent
echo " ✅ Frontend coverage OK"
# ── 2. Rust lint (clippy + fmt) — fast, run before coverage ─────────────
# ── 3. Rust lint (clippy + fmt) — fast, run before coverage ─────────────
echo ""
if [ "$RUST_CHANGED" = true ]; then
echo "🔧 [2/5] Clippy + rustfmt..."
echo "🔧 [3/6] Clippy + rustfmt..."
cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings
cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check
echo " ✅ Rust lint OK"
else
echo "⏭️ [2/5] Rust lint — skipped (no src-tauri/ changes)"
echo "⏭️ [3/6] Rust lint — skipped (no src-tauri/ changes)"
fi
# ── 3. Rust coverage (≥85% lines) ──────────────────────────────────────
# ── 4. Rust coverage (≥85% lines) ──────────────────────────────────────
echo ""
if [ "$RUST_CHANGED" = true ]; then
LLVM_COV_FLAGS="--no-clean"
if [ "${LAPUTA_FULL_COVERAGE:-0}" = "1" ]; then
LLVM_COV_FLAGS=""
echo "🦀 [3/5] Rust coverage — FULL (LAPUTA_FULL_COVERAGE=1)..."
echo "🦀 [4/6] Rust coverage — FULL (LAPUTA_FULL_COVERAGE=1)..."
else
echo "🦀 [3/5] Rust coverage (≥85%, incremental)..."
echo "🦀 [4/6] Rust coverage (≥85%, incremental)..."
fi
# Unset GIT_DIR so git tests create isolated repos without inheriting hook context
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
@@ -148,24 +202,24 @@ if [ "$RUST_CHANGED" = true ]; then
-- --test-threads=1
echo " ✅ Rust coverage OK"
else
echo "⏭️ [3/5] Rust coverage — skipped (no src-tauri/ changes)"
echo "⏭️ [4/6] Rust coverage — skipped (no src-tauri/ changes)"
fi
# ── 4. Playwright core smoke lane (if any exist) ──────────────────────
# ── 5. Playwright core smoke lane (if any exist) ──────────────────────
echo ""
SMOKE_FILES=$(find tests/smoke tests/integration -name '*.spec.ts' 2>/dev/null | head -1)
if [ -n "$SMOKE_FILES" ]; then
echo "🎭 [4/5] Playwright core smoke tests..."
echo "🎭 [5/6] Playwright core smoke tests..."
if ! pnpm playwright:smoke; then
echo " ❌ Core smoke tests FAILED"
exit 1
fi
echo " ✅ Core smoke tests OK"
else
echo "⏭️ [4/5] Playwright core smoke tests — skipped (no tests/**/*.spec.ts)"
echo "⏭️ [5/6] Playwright core smoke tests — skipped (no tests/**/*.spec.ts)"
fi
# ── 5. CodeScene code health gate (ratchet) ──────────────────────────────
# ── 6. CodeScene code health gate (ratchet) ──────────────────────────────
# Thresholds live in .codescene-thresholds and only ever go UP (ratchet).
# If remote scores improved, the hook updates the file and stops so the new
# floor is committed with normal verified hooks before the next push.
@@ -181,7 +235,7 @@ if [ -f "$THRESHOLDS_FILE" ]; then
fi
echo ""
echo "🏥 [5/5] CodeScene code health (Hotspot ≥${HOTSPOT_MIN} + Average ≥${AVERAGE_MIN})..."
echo "🏥 [6/6] CodeScene code health (Hotspot ≥${HOTSPOT_MIN} + Average ≥${AVERAGE_MIN})..."
if [ -z "$CODESCENE_PAT" ] || [ -z "$CODESCENE_PROJECT_ID" ]; then
echo " ⚠️ CODESCENE_PAT or CODESCENE_PROJECT_ID not set — skipping"
else

10
.semgrepignore Normal file
View File

@@ -0,0 +1,10 @@
tests/
e2e/
node_modules/
dist/
coverage/
test-results/
src-tauri/target/
target/
src-tauri/gen/apple/assets/mcp-server/index.js
src-tauri/gen/apple/assets/mcp-server/ws-bridge.js

185
AGENTS.md
View File

@@ -1,30 +1,88 @@
# AGENTS.md — Laputa App
# AGENTS.md — Tolaria App
> Quick links: [Project Spec](docs/PROJECT-SPEC.md) · [Architecture](docs/ARCHITECTURE.md) · [Abstractions](docs/ABSTRACTIONS.md) · [Wireframes](ui-design.pen)
## 1. Development Process
---
## 1. Task Workflow
### 1a. Pick up a task
Run `/laputa-next-task` — fetches next task (To Rework first, then Open), moves to In Progress, returns full description.
### Start working on a task
**Before writing a single line of code:** run `mcp__codescene__code_health_score` to check the current codebase health against `.codescene-thresholds`. If the score is already below the threshold, **stop and refactor first** — find the worst files with the MCP, improve them, commit, then start the task. Never start feature work on a codebase that is already below the gate.
- Read task description and all comments fully
- For To Rework: the ❌ QA failed comment tells you exactly what to fix
- Check `docs/adr/` for relevant architecture decisions before structural choices
- Add a comment: `🚀 Starting work on this task. [Brief description of approach]`
- Check `docs/ARCHITECTURE.md` and `docs/ABSTRACTIONS.md` for relevant structural information
- For UI tasks: study app visual language and components first. Prioritize reusing existing components, assets, and variables over recreating them.
- If working on a Todoist task, add a comment: `🚀 Starting work on this task. [Brief description of approach]`
### 1b. Implement
### Commits & pushes
- Work on `main` branch — **no branches, no PRs, ever**. Pre-commit and pre-push block work from any other branch.
- Push directly to `main` — no PRs, no branches. Pre-push blocks non-`main` pushes.
- Commit every 2030 min: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`
- **⛔ NEVER use --no-verify**
- For UI tasks: open `ui-design.pen` first, study visual language, design in light mode
- Pre-push hook runs full check suite (build + tests + core Playwright smoke + CodeScene)
- **A task is NOT done until `git push origin main` succeeds.** If the hook blocks: read the error, fix it (clippy, tests, CodeScene, build), commit the fix, push again. **⛔ NEVER use --no-verify**
### 1c. When done
### TDD (mandatory)
Red → Green → Refactor → Commit. One cycle per commit. For bugs: write failing regression test first, then fix. Exception: pure CSS/layout changes.
**Test quality (Kent Beck's Desiderata):** Isolated · Deterministic · Fast · Behavioral · Structure-insensitive · Specific · Predictive. Fix flaky tests first. Prefer E2E over unit tests for user flows.
### Localization (mandatory for UI copy)
All user-facing UI labels/copy must live in `src/lib/locales/en.json` and be translated into every target listed in `lara.yaml`. When adding or changing interface copy:
```bash
pnpm l10n:translate
```
Use `pnpm l10n:translate:force` only when intentionally regenerating existing translations. Commit `src/lib/locales/*.json`, `lara.yaml`/`lara.lock` changes if produced, and verify placeholders/product names stayed intact.
### Product analytics (mandatory for meaningful features)
New features should almost always emit a PostHog event so we can see whether users actually discover and use them. Skip instrumentation only for very small changes where a dedicated event would create noise. Use clear, stable event names, avoid PII or note content, and include only safe metadata that helps evaluate adoption and failures.
When adding or changing a meaningful user-facing feature, include the event name(s) in the Todoist completion comment alongside QA, docs, and code health. If intentionally not instrumenting a feature, explain why in the completion comment.
### Code health (mandatory)
Pre-commit and pre-push hooks enforce **Hotspot Code Health** and **Average Code Health** ≥ thresholds in `.codescene-thresholds`. Both gates block commit/push. Thresholds are a **ratchet** — only go up. When pre-push sees improved remote scores, it updates `.codescene-thresholds`, stages it, and stops so you can commit the new floor with normal verified hooks before pushing again. Never add `// eslint-disable`, `#[allow(...)]`, or `as any`.
**Release rule:** CodeScene is a before/after gate, not just a final score. Every task must record the starting CodeScene state before edits and the final state after edits. If touched code gets worse, refactor before committing.
**⛔ NEVER edit `.codescene-thresholds` to lower the values.** If the gate blocks you, improve the code — do not lower the bar.
**CodeScene access order:** use CodeScene MCP tools if available. If MCP is unavailable, use the installed `cs` CLI for file-level review/delta work, and use the CodeScene API (`CODESCENE_PAT` + `CODESCENE_PROJECT_ID`) for project-wide Hotspot/Average threshold checks from `.codescene-thresholds`.
**Before editing any existing code file:** capture its current file-level CodeScene score. After your edits, re-run the same file-level review and verify the score is higher. If the file already starts at `10.0`, it must remain `10.0`.
**New files:** every new **scorable code file** must reach CodeScene score `10.0` before commit. If CodeScene reports `null` / "no scorable code" for a new file, it must still have zero CodeScene findings/warnings.
**Before every commit:** run CodeScene file-level review on every touched or newly created code file and verify the rule above. **Boy Scout Rule:** every file you touch must leave with a higher score, unless it was already `10.0`, in which case it must stay `10.0`.
**If CodeScene gate blocks your push:** use `mcp__codescene__code_health_score` to find the worst file, refactor it, commit, push again. Do NOT stop or wait for laputa-refactor — that is a background loop, not a substitute for fixing your own regressions.
### Security scan with Codacy (mandatory)
Use Codacy as a security and static-analysis gate before a task is considered releasable.
- Prefer the Codacy MCP inside Codex to inspect repository/file issues for every touched code file.
- If MCP is unavailable, use the local CLI wrapper, e.g. `.codacy/cli.sh analyze <path> --format sarif`; choose the relevant tool when useful (`eslint`, `opengrep`, `trivy`, `lizard`).
- **Always fix Critical and High severity findings introduced by your change.** Do not move the task to In Review with new Critical/High Codacy issues.
- Review Medium findings. Fix them when they are real defects or security-sensitive; otherwise explain why they are acceptable in the completion comment.
- Never silence a Codacy rule just to pass the scan. Prefer small code changes that remove the finding.
### Check suite (runs on every push)
```bash
pnpm lint && npx tsc --noEmit && pnpm test && pnpm test:coverage # frontend ≥70%
cargo test && cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85
```
Coverage is a release gate, not a vanity metric:
- Frontend coverage must stay ≥70%.
- Rust line coverage must stay ≥85%.
- For bug fixes, add a regression test when practical.
- For new behavior, add targeted coverage close to the changed code; do not rely only on broad E2E coverage.
### UI and native QA
**Phase 1 — Playwright (only for core user flows):**
@@ -41,59 +99,30 @@ BASE_URL="http://localhost:5201" npx playwright test tests/smoke/<slug>.spec.ts
```bash
pnpm tauri dev &
sleep 10
bash ~/.openclaw/skills/laputa-qa/scripts/focus-app.sh laputa
bash ~/.openclaw/skills/laputa-qa/scripts/screenshot.sh /tmp/qa-native.png
bash ~/.openclaw/skills/tolaria-qa/scripts/focus-app.sh laputa
bash ~/.openclaw/skills/tolaria-qa/scripts/screenshot.sh /tmp/qa-native.png
```
Use `osascript` for keyboard interactions. Write result as Todoist comment (✅ or ❌). **⚠️ WKWebView:** `osascript keystroke` blocked inside editor — rely on Playwright for text input features.
Use computer-use/browser-control style interaction for native UI QA when available: click, hover, drag, select, scroll, and type the way a real user would with the mouse and trackpad. For every UI feature, test the primary mouse-driven path first, then verify any relevant keyboard shortcut or keyboard-first workflow still works. Tolaria is still a keyboard-first app, but QA must not assume users only interact by keyboard.
After both phases pass, add a **completion comment** to the Todoist task before running `/laputa-done`. The comment must include:
- What was implemented (12 lines)
- QA: what was tested and how (Playwright / native screenshot / osascript)
- Refactoring: any files refactored to meet the CodeScene gate (or "none needed")
- ADRs: any new/updated ADRs (or "none")
- Docs: any updated docs (ARCHITECTURE.md, ABSTRACTIONS.md, etc.) (or "none")
- Code health: final Hotspot and Average scores after push
Use `osascript` for app focus, keyboard shortcuts, and keyboard-specific checks. **⚠️ WKWebView:** `osascript keystroke` can be blocked inside editor content — use computer use for native editor interaction when possible, and rely on Playwright for deterministic text-input coverage. Write result as Todoist comment (✅ or ❌).
Then run `/laputa-done <task_id>` → moves to In Review, notifies Brian, self-dispatches next task.
### Release-readiness checklist
---
Before pushing or moving a task to In Review, verify the release gates and add a **completion comment** to the Todoist task. The comment must include:
## 2. Development Process
### Commits & pushes
- Push directly to `main` — no PRs, no branches. Pre-push blocks non-`main` pushes.
- Pre-push hook runs full check suite (build + tests + core Playwright smoke + CodeScene)
- **A task is NOT done until `git push origin main` succeeds.** If the hook blocks: read the error, fix it (clippy, tests, CodeScene, build), commit the fix, push again. **⛔ NEVER use --no-verify**
### TDD (mandatory)
Red → Green → Refactor → Commit. One cycle per commit. For bugs: write failing regression test first, then fix. Exception: pure CSS/layout changes.
**Test quality (Kent Beck's Desiderata):** Isolated · Deterministic · Fast · Behavioral · Structure-insensitive · Specific · Predictive. Fix flaky tests first. Prefer E2E over unit tests for user flows.
### Code health (mandatory)
Pre-commit and pre-push hooks enforce **Hotspot Code Health** and **Average Code Health** ≥ thresholds in `.codescene-thresholds`. Both gates block commit/push. Thresholds are a **ratchet** — only go up. When pre-push sees improved remote scores, it updates `.codescene-thresholds`, stages it, and stops so you can commit the new floor with normal verified hooks before pushing again. Never add `// eslint-disable`, `#[allow(...)]`, or `as any`.
**⛔ NEVER edit `.codescene-thresholds` to lower the values.** If the gate blocks you, improve the code — do not lower the bar.
**CodeScene access order:** use CodeScene MCP tools if available. If MCP is unavailable, use the installed `cs` CLI for file-level review/delta work, and use the CodeScene API (`CODESCENE_PAT` + `CODESCENE_PROJECT_ID`) for project-wide Hotspot/Average threshold checks from `.codescene-thresholds`.
**Before editing any existing code file:** capture its current file-level CodeScene score. After your edits, re-run the same file-level review and verify the score is higher. If the file already starts at `10.0`, it must remain `10.0`.
**New files:** every new **scorable code file** must reach CodeScene score `10.0` before commit. If CodeScene reports `null` / "no scorable code" for a new file, it must still have zero CodeScene findings/warnings.
**Before every commit:** run CodeScene file-level review on every touched or newly created code file and verify the rule above. **Boy Scout Rule:** every file you touch must leave with a higher score, unless it was already `10.0`, in which case it must stay `10.0`.
**If CodeScene gate blocks your push:** use `mcp__codescene__code_health_score` to find the worst file, refactor it, commit, push again. Do NOT stop or wait for laputa-refactor — that is a background loop, not a substitute for fixing your own regressions.
### Check suite (runs on every push)
```bash
pnpm lint && npx tsc --noEmit && pnpm test && pnpm test:coverage # frontend ≥70%
cargo test && cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85
```
- What was implemented (a few lines covering logic and UX/UI).
- QA: what was tested and how (Playwright / native screenshot / osascript).
- Tests/coverage: commands run and final coverage result.
- CodeScene: before/after touched-file checks plus final Hotspot and Average scores after push; final scores must pass `.codescene-thresholds`.
- Coverage commands passed (`pnpm test:coverage` and `cargo llvm-cov ... --fail-under-lines 85`) or the change is docs-only.
- Codacy: MCP/CLI scan summary; confirm no new Critical/High findings.
- Localization: any user-facing copy lives in `src/lib/locales/en.json`, `pnpm l10n:translate` was run, and `pnpm l10n:validate` passes. If no copy changed, say “Localization: no UI copy changes”.
- PostHog: meaningful new user actions/events are instrumented with safe metadata; noisy/minor changes explicitly say “PostHog: no event needed because …”.
- Refactoring: any files refactored to meet the CodeScene gate, or "none needed".
- ADRs: any new/updated ADRs, or "none".
- Docs: any updated docs (`ARCHITECTURE.md`, `ABSTRACTIONS.md`, etc.), or "none".
- Demo vault dirt checked: `git status --short -- demo-vault demo-vault-v2` is empty unless fixture changes are intentional.
### ADRs & docs
@@ -103,15 +132,24 @@ After any Tauri command, new component/hook, data model change, or new integrati
---
## 3. Product Rules
## 2. Product Rules
### Demo vault hygiene (`demo-vault/`, `demo-vault-v2/`)
Default to `demo-vault-v2/` for testing.
- Treat `demo-vault/` and `demo-vault-v2/` as disposable QA fixtures unless the task explicitly changes demo content.
- If you create untracked notes, attachments, or other temporary files there for testing, delete them before the task is complete.
- If you modify tracked demo-vault files only to test or QA behavior, revert those edits before the final commit.
- Before declaring a task done, make sure `git status --short -- demo-vault demo-vault-v2` is empty unless demo fixture changes are part of the task.
- If a fresh run starts and the only local dirt is inside `demo-vault/` or `demo-vault-v2/`, clean those paths first and continue. That case is recoverable QA residue, not a blocker.
### User vault (`~/Laputa/`)
Default to `demo-vault-v2/`. If you must use `~/Laputa/` for testing: **never commit changes** — always run `cd ~/Laputa && git checkout -- . && git clean -fd` when done.
### UI design
Open `ui-design.pen` first (light mode). Create `design/<slug>.pen` for the task; on completion merge into `ui-design.pen` and delete it.
Default to `demo-vault-v2/`. If you must use `~/Laputa/` for testing:
- **Never commit or push** any test notes to the remote vault
- **Delete all test notes from disk** when done — do not leave untitled or temporary notes on the filesystem. Run `cd ~/Laputa && git checkout -- . && git clean -fd` to restore the vault to its last committed state.
- **Rationale:** test notes pollute the local vault over time, making it a collection of nonsensical untitled files. The vault must stay clean on disk, not just on the remote.
### UI components — mandatory rules
@@ -130,11 +168,11 @@ Open `ui-design.pen` first (light mode). Create `design/<slug>.pen` for the task
| Toggle/switch | `Switch` or `ToggleGroup` from shadcn/ui |
| Dialog/modal | `Dialog` from shadcn/ui |
**When in doubt:** search `src/components/` for an existing component before building new. **Visual language:** all new UI must feel native to Laputa — if it looks like a browser default, it's wrong.
**When in doubt:** search `src/components/` for an existing component before building new. **Visual language:** all new UI must feel native to Tolaria — if it looks like a browser default, it's wrong.
---
## 4. Reference
## 3. Reference
### macOS / Tauri gotchas
@@ -142,12 +180,13 @@ Open `ui-design.pen` first (light mode). Create `design/<slug>.pen` for the task
- Tauri menu accelerators: `MenuItemBuilder::new(label).accelerator("CmdOrCtrl+1")`
- `app.set_menu()` replaces the ENTIRE menu bar — include all submenus
- `mock-tauri.ts` silently swallows Tauri calls — not a substitute for native testing
### QA scripts
```bash
bash ~/.openclaw/skills/laputa-qa/scripts/focus-app.sh laputa
bash ~/.openclaw/skills/laputa-qa/scripts/screenshot.sh /tmp/out.png
bash ~/.openclaw/skills/laputa-qa/scripts/shortcut.sh "command" "s"
bash ~/.openclaw/skills/tolaria-qa/scripts/focus-app.sh Tolaria
bash ~/.openclaw/skills/tolaria-qa/scripts/screenshot.sh /tmp/out.png
bash ~/.openclaw/skills/tolaria-qa/scripts/shortcut.sh "command" "s"
```
### Diagrams

View File

@@ -1,3 +1,8 @@
---
type: Note
_organized: true
---
@AGENTS.md
This file is a Claude Code compatibility shim. Keep shared agent instructions in `AGENTS.md`.
This file is only a Claude Code compatibility shim. Keep shared agent instructions in `AGENTS.md`.

49
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,49 @@
# Contributing to Tolaria
Thanks for being here! Tolaria is still early, and every bug report, idea, and contribution genuinely helps shape the app.
## 🗳️ Where to share what
To keep things clean:
- 🐛 Bugs → GitHub Issues
- 💡 Feature requests / ideas → Canny • <https://tolaria.canny.io/>
If you have a feature idea, please check Canny first and upvote it if it already exists.
## 📥 Pull requests are welcome
PRs are very welcome.
A few things to keep in mind before opening one:
- Bug fixes are always great
- Small improvements are great too
- For bigger features, please check Canny first before building
- Try to avoid things that are already marked **in progress**
- Requests marked **planned** are usually great contribution targets
- Keep PRs small, focused, and easy to review
- Include a short explanation of the problem and your solution
- Follow the dev process described in Tolarias `AGENTS.md` (tests, code health, etc.)
- Avoid bundling unrelated refactors into the same PR
If you want to contribute a feature, the best place to start is here: <https://tolaria.canny.io/>
## 📋 What makes a good bug report
If you open a bug report on GitHub, it really helps to include:
- your Tolaria version
- your OS version
- steps to reproduce
- what you expected to happen
- what actually happened
- screenshots or screen recordings if useful
The clearer the report, the easier it is for us to reproduce and fix it.
## 🙏 Thank you
Tolaria is getting better because people care enough to try it, report whats broken, suggest whats missing, and contribute improvements.
That means a lot. Thanks for helping build it.

8
GEMINI.md Normal file
View File

@@ -0,0 +1,8 @@
---
type: Note
_organized: true
---
@AGENTS.md
This file is only a Gemini CLI compatibility shim. Keep shared agent instructions in `AGENTS.md`.

Binary file not shown.

661
LICENSE Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

157
README.md
View File

@@ -1,93 +1,114 @@
# Laputa App
![Latest stable](https://img.shields.io/github/v/release/refactoringhq/tolaria?display_name=tag) [![CI](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml) [![Codecov](https://codecov.io/gh/refactoringhq/tolaria/graph/badge.svg?branch=main)](https://codecov.io/gh/refactoringhq/tolaria) [![CodeScene Hotspot Code Health](https://codescene.io/projects/76865/status-badges/hotspot-code-health)](https://codescene.io/projects/76865)
Personal knowledge and life management desktop app built with Tauri v2 + React + TypeScript + BlockNote.
# 💧 Tolaria
## Documentation
Tolaria is a desktop app for macOS, Windows, and Linux for managing **markdown knowledge bases**. People use it for a variety of use cases:
- 📐 [ARCHITECTURE.md](docs/ARCHITECTURE.md) — System design, tech stack, data flow
- 🧩 [ABSTRACTIONS.md](docs/ABSTRACTIONS.md) — Core abstractions and models
- 🚀 [GETTING-STARTED.md](docs/GETTING-STARTED.md) — How to navigate the codebase
- 🎨 [THEMING.md](docs/THEMING.md) — Theme system and customization
* Operate second brains and personal knowledge
* Organize company docs as context for AI
* Store OpenClaw/assistants memory and procedures
## Quick Start
Personally, I use it to **run my life** (hey 👋 [Luca here](http://x.com/lucaronin)). I have a massive workspace of 10,000+ notes, which are the result of my [Refactoring](https://refactoring.fm/) work + a ton of personal journaling and *second braining*.
<img width="1000" height="656" alt="1776506856823-CleanShot_2026-04-18_at_12 06 57_2x" src="https://github.com/user-attachments/assets/8aeafb0a-b236-43c2-a083-ec111f903c38" />
## Walkthroughs
You can find some Loom walkthroughs below — they are short and to the point:
- [How I Organize My Own Tolaria Workspace](https://www.loom.com/share/bb3aaffa238b4be0bd62e4464bca2528)
- [My Inbox Workflow](https://www.loom.com/share/dffda263317b4fa8b47b59cdf9330571)
- [How I Save Web Resources to Tolaria](https://www.loom.com/share/8a3c1776f801402ebbf4d7b0f31e9882)
## Principles
- 📑 **Files-first** — Your notes are plain markdown files. They're portable, work with any editor, and require no export step. Your data belongs to you, not to any app.
- 🔌 **Git-first** — Every vault is a git repository. You get full version history, the ability to use any git remote, and zero dependency on Tolaria servers.
- 🛜 **Offline-first, zero lock-in** — No accounts, no subscriptions, no cloud dependencies. Your vault works completely offline and always will. If you stop using Tolaria, you lose nothing.
- 🔬 **Open source** — Tolaria is free and open source. I built this for [myself](https://x.com/lucaronin) and for sharing it with others.
- 📋 **Standards-based** — Notes are markdown files with YAML frontmatter. No proprietary formats, no locked-in data. Everything works with standard tools if you decide to move away from Tolaria.
- 🔍 **Types as lenses, not schemas** — Types in Tolaria are navigation aids, not enforcement mechanisms. There's no required fields, no validation, just helpful categories for finding notes.
- 🪄**AI-first but not AI-only** — A vault of files works very well with AI agents, but you are free to use whatever you want. We support Claude Code, Codex CLI, and Gemini CLI setup paths, but you can edit the vault with any AI you want. We provide an AGENTS file for your agents to figure out.
- ⌨️ **Keyboard-first** — Tolaria is designed for power-users who want to use keyboard as much as possible. A lot of how we designed the Editor and the Command Palette is based on this.
- 💪 **Built from real use** — Tolaria was created for manage my personal vault of 10,000+ notes, and I use it every day. Every feature exists because it solved a real problem.
## Installation
### Homebrew
Install via Homebrew on macOS:
```batch
brew install --cask tolaria
```
### Download from releases
Download the [latest release here](https://refactoringhq.github.io/tolaria/download/) for macOS, Windows, or Linux. Windows installers are Authenticode-signed; company-managed devices may still require IT approval of the Tolaria publisher before first install.
## Getting started
When you open Tolaria for the first time you get the chance of cloning the [getting started vault](https://github.com/refactoringhq/tolaria-getting-started) — which gives you a walkthrough of the whole app.
The public user docs live in [`site/`](site/) and are published to GitHub Pages. Start with [Install Tolaria](site/start/install.md), then [First Launch](site/start/first-launch.md).
## Open source and local setup
Tolaria is open source and built with Tauri, React, and TypeScript. If you want to run or contribute to the app locally, here is [how to get started](https://github.com/refactoringhq/tolaria/blob/main/docs/GETTING-STARTED.md). You can also find the gist below 👇
### Prerequisites
- Node.js 20+
- pnpm 8+
- Rust (latest stable)
- macOS (for development)
- Rust stable
- macOS or Linux for development
### Setup
#### Linux system dependencies
Tauri 2 on Linux requires WebKit2GTK 4.1 and GTK 3:
- Arch / Manjaro:
```bash
sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl \
appmenu-gtk-module libappindicator-gtk3 librsvg
```
- Debian / Ubuntu (22.04+):
```bash
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev \
libsoup-3.0-dev patchelf
```
- Fedora 38+:
```bash
sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file \
libappindicator-gtk3-devel librsvg2-devel
```
The bundled MCP server still spawns the system `node` binary at runtime on Linux, so install Node from your distro package manager if you want the external AI tooling flow.
### Quick start
```bash
# Install dependencies
pnpm install
# Run dev server
pnpm dev
```
# Open in browser (mock mode)
open http://localhost:5173
Open `http://localhost:5173` for the browser-based mock mode, or run the native desktop app with:
# Or run in Tauri
```bash
pnpm tauri dev
```
### Testing
## Tech Docs
```bash
# Frontend tests
pnpm test
- 📐 [ARCHITECTURE.md](docs/ARCHITECTURE.md) — System design, tech stack, data flow
- 🧩 [ABSTRACTIONS.md](docs/ABSTRACTIONS.md) — Core abstractions and models
- 🚀 [GETTING-STARTED.md](docs/GETTING-STARTED.md) — How to navigate the codebase
- 📚 [ADRs](docs/adr) — Architecture Decision Records
# Backend tests
cargo test
## Security
# Coverage
pnpm test:coverage
# E2E tests
pnpm test:e2e
```
### Code Quality
```bash
# Lint
pnpm lint
# Rust checks
cargo clippy
cargo fmt --check
# CodeScene (via Claude Code)
claude 'Check code health with CodeScene MCP'
```
## Development Workflow
See [AGENTS.md](AGENTS.md) for coding guidelines and workflow. [CLAUDE.md](CLAUDE.md) remains as a compatibility shim for Claude Code.
**Key principles:**
- Small, atomic commits
- Test as you go
- Visual verification mandatory
- Documentation updated with code changes
## CI/CD
GitHub Actions runs on every push to `main`:
- ✅ Tests (frontend + Rust)
- 📊 Coverage (70% threshold)
- 🎨 Lint & format
- ⚠️ Documentation check
See [.github/SETUP.md](.github/SETUP.md) for CI/CD configuration.
## Git Hooks
Husky installs the git hooks from `.husky/` during `pnpm install`. The repo enforces `main`-only commits and pushes; see [.github/HOOKS.md](.github/HOOKS.md) for details.
If you believe you have found a security issue, please report it privately as described in [SECURITY.md](./SECURITY.md).
## License
Private repository — not licensed for public use.
Tolaria is licensed under AGPL-3.0-or-later. The Tolaria name and logo remain covered by the projects trademark policy.

55
SECURITY.md Normal file
View File

@@ -0,0 +1,55 @@
# Security Policy
Thanks for helping keep Tolaria safe.
If you believe you have found a security vulnerability, **please do not open a public GitHub issue**. Report it privately instead.
## Supported versions
We currently support security fixes for:
| Version | Supported |
| --- | --- |
| Latest stable release | ✅ |
| `main` branch | Best effort |
| Older releases / prereleases | ❌ |
## Reporting a vulnerability
Please use GitHub's private vulnerability reporting flow for this repository.
Include as much of the following as you can:
- a short description of the issue
- reproduction steps or a proof of concept
- affected version / commit, if known
- impact assessment
- any suggested mitigation
If the issue involves sensitive user data, credentials, or a working exploit, keep the report private and do not post details publicly.
## What to expect
We will try to:
- acknowledge receipt within a few business days
- reproduce and assess the report
- work on a fix or mitigation if the issue is valid
- coordinate public disclosure after users have had a reasonable chance to update
## Disclosure guidelines
Please give us a reasonable amount of time to investigate and ship a fix before publishing details.
We appreciate responsible disclosure and good-faith research.
## Out of scope
The following are generally out of scope unless they demonstrate a real security impact:
- missing best-practice headers or hardening with no practical exploit
- self-XSS or editor behavior that requires unrealistic user actions
- reports that only affect unsupported old builds
- purely theoretical issues with no plausible attack path
If you are unsure whether something qualifies, please still report it privately.

38
biome.json Normal file
View File

@@ -0,0 +1,38 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"files": {
"includes": [
"**",
"!src-tauri/gen/**",
"!target/**",
"!dist/**",
"!node_modules/**"
]
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"overrides": [
{
"includes": ["site/**/*.vue"],
"linter": {
"rules": {
"correctness": {
"noUnusedImports": "off",
"noUnusedVariables": "off",
"useHookAtTopLevel": "off"
}
}
}
}
],
"linter": {
"rules": {
"correctness": {
"useQwikValidLexicalScope": "off"
}
}
}
}

View File

@@ -17,5 +17,5 @@
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
"iconLibrary": "phosphor"
}

View File

@@ -0,0 +1,61 @@
{
"name": "Tolaria QA fixture",
"purpose": "Curated local vault for native QA and developer flows. This is not the public Getting Started starter vault.",
"large_fixture": {
"generator": "python3 scripts/generate_demo_vault.py",
"default_output": "generated-fixtures/demo-vault-large"
},
"scenarios": [
{
"id": "exact-match-search",
"reason": "Quick Open should rank the exact title 'Writing' above prefix matches.",
"files": [
"topic-writing.md",
"writing-for-clarity-vs-writing-for-credit.md",
"writing-weekly-rhythm.md"
]
},
{
"id": "relationship-rendering",
"reason": "Relationship keys should render in the inspector instead of as plain properties.",
"files": [
"responsibility-sponsorships.md",
"measure-sponsorship-mrr.md",
"measure-close-rate.md",
"procedure-quarterly-sponsor-outreach.md",
"procedure-sponsor-onboarding.md",
"24q4-laputa-start.md",
"24q4.md",
"person-luca-rossi.md"
]
},
{
"id": "project-navigation",
"reason": "Projects, quarters, and a saved view give keyboard QA a compact but representative browsing path.",
"files": [
"24q4.md",
"25q1.md",
"25q2.md",
"24q4-laputa-start.md",
"25q1-laputa-v1.md",
"25q2-laputa-v2.md",
"views/active-projects.yml"
]
},
{
"id": "attachment-rendering",
"reason": "A note with a real binary attachment keeps image/block QA anchored to the fixture.",
"files": [
"laputa-qa-reference.md",
"attachments/laputa-reference.png"
]
},
{
"id": "rtl-mixed-direction",
"reason": "Arabic and mixed English/Arabic paragraphs keep rich editor and raw editor BiDi QA anchored to the fixture.",
"files": [
"rtl-mixed-direction-qa.md"
]
}
]
}

View File

@@ -1,16 +1,5 @@
# Laputa app files (machine-specific, never commit)
.laputa/settings.json
# macOS
.git/
.laputa/
.laputa-index.json
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Editors
.vscode/
.idea/
*.swp
*.swo

View File

@@ -1,3 +0,0 @@
{
"theme": null
}

View File

@@ -1,29 +0,0 @@
---
aliases: ["January 2024"]
Is A: Month
Created at: "2024-01-28"
Belongs to: "[[24q1]]"
Rating: "😄"
---
# January 2024
## Focus
- Set up [[24q1-launch-sponsorship-packages]] — research pricing, structure tiers, draft sponsor-facing materials
- Begin planning [[24q1-podcast-season-1]] — outline first 6 episode topics, research recording setup
- Map out cycling calendar for the year ([[24q1-plan-cycling-season]])
## Highlights
- Finalized the three-tier sponsorship model (Gold, Silver, Bronze) with [[person-matteo-cellini]] — spent two full days on pricing research and competitive analysis
- Ordered podcast equipment: Shure SM7B, Focusrite Scarlett, pop filter — the home studio is starting to take shape
- Newsletter hit 35.4k subscribers, steady organic growth from December's end-of-year content
- Read "The Hard Thing About Hard Things" and "Measure What Matters" — both immediately applicable to how I think about Refactoring's goals
- Registered for Nove Colli (May) and submitted lottery application for Maratona dles Dolomites (July)
- Started base training: 380 km this month, mostly zone 2 rides on the trainer
## Reflections
January always feels like a slow month, but this one was quietly productive. The sponsorship package work was the kind of unglamorous, strategic task that doesn't produce visible output but sets up everything that follows. [[person-matteo-cellini]] and I went back and forth on pricing for days — we kept second-guessing whether Gold at EUR 2.8k/issue was too high. In the end, we decided to launch at that price and see what happens. Worst case, we adjust.
The podcast planning was fun but nerve-wracking. I've been talking about launching a podcast for over a year, and now it's actually happening. The equipment is here, the topics are outlined. No more excuses. February is recording month.

View File

@@ -1,29 +0,0 @@
---
aliases: ["February 2024"]
Is A: Month
Created at: "2024-02-28"
Belongs to: "[[24q1]]"
Rating: "🤩"
---
# February 2024
## Focus
- Record and edit first batch of podcast episodes ([[24q1-podcast-season-1]])
- Launch [[24q1-launch-sponsorship-packages]] to existing sponsors and new prospects
- Start [[24q1-redesign-newsletter-template]] with improved mobile layout
## Highlights
- Recorded episodes 1-4 of [[24q1-podcast-season-1]] in a single weekend marathon session — exhausting but exhilarating
- Published Episode 1 ("Why Engineering Managers Fail") — 600 downloads in the first 48 hours, way above my modest expectations
- [[24q1-launch-sponsorship-packages]] went live — sent the new pricing deck to 12 existing sponsors and 20 cold prospects
- [[person-matteo-cellini]] closed the first Gold sponsor deal within a week of launch — a DevOps platform company, EUR 2.8k/issue for 4 issues
- Newsletter grew to 36.1k subscribers — the podcast announcement email had a 52% open rate
- First draft of [[24q1-redesign-newsletter-template]] ready — cleaner header, better CTA placement, responsive images
## Reflections
This was the month everything started to feel real. Hearing my own voice on the podcast for the first time was deeply uncomfortable — I hated it, edited obsessively, almost scrapped the whole thing. But then the downloads came in, and the feedback was overwhelmingly positive. People said things like "it feels like you're explaining this to me over coffee." That's exactly the vibe I was going for. Maybe my voice isn't so bad after all.
The sponsorship launch going well this quickly was a confidence boost. [[person-matteo-cellini]] deserves the credit — he positioned the packages perfectly and his follow-up game is relentless. Having a second Gold sponsor in the pipeline already makes the Q2 revenue forecast look strong. February was a month where preparation met opportunity. Best month in a while.

View File

@@ -1,29 +0,0 @@
---
aliases: ["March 2024"]
Is A: Month
Created at: "2024-03-28"
Belongs to: "[[24q1]]"
Rating: "😄"
---
# March 2024
## Focus
- Finalize [[24q1-redesign-newsletter-template]] and ship to production
- Continue podcast momentum with episodes 3-4 release
- Set up [[24q1-set-investing-framework]] — automate monthly contributions
## Highlights
- Shipped [[24q1-redesign-newsletter-template]] — [[measure-open-rate]] improved by 1.5pp in the first two weeks, click-through rate also up
- Released episodes 3 and 4 of [[24q1-podcast-season-1]] — Episode 3 on "Technical Debt as a Strategic Tool" got shared widely on LinkedIn
- Set up [[24q1-set-investing-framework]]: automated monthly DCA into VWCE (global ETF), topped up emergency fund to 6 months of expenses
- Newsletter crossed 37k subscribers — strong organic growth from podcast cross-promotion
- Got accepted for Maratona dles Dolomites via lottery — both gran fondos now confirmed for [[2024-complete-two-gran-fondos]]
- Cycling: 420 km this month, first outdoor rides of the season — legs feel good after months on the trainer
## Reflections
March wrapped up Q1 nicely. The newsletter template redesign was one of those things that seems small but makes a real difference — the new layout just looks more professional, and the numbers back it up. Sometimes the boring improvements are the most impactful.
Getting into Maratona via lottery was a huge relief. I'd been anxious about it for weeks. Now both races are locked in and the training plan has clear milestones. The investing framework is the kind of "set it and forget it" system I love — one less thing to think about each month. [[24q1]] was a strong quarter overall. Time to execute in Q2.

View File

@@ -1,30 +0,0 @@
---
aliases: ["April 2024"]
Is A: Month
Created at: "2024-04-28"
Belongs to: "[[24q2]]"
Rating: "😄"
---
# April 2024
## Focus
- Kick off [[24q2-hire-editor]] search — post the role, screen candidates, run test edits
- Start [[24q2-10-pillar-articles]] strategy — identify topics, outline first 3 articles
- Begin building [[24q2-sponsor-crm]] in Notion
## Highlights
- Posted the freelance editor role on LinkedIn and two newsletter job boards — received 45 applications in two weeks
- Shortlisted 5 editor candidates and ran paid test edits on a sample newsletter issue — two stood out clearly
- Outlined the first 3 [[24q2-10-pillar-articles]]: "Staff Engineer vs. Manager," "How to Prioritize Technical Debt," and "The 1:1 Framework That Actually Works"
- Started building [[24q2-sponsor-crm]] in Notion — pipeline view, deal stages, revenue tracking
- [[person-paco-furiani]] took over invoicing — freed up ~3 hours/month of admin work
- Newsletter at 38.5k subscribers — steady growth, no spikes
- Cycling: ramping up volume — 480 km this month, including a 120 km long ride in Tuscany
## Reflections
A transitional month. Not flashy, but the groundwork being laid is important. The editor hire is going to be transformative if I get it right — spending Sunday evenings editing is my least favorite part of the workflow. The test edits revealed that most applicants can fix grammar but few can preserve voice. The two finalists both "got" the Refactoring tone, which is rare.
The pillar articles strategy feels right. Instead of grinding out a new topic every week, investing in fewer, deeper pieces that can rank on search and compound over time. It's a bet on long-term value over short-term output. [[person-paco-furiani]] quietly picking up the invoicing was a small moment that mattered more than it looked — the team is starting to function as a team, not just a collection of helpers.

View File

@@ -1,30 +0,0 @@
---
aliases: ["May 2024"]
Is A: Month
Created at: "2024-05-28"
Belongs to: "[[24q2]]"
Rating: "😐"
---
# May 2024
## Focus
- Finalize [[24q2-hire-editor]] — make the offer, onboard
- Publish first batch of [[24q2-10-pillar-articles]]
- Race Nove Colli ([[24q2-spring-gran-fondo]])
## Highlights
- Hired the editor — she starts full-time in June, doing first-pass editing on every newsletter issue
- Published "Staff Engineer vs. Manager" — it went semi-viral on LinkedIn, drove 800+ new subscribers in a single week
- Completed Nove Colli ([[24q2-spring-gran-fondo]]) in 7h42m — finished but bonked hard on the fourth climb due to heat
- [[24q2-build-podcast-landing-page]] kicked off — basic wireframes done, episodes 1-6 now need a proper home
- Newsletter hit 40.5k subscribers — the pillar article spike was significant
## Reflections
Mixed feelings this month. The editor hire is great news — having someone who can handle the first pass means I can focus on the ideas rather than the polish. But Nove Colli was humbling. The heat in Romagna was 34 degrees, and I didn't manage my nutrition well enough. Bonking on the fourth climb — legs completely empty, seeing spots — was scary. I sat on the side of the road for 10 minutes eating everything in my pockets before I could continue.
I finished, and that matters. But the time was disappointing. Lesson learned: nutrition strategy needs to be as planned as the training itself. The Maratona in July will be at altitude, which adds another variable. Need to take fueling much more seriously.
The "Staff Engineer vs. Manager" article reminded me why I do this. The number of engineers who DMed me saying "I needed to read this three years ago" — that's the impact I care about. More than subscriber counts.

View File

@@ -1,32 +0,0 @@
---
aliases: ["June 2024"]
Is A: Month
Created at: "2024-06-28"
Belongs to: "[[24q2]]"
Rating: "🤩"
---
# June 2024
## Focus
- Ship [[24q2-build-podcast-landing-page]] with full episode archive
- Complete [[24q2-sponsor-crm]] and migrate all sponsor data
- Ramp up training for Maratona dles Dolomites in July
## Highlights
- Launched [[24q2-build-podcast-landing-page]] — clean design, Apple/Spotify links, full show notes for all 8 episodes — podcast downloads crossed 5k/month
- [[24q2-sponsor-crm]] completed and live — all pipeline data migrated from scattered spreadsheets, [[person-matteo-cellini]] now tracks everything in one place
- Published 3 more [[24q2-10-pillar-articles]] including "How to Prioritize Technical Debt" (32k views)
- Editor fully onboarded and in rhythm — first-pass drafts arriving every Monday morning, quality is excellent
- Newsletter hit 42k subscribers — right on pace for [[2024-reach-50k-subscribers]]
- [[measure-sponsorship-mrr]] reached EUR 8.2k — strong Q2 performance
- Cycling: biggest training month yet — 620 km, including two back-to-back long rides simulating Maratona's profile
## Reflections
June was one of those months where everything clicks. The podcast landing page looks professional and is already converting visitors to subscribers. The sponsor CRM might be the most impactful internal tool we've built — no more "where's that sponsor contract?" emails. [[person-matteo-cellini]] went from managing sponsors in his head to having a proper pipeline view.
The editor hire is already paying off. She catches things I'd miss after staring at my own words for hours. The Monday morning routine of reviewing a clean draft instead of starting from a rough mess is a genuine quality-of-life improvement. My writing quality is actually better because I can focus on substance rather than mechanics.
Training is going well. The two back-to-back long rides (140 km + 120 km over a weekend) left me wrecked but confident. If I can do that in Tuscany's hills, the Dolomites are within reach. Bring on July.

View File

@@ -1,29 +0,0 @@
---
aliases: ["July 2024"]
Is A: Month
Created at: "2024-07-28"
Belongs to: "[[24q3]]"
Rating: "😄"
---
# July 2024
## Focus
- Race Maratona dles Dolomites — the big one for [[2024-complete-two-gran-fondos]]
- Begin [[24q3-premium-tier]] planning — pricing, content strategy, platform selection
- Launch [[24q3-summer-reading-sprint]] — target 8 books in July-August
## Highlights
- Completed Maratona dles Dolomites in 9h15m — Passo Giau at dawn was transcendent, Fedaia was brutal, but finished strong — [[2024-complete-two-gran-fondos]] achieved
- Started planning [[24q3-premium-tier]]: researched Substack paid, Ghost memberships, and Stripe checkout — leaning toward custom Stripe integration for flexibility
- Read "An Elegant Puzzle" and "Team Topologies" — both excellent, immediately useful for newsletter content
- Newsletter at 43.5k subscribers — summer slowdown typical but growth still positive
- Began outlining [[24q3-codemotion-talk]] abstract — submitted "Scaling Engineering Culture" as the proposed topic
- [[measure-podcast-downloads]] hit 6.5k/month — organic growth without new episodes (between seasons)
## Reflections
Maratona was the cycling highlight of the year, maybe of my life so far. 138 km through the Dolomites, 4,200m of climbing, starting in the dark at 6am in Corvara. The Sella and Gardena passes were hard, Campolongo was deceptively steep, but Giau — Giau was magical. The sunrise hitting the peaks as I climbed the final switchbacks, the silence broken only by breathing and chain noise. I didn't care about my time. I was just there.
The post-race week was recovery — both physical and mental. After months of structured training and race anxiety, there's a strange emptiness when the goal is achieved. I channeled that into reading and planning the premium tier. Pricing is the hardest part: too cheap and it devalues the content, too expensive and the audience is too small to sustain it. Need to think about this more in August.

View File

@@ -1,29 +0,0 @@
---
aliases: ["August 2024"]
Is A: Month
Created at: "2024-08-28"
Belongs to: "[[24q3]]"
Rating: "🤩"
---
# August 2024
## Focus
- Build and launch [[24q3-premium-tier]] — set up payments, create first premium content
- Continue [[24q3-summer-reading-sprint]] — deep reading weeks with lighter work schedule
- Record first episodes of [[24q3-podcast-season-2]]
## Highlights
- Launched [[24q3-premium-tier]] at EUR 8/month — 120 subscribers in the first week, bumped to EUR 12/month after seeing strong demand
- Premium content: published first deep-dive case study ("How Stripe Scales Engineering Teams") and announced monthly AMA calls
- Finished [[24q3-summer-reading-sprint]]: 8 books total across July-August, including "The Ride of a Lifetime" and "Thinking in Systems"
- Recorded episodes 13-16 of [[24q3-podcast-season-2]] — new guest interview format with [[person-sara-ricci]] coordinating guests
- Newsletter at 44.8k — August is always slow, but premium launch generated a subscriber bump
- [[person-matteo-cellini]] began outreach for [[24q3-new-sponsor-verticals]] — targeting cloud and DevOps companies
## Reflections
August is usually my favorite work month — lighter pace, longer mornings, room to think. This year it was productive in a different way. The premium tier launch felt risky: asking people to pay for something they'd been getting for free. But the response was encouraging. 120 subscribers in the first week validated the hypothesis. Bumping the price from EUR 8 to EUR 12 was a gut call based on [[person-matteo-cellini]]'s advice — "if people are subscribing this fast, you're underpriced." He was right.
The reading sprint was exactly what I needed. Eight books in two months, with time to actually think about each one. "Thinking in Systems" by Donella Meadows is probably the most underrated book in the business canon — it changed how I think about feedback loops in newsletter growth. Summer energy is real — I feel recharged and ready for an intense September.

View File

@@ -1,29 +0,0 @@
---
aliases: ["September 2024"]
Is A: Month
Created at: "2024-09-28"
Belongs to: "[[24q3]]"
Rating: "😄"
---
# September 2024
## Focus
- Deliver [[24q3-codemotion-talk]] at Codemotion Milan
- Push premium tier to 300+ subscribers
- Close Q3 strong on sponsor pipeline ([[24q3-new-sponsor-verticals]])
## Highlights
- Delivered [[24q3-codemotion-talk]] on "Scaling Engineering Culture" to ~400 attendees — standing ovation, 3 inbound sponsor inquiries within a week
- [[24q3-premium-tier]] hit 320 paid subscribers — AMA calls proving to be the most valued feature
- [[24q3-new-sponsor-verticals]]: [[person-matteo-cellini]] closed deals with two DevOps platform companies, diversifying the sponsor base
- Released episodes 17-20 of [[24q3-podcast-season-2]] — guest episode with a VP Engineering at a Series D startup was the most downloaded yet
- Newsletter hit 46k subscribers — Codemotion exposure plus podcast cross-promotion driving growth
- [[measure-sponsorship-mrr]] reached EUR 10.1k — first time crossing EUR 10k
## Reflections
Codemotion was terrifying and wonderful. I spent the week before rehearsing obsessively, convinced I'd forget my lines or that the audience wouldn't care. None of that happened. The talk landed well, the Q&A was engaged, and the hallway conversations afterward were some of the best I've had this year. Three sponsors reaching out unsolicited — that's the power of being on stage in front of your target audience.
But September also felt like the start of a crunch period. Premium content, podcast production, conference prep, sponsor pipeline — the workload is stacking up. I'm starting to feel the edges of my capacity. [[person-paco-furiani]] flagged that I missed two invoicing deadlines because I was focused on the talk. He's right — I need to stay disciplined about the operational basics even when the creative work is exciting. Heading into Q4, I need to be honest about what I can sustain.

View File

@@ -1,28 +0,0 @@
---
aliases: ["October 2024"]
Is A: Month
Created at: "2024-10-28"
Belongs to: "[[24q4]]"
Rating: "😐"
---
# October 2024
## Focus
- Begin [[24q4-laputa-start]] — initial prototype of the PKM tool
- Design [[24q4-annual-review-process]] template
- Start planning [[24q4-black-friday-campaign]]
## Highlights
- Started [[24q4-laputa-start]]: Tauri v2 + React scaffold, basic markdown file reading, first pass at YAML frontmatter parsing — spending evenings and weekends on it
- Drafted [[24q4-annual-review-process]] template with quarterly, goal, and project review sections
- [[24q4-black-friday-campaign]] strategy defined: 30% off annual premium, referral bonuses, limited-time offers
- Newsletter at 47.5k subscribers — growth steady but nothing spectacular
- [[person-matteo-cellini]] started building the [[24q4-sponsor-dashboard]] concept — mockups in Figma
## Reflections
October was a grind. The post-Codemotion high wore off fast, replaced by the reality of a packed Q4. Starting Laputa was exciting but also a bit reckless — adding a software project on top of everything else. But I couldn't help it. The frustration with existing PKM tools had been building for months, and once I started coding, I couldn't stop. The first time I saw my vault rendered in the four-panel layout, I got that spark you only get when you're building something you genuinely need.
The newsletter growth plateauing at ~47k is worrying. We need a big push to hit [[2024-reach-50k-subscribers]] by year end. The Black Friday campaign is the obvious lever, but we need it to be genuinely compelling, not just another discount email. [[person-paco-furiani]] suggested adding a community element to the campaign — worth exploring. Energy-wise, I'm running at about 70%. Not burned out, but not firing on all cylinders either. Need to protect sleep and exercise.

View File

@@ -1,31 +0,0 @@
---
aliases: ["November 2024"]
Is A: Month
Created at: "2024-11-28"
Belongs to: "[[24q4]]"
Rating: "😄"
---
# November 2024
## Focus
- Execute [[24q4-black-friday-campaign]] — the big push to 50k subscribers
- Ship [[24q4-sponsor-dashboard]] for sponsor self-service metrics
- Continue Laputa development ([[24q4-laputa-start]])
## Highlights
- [[24q4-black-friday-campaign]] was a massive success: 2,800 new subscribers in 10 days, 180 free-to-premium conversions, best campaign ever
- Newsletter crossed 50k subscribers on November 22nd — [[2024-reach-50k-subscribers]] achieved, two months ahead of my mental timeline
- Shipped [[24q4-sponsor-dashboard]] — sponsors can now see impressions, clicks, and conversion data in real-time — [[person-matteo-cellini]] reports dramatically fewer support emails
- Laputa ([[24q4-laputa-start]]) now has working search, type-based navigation, and basic frontmatter editing — using it for my own vault daily
- [[measure-sponsorship-mrr]] hit EUR 11.2k — November renewals came in strong
- Recorded end-of-year reflections episode for the podcast — raw and personal, felt right
## Reflections
Crossing 50k subscribers was the defining moment of the month. I remember refreshing the Substack dashboard at 11pm on a Friday, watching the number tick from 49,998 to 50,001. It sounds silly, but I actually teared up a little. Two years ago, 50k felt like a fantasy. The Black Friday campaign was the catalyst, but it only worked because of 11 months of consistent work before it.
The sponsor dashboard was [[person-matteo-cellini]]'s baby, and he executed it beautifully. Giving sponsors self-serve access to their data is the kind of product thinking that separates a media business from a newsletter. [[person-paco-furiani]] handled the backend automation — pulling data from Substack's API into a clean dashboard. Small team, big impact.
Laputa is becoming a real tool. I'm using it every day now, and the workflow of navigating my vault through types and relationships is already better than what I had in Obsidian. It's rough around the edges, but the bones are good. Heading into December with real momentum.

View File

@@ -1,32 +0,0 @@
---
aliases: ["December 2024"]
Is A: Month
Created at: "2024-12-28"
Belongs to: "[[24q4]]"
Rating: "😄"
---
# December 2024
## Focus
- Run [[24q4-annual-review-process]] — review all goals, projects, and metrics for [[2024]]
- Complete [[24q4-cycling-year-review]] — analyze training data, plan 2025 cycling goals
- Set [[2025]] goals and plan [[25q1]]
## Highlights
- Completed [[24q4-annual-review-process]]: reviewed all 4 quarters, 20 projects, and 5 annual goals — documented everything in structured templates
- [[24q4-cycling-year-review]]: 6,200 km total, FTP up 12% YoY, 2 gran fondos completed — decided to target Stelvio for [[2025-ride-stelvio]]
- Set [[2025]] goals: [[2025-reach-85k-subscribers]], [[2025-reach-22k-mrr]], [[2025-ship-laputa]], [[2025-ride-stelvio]], [[2025-read-20-books]]
- Newsletter closed the year at 50.2k subscribers — slight post-Black-Friday churn, but net position is excellent
- [[measure-sponsorship-mrr]] ended at EUR 11.4k — [[2024-double-revenue]] confirmed (was EUR 5.5k in January)
- Read 2 books in December to finish at 26 total — [[2024-read-24-books]] exceeded
- [[person-paco-furiani]] and I sketched out the 2025 operational calendar — quarterly planning cadence locked in
## Reflections
December is always reflective, and this one more than most. Sitting down to review [[2024]] in detail, I realized that every major goal was either hit or exceeded. That almost never happens. The newsletter from 35k to 50k. Revenue doubled. Podcast launched and growing. Two gran fondos. 26 books. It was a genuinely great year.
But the review also surfaced where I was lucky vs. where I was good. The Black Friday campaign was well-executed, but the timing was also favorable — low competition in the engineering leadership newsletter space. The pillar articles strategy worked partly because of SEO tailwinds. I don't want to mistake a rising tide for good swimming.
Looking ahead to [[2025]], the goals are deliberately ambitious: 85k subscribers, EUR 22k MRR, shipping Laputa as a product, riding Stelvio. These require a different gear than 2024. More team, more systems, less heroics. The cycling review was satisfying — seeing 6,200 km logged, the FTP curve rising, the race results improving — it's the same compounding principle as the newsletter, just applied to watts instead of words. Here's to [[2025]].

View File

@@ -1,32 +0,0 @@
---
aliases: ["Complete Two Gran Fondos"]
Is A: Goal
Belongs to: "[[2024]]"
Status: Done
---
# Complete Two Gran Fondos
Cycling is a core part of how I manage energy and sustain focus for the business. Completing two gran fondos in 2024 was about proving that endurance fitness and a demanding content business can coexist, and about setting a physical benchmark for the year tied to [[responsibility-health-fitness]].
## Success criteria
- Finish at least two official gran fondo events (120+ km each)
- Complete both without DNF, regardless of placement
- Maintain consistent training volume of 400+ km/month during the build phase
- No injuries that impact work capacity
## Key milestones
- Plan the cycling season and select target events during [[24q1-plan-cycling-season]]
- Build base fitness through structured winter training (Jan-Mar)
- Complete first gran fondo by end of Q2
- Complete second gran fondo by end of Q3
- Maintain [[measure-cycling-km-per-month]] above 400 km during peak training months
## Notes
- Completed both events successfully: Nove Colli in May and Maratona dles Dolomites in July.
- Training volume averaged 480 km/month from February through July, well above target.
- The structured training plan from [[24q1-plan-cycling-season]] was essential. Without it, the business would have crowded out ride time.
- Key insight: blocking training rides on the calendar like meetings is non-negotiable. Treating fitness as optional leads to skipped sessions.
- This goal set the foundation for the more ambitious [[2025-ride-stelvio]] target.

View File

@@ -1,34 +0,0 @@
---
aliases: ["Double Sponsorship Revenue"]
Is A: Goal
Belongs to: "[[2024]]"
Status: Done
---
# Double Sponsorship Revenue
Revenue growth is the clearest signal that the newsletter business is becoming a sustainable long-term venture. Doubling MRR from ~7k to 14k+ in 2024 would prove that the sponsorship model scales with audience growth and that the business can support a small team without requiring a pivot to paid subscriptions or courses.
## Success criteria
- Reach 14,000 EUR MRR by December 2024
- Maintain or increase average deal size (target: 1,200+ EUR per placement)
- Onboard at least 6 new sponsors over the year
- Achieve 80%+ renewal rate on existing sponsor contracts
- Keep [[measure-close-rate]] above 30% on inbound leads
## Key milestones
- Launch restructured sponsorship packages with tiered pricing in [[24q1-launch-sponsorship-packages]]
- Build a lightweight CRM to manage pipeline and renewals via [[24q2-sponsor-crm]]
- Hit 10k MRR by end of Q2 (midpoint target)
- Reach 12k MRR by end of Q3 with [[24q3-premium-tier]] upsells
- Close Q4 at 14k+ MRR, with pipeline visibility into Q1 2025
## Notes
- Final MRR in December 2024: approximately 15,200 EUR. Goal exceeded by ~8%.
- The tiered sponsorship packages from [[24q1-launch-sponsorship-packages]] were the single biggest driver. The premium tier accounted for 40% of new revenue.
- [[24q2-sponsor-crm]] was simple (a Notion database) but effective. Having a structured pipeline view reduced missed follow-ups significantly.
- Renewal rate landed at 78%, just below the 80% target. Two sponsors churned due to budget cuts, not dissatisfaction.
- The subscriber growth from [[2024-reach-50k-subscribers]] directly enabled higher pricing. Audience size and engagement metrics are the foundation of sponsorship value.
- This sets the trajectory for [[2025-reach-22k-mrr]], which will require both audience growth and continued pricing optimization.

View File

@@ -1,34 +0,0 @@
---
aliases: ["Launch Refactoring Podcast"]
Is A: Goal
Belongs to: "[[2024]]"
Status: Done
---
# Launch Refactoring Podcast
The podcast is the biggest new distribution channel for Refactoring in 2024. Audio content reaches a different audience segment than the newsletter and deepens engagement with existing subscribers. A successful podcast also opens new sponsorship inventory, directly supporting [[2024-double-revenue]] and [[responsibility-sponsorships]].
## Success criteria
- Launch Season 1 by end of Q1 2024
- Publish at least 20 episodes across two seasons in the year
- Reach 1,000+ downloads per episode by Season 2
- Secure at least 2 podcast-specific sponsors
- Maintain a sustainable production cadence without degrading newsletter quality
## Key milestones
- Produce and launch Season 1 during [[24q1-podcast-season-1]] (8-10 episodes)
- Establish production workflow with [[person-paco-furiani]] handling editing and distribution
- Launch Season 2 during [[24q3-podcast-season-2]] with improved format based on Season 1 feedback
- Track growth via [[measure-podcast-downloads]] and [[measure-podcast-episodes-per-month]]
- Review analytics quarterly using [[procedure-podcast-analytics]]
## Notes
- Season 1 launched in March 2024 with 10 episodes. Season 2 launched in August with 12 episodes. Total: 22 episodes, exceeding the 20-episode target.
- Downloads per episode grew from ~400 in Season 1 to ~1,300 by late Season 2. Crossed the 1,000 threshold in episode 16.
- Secured 2 podcast sponsors by Q3, both cross-sold from existing newsletter sponsors. The podcast inventory added approximately 1,800 EUR/month to [[measure-sponsorship-mrr]].
- Production workflow stabilized by episode 5. [[person-paco-furiani]] now handles end-to-end production with minimal input needed.
- Interview format performed better than solo episodes for downloads. Solo episodes had higher completion rates.
- The podcast is now a permanent channel. Planning [[25q2-podcast-season-3]] to continue momentum into 2025.

View File

@@ -1,34 +0,0 @@
---
aliases: ["Reach 50k Subscribers"]
Is A: Goal
Belongs to: "[[2024]]"
Status: Done
---
# Reach 50k Subscribers
Subscriber count is the foundational growth metric for the newsletter business. Reaching 50,000 subscribers by end of 2024 would put Refactoring firmly in the top tier of engineering newsletters, enabling premium sponsorship pricing and establishing the audience base needed for future product launches. This goal is the north star for all [[responsibility-grow-newsletter]] efforts.
## Success criteria
- End 2024 with 50,000+ total newsletter subscribers
- Maintain monthly net growth of 2,000+ subscribers
- Keep unsubscribe rate below 1.5% per issue
- Achieve organic growth rate of at least 60% (not purely paid acquisition)
- Maintain [[measure-open-rate]] above 45% as the list scales
## Key milestones
- Cross 35,000 subscribers by end of Q1, validating early-year growth tactics
- Launch [[24q2-10-pillar-articles]] to drive SEO-based organic acquisition
- Cross 42,000 by mid-year, on pace for the 50k target
- Experiment with [[24q4-linkedin-crossposting]] as an additional growth channel in Q4
- Hit 50,000 by December with a sustained final push through [[responsibility-grow-newsletter]]
## Notes
- Final count: 53,000 subscribers by December 2024. Exceeded target by 6%.
- Organic growth accounted for approximately 65% of new subscribers. Top sources: Twitter/X, SEO from pillar articles, and word-of-mouth referrals.
- The [[24q2-10-pillar-articles]] project was the single biggest organic growth lever. Those 10 articles now account for ~30% of monthly organic signups.
- [[measure-open-rate]] held at 47% even at scale, which is strong for a list this size. This is a key selling point for sponsors.
- LinkedIn cross-posting ([[24q4-linkedin-crossposting]]) added ~150 subscribers directly but had an outsized brand awareness effect.
- The path to [[2025-reach-85k-subscribers]] is clear but will require new channels. Organic growth from existing channels alone will not close the gap.

View File

@@ -1,33 +0,0 @@
---
aliases: ["Read 24 Books in 2024"]
Is A: Goal
Belongs to: "[[2024]]"
Status: Behind
---
# Read 24 Books in 2024
Consistent reading is the primary input for original thinking and content quality. A target of 24 books (2 per month) ensures a steady flow of ideas feeding into newsletter essays, podcast topics, and [[responsibility-learning]]. Falling behind on reading directly correlates with weaker content output.
## Success criteria
- Read 24 books by December 31, 2024 (2 per month average)
- Maintain at least 1 book per month even in busy periods
- Track progress via [[measure-books-per-month]]
- At least 50% should be non-fiction relevant to the newsletter (engineering, business, leadership)
- Create at least 5 [[measure-evergreen-notes-created]] from each book
## Key milestones
- Establish the reading habit in Q1 with 6 books by March
- Maintain pace through Q2 despite the busy season for [[24q2-10-pillar-articles]]
- Reach 18 books by end of Q3
- Close the year at 24 by maintaining 2/month in Q4
## Notes
- Final count: 18 books. Missed the 24-book target by 6 books (75% completion).
- Q1 started strong at 7 books, ahead of pace. Q2 dropped to 3 books due to the pillar articles push and podcast launch.
- Q3 recovered to 5 books. Q4 was only 3 books as year-end business priorities took over.
- The shortfall was entirely a time management issue, not a motivation issue. During weeks with heavy content production, reading was the first thing to get cut.
- Adjusted the 2025 target to 20 books ([[2025-read-20-books]]) to be more realistic given current business demands.
- Key insight: audiobooks during cycling training significantly boosted volume. 6 of the 18 books were consumed this way. Will lean into this more in 2025.

View File

@@ -1,49 +0,0 @@
---
aliases: ["2024"]
Is A: Year
Created at: "2024-01-01"
Has: ["[[24q1]]", "[[24q2]]", "[[24q3]]", "[[24q4]]"]
---
# 2024
## Theme
2024 was the growth year — the year Refactoring went from "successful newsletter" to "real business." At the start of January, I had ~35,000 subscribers, ad-hoc sponsor deals, no podcast, and a vague sense that this could become something bigger. By December, the newsletter had crossed 50k subscribers, revenue had more than doubled, the podcast was a genuine channel, and I had a small team helping me run things. The word that keeps coming back when I think about this year is *professionalization*.
But 2024 was also the year I stopped being just a content creator and started being a founder. Launching sponsorship packages, hiring an editor, building a sponsor CRM, creating a premium tier — these aren't creative acts, they're business-building acts. And toward the end of the year, starting [[24q4-laputa-start]] added a product dimension that I hadn't anticipated. I ended the year wearing more hats than ever, but feeling more focused than ever. Paradox noted.
## Highlights
- Newsletter grew from ~35k to 50k+ subscribers — [[2024-reach-50k-subscribers]] achieved in November
- Revenue more than doubled: [[measure-sponsorship-mrr]] went from EUR 5.5k in January to EUR 11.4k in December — [[2024-double-revenue]] hit
- Launched the podcast in [[24q1]] — grew from 0 to 8k+ downloads/month across two seasons ([[24q1-podcast-season-1]], [[24q3-podcast-season-2]])
- Shipped [[24q3-premium-tier]] with 320+ paid subscribers by year end
- Completed both gran fondos — Nove Colli ([[24q2-spring-gran-fondo]]) and Maratona dles Dolomites — [[2024-complete-two-gran-fondos]] done
- Published [[24q2-10-pillar-articles]] including "Staff Engineer vs. Manager" (50k+ views)
- Delivered [[24q3-codemotion-talk]] to ~400 attendees — first major conference appearance
- Built the team: [[person-matteo-cellini]] on partnerships, [[person-paco-furiani]] on operations, freelance editor for content
- [[24q4-black-friday-campaign]] drove 2,800 new subscribers in 10 days
- Started building [[24q4-laputa-start]] — the PKM tool I'd been dreaming about for years
## By the numbers
- **Subscribers**: 35k to 50.2k (+43%)
- **Sponsorship MRR**: EUR 5.5k to EUR 11.4k (+107%)
- **Podcast downloads**: 0 to 8.2k/month (24 episodes across 2 seasons)
- **Premium subscribers**: 0 to 320 (launched in [[24q3]])
- **Books read**: 26 — [[2024-read-24-books]] exceeded by 2
- **Cycling km**: 6,200 km — [[measure-cycling-km-per-month]] averaged ~517 km
- **Gran fondos**: 2/2 — Nove Colli (7h42m), Maratona dles Dolomites (9h15m)
- **Open rate**: [[measure-open-rate]] averaged 44.2%, up from 41.8% in 2023
- **Conference talks**: 1 (Codemotion Milan)
- **Total revenue**: ~EUR 155k (sponsorships + premium + speaking)
## Reflections
Looking back at 2024, the thing I'm most proud of isn't any single achievement — it's the system. At the start of the year, everything ran through me. By December, [[person-matteo-cellini]] was closing sponsor deals I never even saw, [[person-paco-furiani]] was handling invoicing and logistics autonomously, and the editor was shipping polished drafts on a weekly cadence. I built leverage, not just output.
The podcast was the surprise of the year. I'd been so anxious about launching it — imposter syndrome about my speaking voice, worry about production quality, fear that nobody would listen. But the format I landed on (20-minute focused deep dives) resonated immediately. Season 2 adding guest interviews was the right evolution. By December, the podcast was driving subscriber growth, sponsor interest, and content ideas in a way I hadn't anticipated. It's now an essential pillar, not a side experiment.
The premium tier launch in Q3 validated something I'd been unsure about: that people would pay for curated, structured content even when the free version was already high quality. The 320 subscribers at EUR 12/month represent a small but meaningful revenue stream — and more importantly, a direct relationship with my most engaged readers. The AMA calls are gold for understanding what engineering leaders actually struggle with.
If I'm being honest about what didn't go well: I pushed too hard in Q3. The combination of the premium launch, Codemotion, and Maratona training left me running on fumes by October. The Black Friday campaign in Q4 was successful but felt mechanical — I was going through motions rather than creating with energy. That's a warning sign I need to heed going into [[2025]]. The business can scale, but my energy can't scale linearly with it. I need to hire more, delegate more, and protect creative time more aggressively. The Laputa project ([[24q4-laputa-start]]) is a wildcard heading into next year — it could be a distraction or it could be the most important thing I build. We'll see.

View File

@@ -1,29 +0,0 @@
---
aliases: ["January 2025"]
Is A: Month
Created at: "2025-01-28"
Belongs to: "[[25q1]]"
Rating: "😄"
---
# January 2025
## Focus
- Begin [[25q1-laputa-v1]] development sprint — four-panel layout, markdown rendering, search
- Plan [[25q1-rate-increase]] strategy with [[person-matteo-cellini]]
- Start [[25q1-strength-program]] — 3x/week gym sessions
## Highlights
- Kicked off [[25q1-laputa-v1]] development with a focused 2-week sprint — four-panel layout working, markdown rendering with frontmatter extraction functional
- Researched competitor sponsor rates and prepared [[25q1-rate-increase]] proposal with [[person-matteo-cellini]] — planning to raise Gold from EUR 2.8k to EUR 3.5k/issue
- Started [[25q1-strength-program]]: working with a coach, 3x/week focusing on core stability and leg strength for cycling
- Newsletter at 51.8k subscribers — steady organic growth to start the year
- Read "Build" by Tony Fadell — excellent on product thinking, relevant to Laputa
- Cycling: 320 km, mostly indoor on the trainer — January weather in northern Italy is brutal
## Reflections
January is always a reset month, and this one felt especially deliberate. Coming off the high of [[2024]], I wanted to start [[2025]] with intention rather than momentum alone. The Laputa development sprint was intense — coding every evening after the newsletter work was done, often until midnight. But seeing the four-panel layout come alive with my own vault data was deeply motivating. It's scratching an itch that's been bothering me for years.
The strength program is humbling. My first session, I could barely hold a plank for 60 seconds. Years of cycling have given me the cardiovascular fitness of someone 10 years younger and the upper body strength of someone 10 years older. The coach just smiled and said "we'll fix that." Three weeks in, everything hurts, but I can already feel the difference in posture and stability on the bike.

View File

@@ -1,29 +0,0 @@
---
aliases: ["February 2025"]
Is A: Month
Created at: "2025-02-28"
Belongs to: "[[25q1]]"
Rating: "🤩"
---
# February 2025
## Focus
- Execute [[25q1-rate-increase]] — communicate new pricing to sponsors
- Launch [[25q1-referral-program]] — design tiers, set up tracking
- Continue [[25q1-laputa-v1]] — add search and navigation features
## Highlights
- Executed [[25q1-rate-increase]]: communicated new Gold rate (EUR 3.5k/issue) to all sponsors — 9 out of 10 renewed without pushback, one negotiated a 6-month lock at the old rate
- Launched [[25q1-referral-program]] with three tiers: 3 referrals (exclusive article), 10 referrals (premium trial), 25 referrals (1:1 call) — 1,100 referral subscribers in the first 3 weeks
- [[25q1-laputa-v1]] now has full-text search, type-based sidebar navigation, and keyboard shortcuts — my daily driver for vault browsing
- [[measure-sponsorship-mrr]] jumped to EUR 13.5k after rate increase — significant lift with minimal churn
- Newsletter at 54.2k subscribers — referral program providing a noticeable acceleration
- [[person-matteo-cellini]] onboarded a new enterprise sponsor (cloud infrastructure company) at the higher rate — first deal at the new pricing
## Reflections
This was the month I learned that raising prices is one of the highest-leverage moves in a business. We spent weeks agonizing over the rate increase, running scenarios, worrying about sponsor churn. And then... almost nothing happened. Sponsors said "sure, sounds fair" and renewed. One even said "honestly, I'm surprised you didn't raise earlier." That's the curse of the bootstrapper mindset — you're so grateful for every dollar that you forget to charge what you're worth.
The referral program is generating genuine excitement. Seeing readers actively sharing Refactoring with their colleagues because they want the rewards — it's word-of-mouth at scale. The 1:1 call tier at 25 referrals is clever because it creates a personal connection between me and my most engaged readers. Three people earned it in the first month, and those calls were some of the best conversations I've had about engineering leadership. February was excellent. Everything is tracking.

View File

@@ -1,30 +0,0 @@
---
aliases: ["March 2025"]
Is A: Month
Created at: "2025-03-28"
Belongs to: "[[25q1]]"
Rating: "😄"
---
# March 2025
## Focus
- Complete [[25q1-newsletter-seo-sprint]] — optimize top 15 articles for search
- Ship [[25q1-laputa-v1]] as internal release — feature-complete for personal use
- First outdoor rides of the season
## Highlights
- Completed [[25q1-newsletter-seo-sprint]]: optimized 15 pillar articles with updated titles, meta descriptions, internal linking, and structured data — organic traffic up 40% by month end
- [[25q1-laputa-v1]] shipped as "internal v1" — search, navigation, frontmatter editing, and property panel all working — vault has 9,000+ files and performs well
- Referral program ([[25q1-referral-program]]) running strong — 3,200 total referral subscribers in Q1
- Newsletter hit 56k subscribers — [[25q1]] delivered well above expectations
- [[25q1-strength-program]] paying off — can hold a 2-minute plank now, leg press up 40%, climbing power noticeably improved
- First outdoor ride of the season: 85 km along the Po River, 12 degrees, absolutely beautiful
- [[measure-sponsorship-mrr]] at EUR 14.2k — strong close to [[25q1]]
## Reflections
March was the month where Q1 came together. The SEO sprint was unglamorous work — rewriting titles, adding alt text to images, building internal link maps — but the results were immediate. One article ("How to Run Effective 1:1s") went from page 3 to the #2 result for its target keyword. That single article now brings in 200+ organic subscribers per month. The compounding effect of SEO on a content business is underrated.
Shipping Laputa v1 felt like a real milestone, even if I'm the only user. It's not pretty, it has bugs, and the graph view is more confusing than helpful. But it works. I browse my vault in it every day — types on the left, notes in the middle, properties on the right. The workflow is faster than Obsidian for my use case, and that's all the validation I need to keep building. Next up: bidirectional linking and a better graph. Closing [[25q1]] feeling strong and focused. [[25q2]] is going to be intense.

View File

@@ -1,31 +0,0 @@
---
aliases: ["April 2025"]
Is A: Month
Created at: "2025-04-28"
Belongs to: "[[25q2]]"
Rating: "😄"
---
# April 2025
## Focus
- Begin [[25q2-podcast-season-3]] production — new "Tactical Tuesday" format alongside regular episodes
- Start [[25q2-laputa-v2]] development — bidirectional linking, graph view improvements
- Plan [[25q2-team-retreat]] logistics for late May
## Highlights
- Recorded first 4 episodes of [[25q2-podcast-season-3]] including the debut "Tactical Tuesday" — 15-minute actionable episodes dropping every Tuesday
- Started [[25q2-laputa-v2]] with bidirectional linking engine — the backlinks panel is already transforming how I navigate between notes
- [[25q2-team-retreat]] booked: farmhouse outside Siena, 3 days in late May, agenda drafted with [[person-paco-furiani]]
- Newsletter at 58.5k subscribers — growth accelerating, referral program still compounding
- Published a deep-dive on "The Architecture of Engineering Onboarding" — 28k views, widely shared on HackerNews
- Cycling: 480 km, first real outdoor training block — spring is here and the motivation is high
## Reflections
April felt like the start of the real work for [[25q2]]. The "Tactical Tuesday" format for the podcast was an experiment born from reader feedback — people kept saying "I love the deep dives but sometimes I just need a quick actionable takeaway." So we're doing both: deep dives on Thursdays, tactical tips on Tuesdays. Early numbers are promising — the short episodes have a higher completion rate.
The Laputa work on bidirectional linking was technically challenging but incredibly satisfying. Clicking on a note and seeing all the other notes that link to it, without having to manually maintain those connections — it's the kind of feature that makes you wonder how you ever worked without it. My vault is starting to feel alive, like a knowledge graph I can actually traverse.
Planning the retreat with [[person-paco-furiani]] was a reminder of how much the team dynamic matters. We're four people working remotely, mostly asynchronously. That works for execution, but not for alignment. Three days together in Tuscany should fix that.

View File

@@ -1,30 +0,0 @@
---
aliases: ["May 2025"]
Is A: Month
Created at: "2025-05-28"
Belongs to: "[[25q2]]"
Rating: "😐"
---
# May 2025
## Focus
- Execute [[25q2-team-retreat]] in Tuscany
- Push toward [[25q2-reach-70k]] — growth sprint with coordinated content + referral push
- Continue [[25q2-laputa-v2]] — graph view and frontmatter editing
## Highlights
- [[25q2-team-retreat]] in Siena was transformational — 3 days of strategy, bonding, and cooking together with [[person-matteo-cellini]], [[person-paco-furiani]], and [[person-sara-ricci]]
- The community idea emerged from a dinner conversation at the retreat — [[person-sara-ricci]] pitched it, everyone immediately saw the potential
- Newsletter at 64k subscribers — growth strong but the 70k target for June feels aggressive
- [[25q2-laputa-v2]] graph view working but confusing at scale — need to rethink the visual density for 9,000+ nodes
- Two newsletter issues underperformed — open rates dipped below 40% for the first time this year
## Reflections
The retreat was everything I hoped for and more. There's a moment on the second evening — the four of us on the terrace, bottles of Brunello open, whiteboarding H2 plans on a flip chart — where it hit me that this is a real team, not just a collection of freelancers. [[person-sara-ricci]]'s community pitch was brilliant in its simplicity: "Your readers already help each other in reply-all email chains. Give them a proper space to do it." That became [[25q3-community-launch]].
But May was also the first month this year where I felt like I was slipping. Two issues with below-40% open rates is a warning sign. The topics weren't wrong, but the subject lines were lazy — I wrote them in a rush. In a newsletter business, subject lines are the product as much as the content. Sloppy subject lines = sloppy product. Need to get disciplined about this again.
The Laputa graph view at 9,000 nodes is essentially unusable — just a hairball of connections. I need to rethink the approach: maybe cluster by type, maybe progressive disclosure, maybe skip the full graph entirely and focus on local neighborhoods. Technical challenge, but also a product design challenge. Not everything that works at 100 nodes works at 10,000.

View File

@@ -1,32 +0,0 @@
---
aliases: ["June 2025"]
Is A: Month
Created at: "2025-06-28"
Belongs to: "[[25q2]]"
Rating: "🤩"
---
# June 2025
## Focus
- Hit [[25q2-reach-70k]] subscribers
- Ship [[25q2-laputa-v2]] with refined graph view and improved frontmatter editing
- Start planning [[25q2-dolomites-trip]] logistics for late June
## Highlights
- Hit [[25q2-reach-70k]] subscribers on June 12th — two weeks ahead of schedule, driven by two viral LinkedIn posts and the referral program
- Shipped [[25q2-laputa-v2]]: bidirectional linking, local graph view (neighborhood-based, not full graph), improved frontmatter editing — feels like a real product now
- Completed [[25q2-dolomites-trip]]: 4 days, 5 passes, 380 km with three friends — Passo Fedaia, Pordoi, Sella, Gardena, Campolongo
- [[25q2-podcast-season-3]] "Tactical Tuesday" format growing fast — 14k total downloads/month
- [[measure-sponsorship-mrr]] hit EUR 17.5k — two new enterprise sponsors signed
- Premium tier reached 680 paying subscribers — AMA calls now have 40+ attendees
- Two pillar articles syndicated by InfoQ and The New Stack — drove ~4k new subscribers
## Reflections
June was the peak of [[25q2]] — everything came together. Hitting 70k subscribers two weeks early felt effortless, which is a sign that the flywheel is genuinely spinning now. The LinkedIn posts that went viral weren't planned; they were honest reflections on engineering leadership challenges that resonated. Authenticity scales, apparently.
The Dolomites trip was the cycling highlight of the year so far. Four days of pure mountain riding with friends — the kind of trip where you forget about metrics and just pedal. Passo Fedaia at sunset, the Marmolada glacier turning orange overhead, was a moment I'll remember forever. My legs felt strong — the [[25q1-strength-program]] is paying dividends in climbing power and stability.
Shipping Laputa v2 with the local graph view was the right call. Instead of showing all 9,000 nodes (useless hairball), it shows the current note plus 2 degrees of connection. Clean, useful, navigable. Sometimes the best product decisions are about what you don't show. Heading into Q3 with serious momentum and slightly too much on my plate.

View File

@@ -1,31 +0,0 @@
---
aliases: ["July 2025"]
Is A: Month
Created at: "2025-07-28"
Belongs to: "[[25q3]]"
Rating: "😄"
---
# July 2025
## Focus
- Begin [[25q3-ebook]] production — compile, edit, and design "The Engineering Leader's Playbook"
- Prepare for [[25q3-community-launch]] on Circle — set up platform, invite founding members
- Enter [[25q3-peak-training]] block for Stelvio in August
## Highlights
- [[25q3-ebook]] first draft completed — 12 chapters compiled from the best newsletter content, heavily rewritten for book format, sent to editor
- [[25q3-community-launch]] platform set up on Circle — designed channels (ask-anything, career-advice, tech-leadership, off-topic), invited first 200 premium subscribers as founding members
- [[25q3-peak-training]] underway: structured intervals 4x/week, long ride every Saturday, altitude simulation sessions on the trainer
- Newsletter at 72k subscribers — summer slowdown minimal thanks to the community buzz
- Recorded first episodes of [[25q3-podcast-season-4]] — the 3-part "Building Engineering Culture from Scratch" series
- [[person-sara-ricci]] took over community moderation and onboarding, freeing me to focus on content and training
## Reflections
July was the month where the ambition of Q3 became real. Three major launches (ebook, community, podcast season) plus peak training for Stelvio. On paper it looks insane. In practice... it felt intense but manageable, mostly because [[person-sara-ricci]] stepped up massively on the community side. She went from coordinating podcast guests to designing the entire community architecture. The founding members are engaged, the conversations are high-quality, and I barely had to touch it.
The ebook rewriting was harder than expected. Newsletter articles are written for a 5-minute read with specific context. A book chapter needs to stand alone, build on previous chapters, and go deeper. I spent more time rewriting than compiling. But the result feels substantial — not a lazy content recycling, but a genuine book that adds value beyond the newsletter.
Training is going well. The altitude simulation sessions are brutal — 90 minutes at reduced oxygen while doing threshold intervals — but I can feel my body adapting. FTP is climbing toward 280W. Stelvio from Bormio is 24 km at 7.1% average gradient. At my weight and power, that's roughly 1h50m. Tight, but possible. The mountain is calling.

View File

@@ -1,31 +0,0 @@
---
aliases: ["August 2025"]
Is A: Month
Created at: "2025-08-28"
Belongs to: "[[25q3]]"
Rating: "🤩"
---
# August 2025
## Focus
- Publish [[25q3-ebook]] — final edits, cover design, launch campaign
- Ride Stelvio — the culmination of [[25q3-peak-training]] and [[2025-ride-stelvio]]
- Open [[25q3-community-launch]] to all premium subscribers
## Highlights
- Published [[25q3-ebook]] "The Engineering Leader's Playbook" — 1,400 copies sold in the first month at EUR 29, far exceeding the 500-copy target
- Rode Stelvio from Bormio in 1h52m — [[2025-ride-stelvio]] achieved — the hardest and most beautiful thing I've done on a bike
- [[25q3-community-launch]] opened to all premium members — 850 founding members, 65% weekly active rate from day one
- [[25q3-podcast-season-4]] debut: "Building Engineering Culture from Scratch" Part 1 got 22k downloads in the first week — biggest episode ever
- FTP peaked at 285W during [[25q3-peak-training]] — best numbers of my life
- Newsletter at 75k subscribers
## Reflections
Stelvio. I don't know how to write about it without sounding dramatic, so I'll just be dramatic. It was the hardest thing I've ever done on a bike. The Bormio side starts deceptively gentle — wide roads, gradual grade, you think "this isn't so bad." Then the switchbacks begin. 48 of them. Each one steeper than the last, each one revealing another section of road winding above you. At km 18, with 6 km still to go, I almost stopped. My legs were screaming, my heart rate was at 175, and a voice in my head was saying "you've proved enough." But I kept going because the summit was up there, and I'd been thinking about it since December. 1h52m. Not fast. But done. I cried at the top. No shame.
The ebook launch going well was almost anticlimactic after Stelvio. But 1,400 copies in the first month is genuinely impressive for a self-published niche book. The "real book" effect is interesting — people treat you differently when you have a book, even if the content is largely derived from free newsletter articles. Perception is reality in media.
I need to be honest: by the end of August, I was running on empty. Three big things shipped (ebook, community, Stelvio), podcast recording ongoing, newsletter still weekly. Sleep has been under 6 hours most nights. [[person-paco-furiani]] told me I looked tired on our weekly call. He's right. September needs to include real rest, or I'll pay for it later.

View File

@@ -1,31 +0,0 @@
---
aliases: ["September 2025"]
Is A: Month
Created at: "2025-09-28"
Belongs to: "[[25q3]]"
Rating: "😄"
---
# September 2025
## Focus
- Deliver keynote at [[25q3-leaddev-london]]
- Recover from August intensity — schedule white space, prioritize sleep
- Close Q3 metrics and plan Q4
## Highlights
- Delivered keynote at [[25q3-leaddev-london]] on "The Newsletter-to-Business Pipeline" to 1,200 attendees — biggest stage ever, standing ovation, 5 enterprise sponsor inquiries
- Took 10 days fully off in early September — first real break since January, no email, no Slack, just cycling and reading
- Newsletter hit 78k subscribers by month end — the LeadDev exposure and ebook cross-promotion driving a late-Q3 surge
- [[measure-sponsorship-mrr]] reached EUR 20.1k — [[2025-reach-22k-mrr]] within reach for Q4
- [[25q3-podcast-season-4]] wrapped up with 8 episodes released — consistently above 15k downloads per episode
- Community stabilizing at 850 members with strong engagement — [[person-sara-ricci]] running it almost independently
## Reflections
The 10 days off in early September saved me. I came back from August brittle — snapping at [[person-paco-furiani]] over minor things, dreading the newsletter instead of enjoying it, skipping workouts. The break was non-negotiable: I told the team I'd be unreachable, set up auto-responders, and disappeared into the Dolomites with my bike and a stack of books. By day 4, the fog lifted. By day 7, I was excited about work again. By day 10, I had a notebook full of ideas for Q4.
LeadDev London was the professional peak of the year. The keynote was different from Codemotion last year — bigger stage, broader audience, higher stakes. But I was more prepared and, honestly, more confident. The talk traced the arc from "person with a newsletter" to "media business with a team, a podcast, a book, and a community." The vulnerability of sharing revenue numbers on stage felt risky but landed well. Authenticity again.
Q3 was the most productive quarter of my career. Ebook published, community launched, Stelvio climbed, LeadDev delivered, podcast season completed. But it was also the most draining. The lesson is crystal clear: I can sustain this intensity for 13 weeks, but not 26. Q4 needs to be calmer. Fewer launches. More depth. More sleep.

View File

@@ -1,31 +0,0 @@
---
aliases: ["October 2025"]
Is A: Month
Created at: "2025-10-28"
Belongs to: "[[25q4]]"
Rating: "😐"
---
# October 2025
## Focus
- Begin [[25q4-laputa-v3]] development — AI chat panel, type creation, graph improvements
- Start [[25q4-2026-sponsors]] pipeline with [[person-matteo-cellini]]
- Settle into a sustainable Q4 rhythm after the intensity of Q3
## Highlights
- [[25q4-laputa-v3]] development started: AI chat panel prototype working — can ask questions about vault content and get contextual answers with linked references
- [[person-matteo-cellini]] began [[25q4-2026-sponsors]] outreach — 4 sponsors already in early conversations for Q1-Q2 2026
- Newsletter at 80k subscribers — growth continuing but I'm less obsessed with the number than I used to be
- Community reaching a self-sustaining rhythm — members helping each other without prompting, which is the whole point
- Cycling winding down for the season — 280 km this month, mostly easy rides, letting the body recover from Stelvio
- Read "Working in Public" by Nadia Eghbal and "The Mom Test" by Rob Fitzpatrick — both relevant to the community and Laputa product thinking
## Reflections
October was deliberately quieter than the previous three months, and I'm grateful for it. After Q3's intensity, I needed a month of sustained work at 70% capacity rather than another sprint at 110%. The Laputa v3 work has been the right kind of creative — exploratory, no deadlines, just building something cool. The AI chat panel started as a "what if" experiment and quickly became the most interesting feature I've built. Asking your vault "what did I write about technical debt last year?" and getting a contextual, linked answer feels like magic.
But I'm also feeling a low-grade anxiety about hitting [[2025-reach-85k-subscribers]] by year end. We're at 80k with two months to go. The math works — 2.5k/month is within our normal range — but it doesn't leave room for a bad month. The Black Friday campaign will be critical again.
The sponsor pipeline work is less exciting but equally important. [[person-matteo-cellini]] is already mapping out 2026, which means I need to make decisions about pricing, formats, and what we offer. The business is maturing in ways that require more strategic thinking and less reactive selling. That's a good problem to have, but it requires a different kind of energy.

View File

@@ -1,32 +0,0 @@
---
aliases: ["November 2025"]
Is A: Month
Created at: "2025-11-28"
Belongs to: "[[25q4]]"
Rating: "😄"
---
# November 2025
## Focus
- Push for [[25q4-reach-85k]] with Black Friday campaign
- Ship [[25q4-laputa-v3]] features — type creation UI, improved AI chat
- Begin [[25q4-financial-review]] — compile 2025 revenue and expense data
## Highlights
- Black Friday campaign launched: bundled ebook + premium annual subscription at 30% off — early results strong, 1,800 new subscribers in the first week
- Newsletter at 83.5k subscribers — on pace for [[25q4-reach-85k]] by mid-December
- [[25q4-laputa-v3]] type creation shipped — users (well, me) can now define custom types with properties directly in the app, no more editing YAML manually
- [[25q4-financial-review]] started: 2025 tracking to ~EUR 260k total revenue (sponsorships EUR 210k, premium EUR 35k, ebook EUR 15k)
- [[person-matteo-cellini]] closed 3 more [[25q4-2026-sponsors]] deals — Q1 2026 pipeline at EUR 24k+ MRR
- [[measure-podcast-downloads]] averaging 18k/month — consistent and growing
- AI chat panel refined: better context retrieval, source linking, faster responses — starting to share with a few trusted friends for feedback
## Reflections
The Black Friday campaign this year was more strategic than last year's. Instead of a pure discount play, we bundled the ebook with premium — giving people a reason to upgrade that goes beyond price. Early numbers suggest it's working better than last year's approach. [[person-paco-furiani]] set up the automation so cleanly that I barely had to touch it — sign-ups flowing in, welcome emails going out, access provisioning automatic. The team is genuinely running the machine now.
The financial review numbers are staggering to me. EUR 260k from what was, not that long ago, a side project newsletter. The breakdown is healthy too — sponsorships dominate but premium and the ebook are diversifying the revenue. If the 2026 sponsor pipeline closes as projected, we'll be looking at EUR 300k+ next year. That's "hire a full-time person" territory.
Laputa v3 is getting close to something I could show other people without apologizing for it. The type creation UI was the missing piece — non-technical users (hypothetical ones, since I'm still the only user) can now define their own taxonomy without touching YAML. [[2025-ship-laputa]] feels achievable. I've started sharing it with 3 friends who maintain large vaults. Their feedback has been encouraging: "this is what I wanted Obsidian to be." Music to my ears.

View File

@@ -1,34 +0,0 @@
---
aliases: ["December 2025"]
Is A: Month
Created at: "2025-12-28"
Belongs to: "[[25q4]]"
Rating: "😄"
---
# December 2025
## Focus
- Close the year: hit [[25q4-reach-85k]], finalize [[25q4-financial-review]], run [[25q4-year-review-2025]]
- Complete [[25q4-laputa-v3]] — prepare for closed beta in January
- Reflect, rest, and plan [[2025]] wrap-up
## Highlights
- Newsletter crossed 85k subscribers on December 11th — [[2025-reach-85k-subscribers]] achieved, exactly one year after hitting 50k
- [[25q4-financial-review]] finalized: 2025 total revenue EUR 262k, up 69% from 2024 — [[2025-reach-22k-mrr]] hit in November with MRR at EUR 22.3k
- [[25q4-year-review-2025]] completed: every 2025 goal either hit or exceeded — [[2025-reach-85k-subscribers]], [[2025-reach-22k-mrr]], [[2025-ride-stelvio]], [[2025-ship-laputa]] (beta), [[2025-read-20-books]] (21 books)
- [[25q4-laputa-v3]] beta-ready: 12 closed beta testers lined up for January, onboarding docs written, feedback channels set up
- [[person-matteo-cellini]] locked in 8 sponsors for [[25q4-2026-sponsors]] Q1-Q2 pipeline — projected MRR EUR 24.5k
- End-of-year podcast episode recorded: honest, vulnerable, grateful — the kind of episode that's hard to record and easy to listen to
- Read 3 books in December: finished at 21 for the year
## Reflections
Crossing 85k subscribers felt different from crossing 50k last year. Last year was euphoria — "I can't believe this is happening." This year was quieter — "of course it happened, look at the system we built." That shift from surprise to expectation is both a sign of maturity and a subtle loss. I miss the wonder a little. But I'll take sustainable confidence over periodic amazement.
The financial review was the most sobering exercise of the month. EUR 262k in revenue. A real team of four people. A product in beta. A published book. A keynote at LeadDev. Two years ago, I was a solo newsletter writer hoping to make rent from sponsorships. The speed of change is disorienting when you zoom out. In the day-to-day, it feels gradual. In the annual review, it feels dramatic.
Laputa going to beta testers in January is the thing I'm most excited about heading into 2026. Building software for yourself is satisfying, but it's also a trap — you optimize for your own quirks and blind spots. Having 12 other people use it will surface assumptions I didn't know I was making. Some of those assumptions will be wrong. That's the point.
[[2025]] was the year I stopped being a content creator with a side hustle and became a founder running a media company with a software product. Both descriptions are true, but the second one is the more accurate framing now. Looking ahead: more team, more product, more depth, less heroics. And maybe Ventoux next summer. We'll see.

View File

@@ -1,33 +0,0 @@
---
aliases: ["Reach €22k MRR"]
Is A: Goal
Belongs to: "[[2025]]"
Status: Open
---
# Reach €22k MRR
Growing sponsorship revenue to 22,000 EUR/month is the key financial milestone for 2025. Hitting this target would make Refactoring a comfortably profitable business capable of supporting a team of 3-4, funding product development like [[25q1-laputa-v1]], and providing the financial runway to experiment with new revenue lines such as community or premium offerings.
## Success criteria
- Reach 22,000 EUR MRR by December 2025
- Maintain average deal size above 1,400 EUR per placement
- Grow sponsor roster to 15+ active sponsors (up from ~10)
- Achieve 85%+ renewal rate on existing contracts
- Keep [[measure-close-rate]] above 35% on inbound leads
## Key milestones
- Start the year at 15k MRR baseline (carry-over from 2024 contracts)
- Launch updated sponsorship packages with audience segmentation data in [[25q1]]
- Cross 18k MRR by mid-year through new sponsor acquisition
- Introduce premium placement options tied to [[25q2-reach-70k]] subscriber milestone
- Close Q4 at 22k MRR, with strong pipeline for 2026
## Notes
- Currently tracking at approximately 19k MRR as of Q3. The gap to 22k is closable but requires 2-3 new sponsors or upsells in Q4.
- Subscriber growth ([[2025-reach-85k-subscribers]]) is the enabler. Every 10k subscriber increase unlocks ~1,500 EUR in pricing power.
- The [[25q1-referral-program]] is driving higher-quality subscribers, which improves engagement metrics that sponsors care about (open rate, click rate).
- Risk: if [[measure-subscribers]] growth stalls, pricing increases become harder to justify. Revenue and audience growth are tightly coupled.
- The community experiment ([[25q3-discord-community-soft]]) could become a supplemental revenue line, but is not included in the 22k MRR target to keep it focused on sponsorships.

View File

@@ -1,33 +0,0 @@
---
aliases: ["Reach 85k Subscribers"]
Is A: Goal
Belongs to: "[[2025]]"
Status: Open
---
# Reach 85k Subscribers
Reaching 85,000 subscribers by end of 2025 is the primary growth target for the year. This represents roughly 60% growth over the 53k base from end of 2024 and would position Refactoring as one of the largest independent engineering newsletters globally. Audience scale is the engine that drives [[measure-sponsorship-mrr]], enables premium pricing for [[2025-reach-22k-mrr]], and provides the distribution base for any future product launches.
## Success criteria
- End 2025 with 85,000+ total newsletter subscribers
- Maintain net monthly growth of 2,500+ subscribers
- Keep unsubscribe rate below 1.3% per issue
- Maintain [[measure-open-rate]] above 43% at scale
- Diversify acquisition channels: no single channel should account for more than 40% of new subscribers
## Key milestones
- Launch [[25q1-newsletter-seo-sprint]] to build a long-term organic acquisition engine
- Launch [[25q1-referral-program]] to drive word-of-mouth growth
- Cross 65,000 by end of Q1 (strong start to the year)
- Reach 70,000 by mid-year through [[25q2-reach-70k]] push
- Hit 85,000 by December with sustained multi-channel growth through [[25q4-reach-85k]]
## Notes
- Currently at approximately 75,000 subscribers as of Q3. On track but the pace needs to hold through Q4.
- The [[25q1-referral-program]] has been the strongest new channel in 2025, accounting for ~20% of new subscribers. Referral subscribers also have higher engagement.
- SEO from the [[25q1-newsletter-seo-sprint]] is a slow burn. Articles are ranking but take 3-6 months to reach full traffic potential. Expect the biggest impact in Q4.
- Twitter/X remains the largest single channel at ~35%, close to the 40% cap. Need to continue diversifying.
- Key risk: list quality at scale. As the audience grows, maintaining [[measure-open-rate]] above 43% requires active list hygiene and consistently high content quality from [[responsibility-content-production]].

View File

@@ -1,33 +0,0 @@
---
aliases: ["Read 20 Books in 2025"]
Is A: Goal
Belongs to: "[[2025]]"
Status: Open
---
# Read 20 Books in 2025
Reading remains the highest-leverage input for content quality and original thinking. The 2025 target of 20 books is calibrated to be ambitious but realistic based on the [[2024-read-24-books]] experience, where the 24-book target was missed at 18. The adjusted goal accounts for the increasing demands of a growing business while preserving [[responsibility-learning]] as a non-negotiable priority.
## Success criteria
- Read 20 books by December 31, 2025
- Maintain at least 1 book per month, even in the busiest months
- Track monthly progress via [[measure-books-per-month]]
- At least 60% should be non-fiction relevant to content, engineering, or business
- Create at least 3 newsletter essay ideas per quarter sourced from reading
## Key milestones
- Read 5 books by end of Q1 to build early momentum
- Integrate audiobooks into cycling training to boost volume (learned from 2024)
- Reach 10 books by mid-year
- Maintain pace through the busy Q3 podcast and community launch season
- Close the year at 20 by protecting reading time in Q4
## Notes
- Currently at 14 books as of Q3. On pace to hit 20, but Q4 needs to deliver 6 books which matches the best quarterly performance in 2024.
- Audiobooks during cycling continue to be the most reliable reading channel. 8 of the 14 books so far were consumed via audio during rides.
- The shift to 60% non-fiction is working well. Recent reads on platform economics and community building directly informed the [[25q3-community-launch]] strategy.
- The biggest risk to this goal is the Q4 crunch: [[25q4-reach-85k]] and year-end sponsor renewals tend to consume all available bandwidth.
- Linking reading notes to [[measure-evergreen-notes-created]] helps ensure books translate into lasting knowledge rather than forgotten highlights.

View File

@@ -1,34 +0,0 @@
---
aliases: ["Ride the Stelvio Pass"]
Is A: Goal
Belongs to: "[[2025]]"
Status: Done
---
# Ride the Stelvio Pass
The Stelvio Pass is one of the most iconic climbs in professional cycling: 24.3 km at 7.4% average gradient, reaching 2,758 meters of elevation. Completing a full ascent has been a bucket-list goal for years and represents the intersection of [[responsibility-health-fitness]] and personal ambition. After successfully completing two gran fondos in 2024 ([[2024-complete-two-gran-fondos]]), the Stelvio was the natural next challenge.
## Success criteria
- Complete a full ascent of the Stelvio Pass from Prato allo Stelvio (the classic east side)
- Finish under 2 hours 15 minutes
- No mechanical issues or health incidents during the climb
- Maintain [[measure-cycling-km-per-month]] above 500 km during the 3-month build phase
- Sustain [[measure-resting-hr]] below 52 bpm during peak training
## Key milestones
- Begin structured climbing-specific training block in March 2025
- Complete at least 3 alpine climbs of 1,500+ meters elevation gain as preparation rides by May
- Ride the Stelvio in June or July, weather permitting
- Document the ride for a potential newsletter essay on [[topic-cycling-training]]
- Recover and return to baseline training volume within 2 weeks
## Notes
- Completed the Stelvio in late June 2025 in 2 hours 8 minutes. Under the 2:15 target by 7 minutes.
- The climbing-specific training block made a significant difference. Focused on sustained threshold efforts and long climbs in the Dolomites during April-June.
- [[measure-resting-hr]] averaged 50 bpm during peak training, indicating good aerobic adaptation.
- Weather was perfect on the day. Started early (6:30 AM) to avoid afternoon traffic and wind.
- Wrote a newsletter essay about the experience that became one of the most-engaged issues of the year. Personal stories consistently outperform tactical content.
- This was the most personally meaningful goal completed in 2025. The physical challenge is a reminder that capacity is often self-limited.

View File

@@ -1,34 +0,0 @@
---
aliases: ["Ship Laputa App"]
Is A: Goal
Belongs to: "[[2025]]"
Status: Done
---
# Ship Laputa App
Laputa is a personal knowledge and life management desktop app designed to replace the patchwork of tools currently used to manage the vault of ~9,200 markdown files that power content production, goal tracking, and personal operations. Shipping a usable v1 and adopting it as the daily driver is both a product goal and an infrastructure investment: a better tool for managing knowledge directly improves output quality for [[responsibility-content-production]] and operational clarity across all responsibilities.
## Success criteria
- Ship Laputa v1 as a functional Tauri desktop app with four-panel UI
- Support reading and editing markdown files with YAML frontmatter
- Implement vault navigation, search, and filtering by type
- Use Laputa as the primary daily knowledge management tool (replace current workflow)
- Achieve stable performance with the full ~9,200 file vault
## Key milestones
- Build the core Tauri + React architecture and file I/O layer during [[25q1-laputa-v1]]
- Implement the four-panel UI (sidebar, list, editor, properties) by end of Q1
- Add frontmatter parsing, type filtering, and basic search by mid-Q2
- Iterate on editor experience (CodeMirror 6 integration) during [[25q2-laputa-v2]]
- Reach daily-driver status by end of Q2 and sustain through Q3-Q4
## Notes
- Laputa v1 shipped in March 2025 and has been the daily driver since April. The app handles the full vault without performance issues.
- The Tauri v2 + React + TypeScript stack was the right choice. Desktop performance is excellent and the Rust backend handles file I/O efficiently even at scale.
- The mock layer (`src/mock-tauri.ts`) proved invaluable for rapid UI iteration without needing the full backend running.
- CodeMirror 6 integration was the most complex part of the frontend. Live preview with reveal-on-focus required significant customization.
- [[25q2-laputa-v2]] added refinements: better search, improved frontmatter editing, and the AI chat panel.
- Key lesson: building your own tools is high-leverage when you are the primary user. Every improvement to Laputa directly improves daily workflow efficiency across [[responsibility-content-production]], [[responsibility-personal-finance]], and [[responsibility-learning]].

View File

@@ -1,51 +0,0 @@
---
aliases: ["2025"]
Is A: Year
Created at: "2025-01-01"
Has: ["[[25q1]]", "[[25q2]]", "[[25q3]]", "[[25q4]]"]
---
# 2025
## Theme
2025 is the scale year — the year the question shifted from "can this work?" to "how big can this get?" Coming off the momentum of [[2024]], every metric pointed upward: subscribers, revenue, podcast downloads, team capacity. The goal wasn't just to grow more, but to grow *better* — building systems and a team that could sustain 85k subscribers, EUR 22k MRR, and a product (Laputa) without everything depending on me.
But scaling surfaced a tension I hadn't anticipated: the more successful Refactoring becomes, the more it pulls me away from the craft that made it successful in the first place — writing. Managing sponsors, coordinating a team, shipping a product, speaking at conferences — these are all valuable, but they're not writing. The creative struggle of 2025 has been protecting the space to think deeply and write well, while everything else demands attention. Some months I won that battle. Others, I didn't.
## Highlights
- Newsletter grew from 50k to 82k+ subscribers (on track for [[2025-reach-85k-subscribers]] by year end)
- [[measure-sponsorship-mrr]] grew from EUR 11.4k to EUR 20.1k by end of Q3, tracking toward [[2025-reach-22k-mrr]]
- Shipped three versions of Laputa: [[25q1-laputa-v1]], [[25q2-laputa-v2]], [[25q4-laputa-v3]] — from internal prototype to closed beta candidate ([[2025-ship-laputa]])
- Podcast grew to 18k downloads/month across Seasons 3 and 4 ([[25q2-podcast-season-3]], [[25q3-podcast-season-4]])
- Published [[25q3-ebook]] "The Engineering Leader's Playbook" — 1,400+ copies sold in the first month
- Launched [[25q3-community-launch]] with 850 founding members
- Rode Stelvio from Bormio in 1h52m — [[2025-ride-stelvio]] achieved
- Keynote at [[25q3-leaddev-london]] to 1,200 attendees — biggest speaking engagement to date
- [[25q2-team-retreat]] in Tuscany aligned the team for H2 and catalyzed the community idea
- Executed [[25q1-rate-increase]] — sponsor rates up 25%, retention at 90%
## By the numbers
- **Subscribers**: 50.2k to ~82k (projected 85k by Dec) — +64% YoY
- **Sponsorship MRR**: EUR 11.4k to EUR 20.1k (Q3 close) — targeting EUR 22k by year end
- **Podcast downloads**: 8.2k/month to 18k/month — +120% YoY
- **Premium subscribers**: 320 to 680+ — +112% YoY
- **Ebook sales**: 1,400+ copies (launched Q3)
- **Community members**: 850 (launched Q3)
- **Books read**: on track for [[2025-read-20-books]] (16 through Q3)
- **Cycling km**: ~5,800 km through Q3, projected 7,500+ for year — [[measure-cycling-km-per-month]]
- **Stelvio**: 1h52m from Bormio — personal milestone
- **Conference talks**: 2 (LeadDev London keynote, plus a workshop)
- **Total revenue**: projected ~EUR 260k (+68% vs 2024)
- **Resting HR**: [[measure-resting-hr]] averaged 48 bpm, down from 52 bpm in 2024
## Reflections
The most important thing I did in 2025 was hire well and delegate aggressively. [[person-matteo-cellini]] now runs the entire sponsor relationship lifecycle — from prospecting to renewal — and does it better than I did. [[person-paco-furiani]] owns operations, invoicing, and now coordinates the community logistics. [[person-sara-ricci]] handles podcast production and guest coordination. For the first time, I can take a week off and nothing breaks. That's the real milestone, even if it doesn't show up in the metrics.
The ebook and community launches in Q3 represented a philosophical shift. For three years, Refactoring was a one-to-many broadcast channel: I write, you read. The community made it many-to-many. Engineering leaders helping each other, sharing war stories, giving feedback on each other's challenges. I'm a facilitator now, not just a broadcaster. It's a better model, but it requires a different kind of energy — more listening, less performing.
Laputa has been the most surprising journey of the year. What started as a weekend hack in Q4 2024 has become a serious product with three major versions shipped. Using it daily to manage my 9,000+ file vault has been the best kind of dogfooding — every friction point I hit becomes a feature. The AI chat panel in v3 was a breakthrough moment: asking your own vault questions and getting contextual, linked answers feels like the future of personal knowledge management. Whether this becomes a real product with real users or stays a personal tool, building it has made me a better engineer and a better thinker.
If I'm being honest, Q3 nearly broke me. Three major launches, a keynote, and peak cycling training in the same 13 weeks was too much. I hit a wall in late August — low energy, poor sleep, short temper. [[person-paco-furiani]] noticed before I did. I took 10 days fully off in early September and came back feeling human again. The lesson is one I keep having to relearn: ambition without recovery is just a burnout timeline. Going into Q4 and planning for 2026, I'm trying to build more white space into the calendar. Fewer launches per quarter. More weeks with nothing scheduled. The business can handle it — the question is whether I can resist the urge to fill every gap with a new project. History suggests I can't, but I'm working on it.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Launch Sponsorship Packages"]
Is A: Project
Belongs to: "[[24q1]]"
Advances: "[[responsibility-sponsorships]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Launch Sponsorship Packages
## Overview
This project formalized Refactoring's monetization through sponsorships by creating structured packages that sponsors could evaluate and purchase. Before this, sponsorship deals were ad-hoc and inconsistent — pricing varied per deal, deliverables were loosely defined, and there was no media kit to send prospects.
The goal was to create three clear tiers (Logo, Spotlight, and Deep Dive) with fixed pricing, well-defined deliverables, and a professional media kit PDF. This would make outreach scalable and give [[person-luca-rossi]] a repeatable process instead of negotiating every deal from scratch. The project directly supported [[2024-double-revenue]] by establishing the revenue engine for the year.
## Goals
- Define three sponsorship tiers with clear deliverables and pricing (Logo: $500, Spotlight: $1,200, Deep Dive: $2,500)
- Create a professional media kit PDF with audience demographics, open rates, and testimonials
- Set up a Stripe billing workflow for recurring sponsors
- Draft email templates for outreach, follow-up, and renewal
- Close the first 3 paying sponsors before end of [[24q1]]
## Key decisions
- **Three tiers, not two or four.** Considered a simpler two-tier model, but having three gives sponsors a clear upgrade path. The Logo tier serves as a low-friction entry point.
- **Monthly pricing, not per-issue.** Charging monthly (4 issues) simplifies invoicing and gives sponsors more predictable budgets. This also aligns with how [[measure-sponsorship-mrr]] is tracked.
- **No exclusivity clauses.** Decided against offering category exclusivity because the newsletter audience is too broad to guarantee meaningful exclusivity, and it would limit revenue potential.
## Notes
- The media kit took longer than expected — gathering accurate audience data required exporting from Substack and ConvertKit, which have inconsistent analytics. Settled on a conservative subscriber count to maintain credibility.
- [[person-matteo-cellini]] provided useful feedback on the pricing deck layout. His experience with B2B sales helped shape the value proposition framing.
- First three sponsors came from warm outreach to developer tool companies who had previously engaged with newsletter content. Cold outreach conversion was near zero at this stage.
- This project established the foundation for [[procedure-sponsor-onboarding]] and [[procedure-quarterly-sponsor-outreach]], which were formalized later in [[24q2]].

View File

@@ -1,36 +0,0 @@
---
aliases: ["Plan 2024 Cycling Season"]
Is A: Project
Belongs to: "[[24q1]]"
Advances: "[[responsibility-health-fitness]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Plan 2024 Cycling Season
## Overview
This project laid out the full 2024 cycling calendar — target events, training blocks, rest weeks, and equipment needs. The main objective was to complete two gran fondos in 2024, supporting [[2024-complete-two-gran-fondos]], while maintaining a sustainable training load that would not interfere with work commitments.
Planning happened in January and February so that structured training could begin in March. This included selecting target events, mapping out a periodized training plan (base, build, peak, recovery), and identifying any gear upgrades needed before the season. [[person-paco-furiani]] helped review the training plan and suggested adjustments based on his own racing experience.
## Goals
- Select two target gran fondos for 2024 (spring and autumn)
- Design a 24-week periodized training plan with base, build, and peak phases
- Establish weekly volume targets: 8-12 hours per week during build phase
- Budget and plan equipment upgrades (new wheelset, tire strategy for events)
- Set up tracking in Strava and TrainingPeaks for [[measure-cycling-km-per-month]]
## Key decisions
- **Granfondo di Varese (May) and Granfondo dell'Appennino (September)** selected as the two target events. Varese was chosen for its accessibility and moderate difficulty; Appennino for its challenging profile that would test peak fitness.
- **Polarized training model** rather than threshold-heavy. After reading several studies on amateur endurance performance, decided that 80/20 (easy/hard) distribution would be more sustainable alongside a demanding work schedule.
- **No coach for now.** Considered hiring a cycling coach but decided to self-coach for 2024 using structured plans from TrainingPeaks. If results are good, revisit for 2025.
## Notes
- The biggest risk to the plan was always going to be consistency — weeks with heavy newsletter deadlines or sponsor calls tend to eat into training time. Built in buffer weeks to account for this.
- Realized during planning that the winter base phase was already partially missed. Adjusted by extending the base phase by two weeks and compressing the first build block slightly.
- Equipment decision: went with a new set of carbon wheels (Campagnolo Bora WTO 45) rather than upgrading the frame. Better bang for the buck in terms of performance gain per euro.
- This plan feeds directly into [[24q2-spring-gran-fondo]] for the first event execution and into [[topic-cycling-training]] for ongoing training notes.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Podcast Season 1 Launch"]
Is A: Project
Belongs to: "[[24q1]]"
Advances: "[[responsibility-podcast]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Podcast Season 1 Launch
## Overview
This project launched the Refactoring podcast — a long-form interview show focused on engineering culture, technical leadership, and the human side of building software. The idea had been brewing for months, but the decision to actually ship was driven by [[2024-launch-podcast]] as a key goal for the year.
The strategy was to batch-record 6 episodes before publishing anything, so that the launch would have a backlog and a consistent weekly cadence from day one. This meant spending most of January and February on guest outreach, recording, and editing, with the public launch in mid-March. [[person-sara-ricci]] joined as editor toward the end of the project to handle post-production, which proved essential for maintaining quality while keeping up with the newsletter schedule.
## Goals
- Record 6 episodes before public launch (buffer for consistent weekly releases)
- Secure guests with strong engineering leadership backgrounds (CTOs, VPEs, Staff+ engineers)
- Set up podcast hosting, RSS feed, and distribution to Apple Podcasts, Spotify, and YouTube
- Design cover art and episode template graphics
- Establish [[procedure-podcast-recording]] and [[procedure-podcast-editing]] workflows
## Key decisions
- **Interview format, not solo.** Considered doing solo episodes or co-hosted commentary, but interviews leverage guest audiences for cross-promotion and are more engaging for a new show with no existing listener base.
- **Season model, not continuous.** Adopted a seasonal structure (8-10 episodes per season) with breaks between seasons. This prevents burnout and allows time to plan each season's theme. Season 1 theme: "Engineering Culture."
- **Audio-first, video as bonus.** Recorded video but optimized for audio quality. Video clips are used for social promotion, but the primary distribution is audio podcasts. This reduced production overhead significantly.
## Notes
- Guest booking was the hardest part. Sent about 40 outreach emails to get 8 confirmed guests (6 recorded, 2 cancelled). Cold outreach to high-profile CTOs had about a 5% response rate. Warm intros from [[person-matteo-cellini]] and [[person-david-kim]] were far more effective.
- The first two episodes had noticeable audio quality issues — learned the hard way that remote recordings via Zoom are not sufficient. Switched to Riverside.fm for episodes 3-6, which was a significant improvement.
- Launch week saw about 1,200 downloads across the first 3 episodes, which was above the initial target of 500. The newsletter cross-promotion was the primary driver — [[measure-podcast-downloads]] tracked closely with newsletter send days.
- This project created the foundation for [[24q3-podcast-season-2]] and all subsequent seasons. The workflows established here, particularly [[procedure-podcast-recording]], have remained largely unchanged.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Redesign Newsletter Template"]
Is A: Project
Belongs to: "[[24q1]]"
Advances: "[[responsibility-content-production]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Redesign Newsletter Template
## Overview
The Refactoring newsletter template had been largely unchanged since launch — a plain text-heavy layout that worked at small scale but felt increasingly dated as the subscriber base grew. This project redesigned the email template for better readability, visual hierarchy, and sponsor placement. The redesign needed to improve the reading experience while also making sponsorship placements more visually distinct and valuable, supporting [[2024-double-revenue]].
The new template introduced a cleaner header, better typography spacing, a dedicated sponsor block with clear visual boundaries, and a structured footer with social links and podcast promotion. It was tested across major email clients (Gmail, Apple Mail, Outlook) and went through three iterations based on feedback from a small group of beta readers.
## Goals
- Redesign the email template with improved visual hierarchy and readability
- Create a dedicated, visually distinct sponsor placement section
- Ensure compatibility across Gmail, Apple Mail, Outlook, and mobile clients
- Improve click-through rate on inline links by at least 15%
- Establish the new template as the standard for [[procedure-weekly-newsletter]]
## Key decisions
- **Single-column layout.** Evaluated two-column layouts with sidebar content, but testing showed that single-column performs significantly better on mobile (where 65%+ of opens happen). Simplicity won.
- **Sponsor block above the fold.** Moved the sponsor placement from mid-article to just below the header intro. This increased sponsor visibility without feeling intrusive, since the block is clearly labeled and visually separated. Sponsors noticed and appreciated the change.
- **No images in the main body.** Kept the template text-focused with minimal imagery. Images in email are unreliable (blocked by many clients), increase load time, and distract from the content. The only image is the sponsor logo.
## Notes
- The biggest surprise was how much email client rendering varies. A layout that looked perfect in Gmail was broken in Outlook. Ended up using a very conservative CSS approach with inline styles and table-based layout for maximum compatibility.
- [[person-sara-ricci]] helped with copy feedback on the new footer and CTA sections. Her editorial eye caught several awkward phrasings in the template boilerplate.
- A/B tested the new template against the old one over two issues. The new template showed a 22% increase in click-through rate, primarily driven by better link placement and the structured "further reading" section at the bottom.
- [[measure-open-rate]] remained stable through the transition, confirming that the redesign did not trigger spam filters or cause deliverability issues. This was a real concern given how sensitive email infrastructure is to template changes.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Set Up Investing Framework"]
Is A: Project
Belongs to: "[[24q1]]"
Advances: "[[responsibility-personal-finance]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Set Up Investing Framework
## Overview
As Refactoring's revenue grew, it became clear that personal finance needed a more structured approach. This project defined a personal investment policy statement (IPS) and set up automated monthly contributions to a diversified portfolio of index funds. The goal was to remove emotion and ad-hoc decision-making from investing and replace it with a simple, repeatable system.
Before this project, savings were sitting in a low-yield savings account with no clear allocation strategy. The framework needed to be simple enough to maintain alongside a busy content business, while being sophisticated enough to actually build long-term wealth. Research phase took about three weeks, followed by account setup and automation.
## Goals
- Draft a personal Investment Policy Statement defining asset allocation, risk tolerance, and rebalancing rules
- Open a brokerage account with a low-cost provider (selected Degiro for European access and low fees)
- Set up automated monthly contributions (20% of net monthly revenue)
- Define a target allocation: 70% global equities (VWCE), 20% bonds (VAGF), 10% cash reserve
- Create a quarterly review checklist to assess allocation drift and rebalance if needed
## Key decisions
- **Passive index funds, not individual stocks.** After extensive reading (Bogle, Bernstein, Housel), decided that passive investing is the only approach that makes sense for someone whose primary income comes from a content business. Time spent stock-picking is time not spent on the newsletter.
- **Single accumulating ETF for equities (VWCE).** Rather than splitting across US, European, and emerging market ETFs, chose a single all-world accumulating ETF. Simplicity reduces friction and the urge to tinker.
- **20% of revenue, not a fixed amount.** Tying contributions to revenue percentage means the system scales with business growth and naturally reduces in leaner months without requiring manual adjustment.
## Notes
- The hardest part was actually committing to a framework and stopping the research phase. There is an infinite amount of personal finance content online, and it is easy to fall into analysis paralysis. Set a hard deadline of February 15 to finalize the IPS and stop reading.
- [[person-marco-bianchi]] shared his own investment framework, which was helpful for validating the approach. His suggestion to keep a separate 6-month emergency fund in a high-yield savings account was incorporated.
- The [[24q2-stock-screener]] experiment later in the year was a brief detour into individual stock analysis, but it reinforced the decision to stick with passive investing. The time spent did not justify the marginal (and uncertain) returns.
- Quarterly reviews are tracked as part of [[responsibility-personal-finance]]. The first two reviews in 2024 showed minimal drift, confirming that the set-and-forget approach works as intended.

View File

@@ -1,31 +0,0 @@
---
aliases: ["Q1 2024"]
Is A: Quarter
Created at: "2024-01-01"
Belongs to: "[[2024]]"
Has: ["[[24q1-launch-sponsorship-packages]]", "[[24q1-redesign-newsletter-template]]", "[[24q1-plan-cycling-season]]", "[[24q1-set-investing-framework]]", "[[24q1-podcast-season-1]]"]
Status: Done
---
# Q1 2024
## Focus
The first quarter of 2024 was about laying foundations. After a strong 2023, the goal was to professionalize the business side of Refactoring — move from ad-hoc sponsor deals to structured packages, refresh the newsletter design, and finally launch the podcast that had been in planning for months. On the personal side, I wanted to get the cycling season mapped out early and put a real investing framework in place.
## Highlights
- Launched [[24q1-launch-sponsorship-packages]] with three tiers (Gold, Silver, Bronze) — immediately booked two Gold sponsors for Q2
- Shipped [[24q1-redesign-newsletter-template]] with a cleaner layout, better mobile rendering, and improved CTA placement — [[measure-open-rate]] ticked up ~1.5pp
- Recorded and released the first four episodes of [[24q1-podcast-season-1]], crossing 2k downloads in the first month
- Mapped out the full [[24q1-plan-cycling-season]] including target events: Nove Colli in May and Maratona dles Dolomites in July (goal: [[2024-complete-two-gran-fondos]])
- Built out [[24q1-set-investing-framework]] — monthly DCA into a global ETF portfolio, emergency fund topped up
- Newsletter crossed 37k subscribers by end of March, up from ~35k at year start
- [[person-matteo-cellini]] closed the first two sponsor deals under the new packages within weeks of launch
## Reflections
Q1 felt like the quarter where things started clicking into place on the business side. The sponsorship packages were long overdue — I'd been doing custom deals for every sponsor, which was eating time and leaving money on the table. Having a clear menu made everything faster: [[person-matteo-cellini]] could sell without looping me in on every detail, and sponsors appreciated the transparency.
The podcast launch was nerve-wracking. I'd been procrastinating on it for almost a year, finding reasons to delay. But once the first episode went live and the feedback started coming in, I wondered why I'd waited so long. The format — 20-minute deep dives on one engineering leadership topic — resonated immediately. Production quality was rough in the first couple of episodes, but it improved fast.
On the personal front, planning the cycling season early was a game changer. Having Nove Colli and Maratona on the calendar gave structure to my training weeks. The investing framework was less exciting but equally important — automating contributions meant I could stop thinking about it and focus on building the business. All in all, a solid start to [[2024]].

View File

@@ -1,36 +0,0 @@
---
aliases: ["Write 10 Pillar Articles"]
Is A: Project
Belongs to: "[[24q2]]"
Advances: "[[responsibility-content-production]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Write 10 Pillar Articles
## Overview
This project focused on creating 10 long-form "pillar" articles — comprehensive, SEO-optimized pieces targeting high-traffic search terms in the engineering leadership and developer productivity space. These articles serve a dual purpose: they drive organic search traffic to the Refactoring website (feeding [[measure-subscribers]] growth) and establish topical authority that supports [[2024-reach-50k-subscribers]].
Each article was 2,500-4,000 words, thoroughly researched, and structured for both readability and search engine performance. Topics were selected based on keyword research using Ahrefs, focusing on terms with decent search volume (1K-10K monthly) and moderate competition. The articles were published on the Refactoring blog and cross-promoted through the newsletter over [[24q2]].
## Goals
- Research and select 10 high-value keyword targets using Ahrefs
- Write 10 articles of 2,500-4,000 words each, with clear structure and internal linking
- Optimize each article for on-page SEO (meta descriptions, headers, image alt text)
- Publish on a bi-weekly cadence throughout Q2
- Achieve first-page Google ranking for at least 3 target keywords within 6 months
## Key decisions
- **Evergreen topics only.** All 10 articles target topics that will remain relevant for 2+ years (e.g., "how to run effective 1:1s," "staff engineer vs. engineering manager"). Avoided trending or news-driven topics that would decay quickly.
- **Newsletter-first drafts, then expanded for SEO.** Several articles started as popular newsletter issues that were expanded with additional depth, examples, and structure. This reduced research time and ensured the topics had proven audience interest.
- **No freelance writers.** Considered outsourcing some articles but decided that the Refactoring voice is too distinctive to delegate at this stage. [[person-sara-ricci]] edited all 10 pieces, which maintained quality while freeing time for drafting.
## Notes
- Writing 10 pillar articles in one quarter while maintaining the weekly newsletter was extremely demanding. Weeks 5-8 were particularly brutal — the output quality dipped on newsletter issues during that period. In hindsight, 7-8 articles would have been more sustainable.
- The article on "Engineering Manager vs. Tech Lead" became the highest-performing piece, ranking on page 1 within 8 weeks and driving ~3,000 monthly organic visits. This single article accounted for about 30% of the total organic traffic from all 10 pieces.
- Internal linking between pillar articles and existing newsletter content created a noticeable boost in site-wide SEO metrics. Average session duration increased by 40% during Q2.
- This project directly informed [[topic-content-strategy]] and established the content pillar framework that was later extended in [[25q1-newsletter-seo-sprint]].

View File

@@ -1,36 +0,0 @@
---
aliases: ["Build Podcast Landing Page"]
Is A: Project
Belongs to: "[[24q2]]"
Advances: "[[responsibility-podcast]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Build Podcast Landing Page
## Overview
After launching the podcast in [[24q1-podcast-season-1]], it became clear that there was no proper home for the show on the web. Episodes were scattered across Apple Podcasts, Spotify, and YouTube, but there was no single page where listeners could browse the archive, read show notes, and subscribe. This project created a dedicated landing page on the Refactoring website to serve as the podcast's canonical home.
The page needed to work both as a discovery tool for new listeners and as a reference for existing ones. It also needed to support sponsor visibility, since podcast sponsorships were part of the package tiers defined in [[24q1-launch-sponsorship-packages]]. The page was built using the existing site's tech stack (Next.js) and went live in early May.
## Goals
- Design and build a podcast landing page with episode archive, show notes, and embedded audio player
- Include subscribe CTAs for Apple Podcasts, Spotify, YouTube, and RSS
- Add a "Featured Guest" section to highlight notable episodes and drive social proof
- Integrate sponsor logos and links for current podcast sponsors
- Ensure the page is mobile-responsive and loads quickly (target: <2s LCP)
## Key decisions
- **Static generation, not dynamic.** Each episode page is statically generated at build time from a markdown file with frontmatter metadata. This keeps hosting costs zero (Vercel free tier) and ensures fast page loads. New episodes trigger a rebuild via a GitHub webhook.
- **Embedded player, not custom.** Used the native Spotify/Apple embed widgets rather than building a custom audio player. The maintenance cost of a custom player was not justified given that most listeners use their preferred podcast app anyway.
- **Show notes as full articles.** Rather than just listing bullet points, each episode's show notes page is a full article with key takeaways, timestamps, and links. This gives the pages SEO value and provides genuine utility for listeners who prefer reading to listening.
## Notes
- The page design went through two iterations. The first version was too sparse — just a list of episodes with play buttons. [[person-giulia-conti]] suggested adding guest photos and pull quotes, which made the page significantly more engaging.
- Getting the embedded players to render correctly across browsers was more annoying than expected. Spotify's embed widget has inconsistent height behavior on Safari. Ended up using a fixed-height iframe with a fallback link.
- The landing page became the default link shared in the newsletter for podcast promotion, replacing direct Spotify/Apple links. This centralized analytics tracking through [[measure-podcast-downloads]] and gave better visibility into listener behavior.
- Page traffic is modest (~500 unique visitors/month) but steady, and it ranks for several long-tail podcast-related keywords. The SEO investment in show notes is paying off gradually.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Hire Editor (Sara)"]
Is A: Project
Belongs to: "[[24q2]]"
Advances: "[[responsibility-team-management]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Hire Editor (Sara)
## Overview
By mid-2024, writing the weekly newsletter, managing sponsorships, recording podcast episodes, and handling business operations was becoming unsustainable as a solo operation. The most impactful hire at this stage was an editor — someone who could elevate the writing quality, reduce revision cycles, and free up time for higher-leverage activities like strategy and guest relationships.
After a lightweight hiring process, [[person-sara-ricci]] joined as a part-time editor in May 2024. Sara had previous experience editing technical content for developer publications, which was critical — engineering leadership content requires an editor who understands the domain, not just grammar. This hire directly supported scaling [[responsibility-content-production]] and was a prerequisite for hitting [[2024-reach-50k-subscribers]].
## Goals
- Define the editor role: scope, hours, compensation, and working relationship
- Source and evaluate 5-10 candidates through network referrals and job boards
- Run a paid trial edit with top 3 candidates using a real newsletter draft
- Onboard the selected editor with style guide, editorial calendar, and tooling access
- Achieve a steady-state workflow where drafts go through one editorial pass before publish
## Key decisions
- **Part-time contractor, not full-time employee.** The workload did not justify a full-time hire yet, and contractor status keeps the arrangement flexible as the business scales. Sara works approximately 15 hours/week.
- **Paid trial before commitment.** Rather than hiring based on interviews alone, asked the top 3 candidates to edit the same newsletter draft. This revealed huge differences in editorial judgment that interviews could not surface. Sara's edits were the most substantive — she restructured sections and challenged weak arguments, not just fixed typos.
- **Editor has veto power on publish readiness.** Gave Sara explicit authority to push back on drafts that are not ready. This was uncomfortable at first but has significantly improved quality. If Sara says it needs another pass, it gets another pass.
## Notes
- The hiring process took about 4 weeks from posting to signed contract. Received ~30 applications, shortlisted 8, did calls with 5, trial edits with 3. The trial edit step was by far the most valuable signal.
- Biggest adjustment was learning to write "editor-friendly" first drafts — meaning rough structure and arguments are solid, but prose is intentionally unpolished. Trying to write perfectly on the first pass and then having an editor change it creates friction. Better to write fast and let Sara shape the prose.
- Sara also took on podcast show notes editing, which was not in the original scope but made sense as her familiarity with the content grew. This expanded into the workflow documented in [[procedure-podcast-editing]].
- [[person-matteo-cellini]] recommended Sara — they had worked together at a previous company. Network referrals continue to be the best hiring channel for roles that require domain expertise.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Set Up Sponsor CRM"]
Is A: Project
Belongs to: "[[24q2]]"
Advances: "[[responsibility-sponsorships]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Set Up Sponsor CRM
## Overview
With sponsorship deals increasing after [[24q1-launch-sponsorship-packages]], tracking outreach, deal status, invoicing, and renewals in a spreadsheet was no longer viable. This project built a proper CRM system in Airtable to manage the full sponsor lifecycle — from initial outreach through booking, fulfillment, reporting, and renewal.
The CRM needed to support [[procedure-quarterly-sponsor-outreach]] and [[procedure-sponsor-onboarding]] while being lightweight enough for a one-person sales operation (with occasional help from [[person-matteo-cellini]] on outreach). The system went live in April 2024 and has been the backbone of sponsor management since, directly supporting [[2024-double-revenue]].
## Goals
- Design an Airtable base with tables for Companies, Contacts, Deals, Invoices, and Placements
- Build views for pipeline management (kanban), upcoming renewals, and revenue tracking
- Set up automations for deal stage notifications and renewal reminders (30 days before expiry)
- Import existing sponsor data from the old spreadsheet (~15 companies, ~25 deals)
- Document the CRM workflow and train [[person-matteo-cellini]] on outreach tracking
## Key decisions
- **Airtable, not a dedicated CRM tool.** Evaluated HubSpot, Pipedrive, and Notion. HubSpot was overkill (and expensive) for ~50 deals/year. Pipedrive was close but lacked the flexibility for tracking placement fulfillment. Airtable's combination of structured data, views, and automations hit the sweet spot for this scale.
- **Deal-centric model, not contact-centric.** The primary entity is the Deal (a specific sponsorship booking), not the Contact or Company. This reflects the actual workflow — most actions are about advancing a specific deal, not managing a relationship in the abstract.
- **Automated renewal reminders at 30 and 7 days.** Renewal is the highest-leverage moment in the sponsor lifecycle. Automating reminders ensures no renewal slips through the cracks, which was happening with the spreadsheet system.
## Notes
- Migrating from the spreadsheet exposed several data quality issues — duplicate companies, inconsistent naming, missing invoice dates. Cleaning this up took a full day but was worth it for a clean foundation.
- The kanban view for pipeline management became the default "daily check" for sponsor operations. Being able to see all deals by stage (Prospect, Outreach, Negotiation, Booked, Fulfilled, Renewed) at a glance changed how proactive the outreach process feels.
- Airtable's automation limits on the free plan were a constraint. Upgraded to the Team plan ($20/month) to get enough automation runs. The cost is trivial relative to the revenue it manages.
- [[measure-sponsorship-mrr]] is now calculated directly from the CRM data, which eliminated the manual reconciliation that was happening before. Revenue reporting went from a quarterly headache to an always-current dashboard.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Spring Gran Fondo 2024"]
Is A: Project
Belongs to: "[[24q2]]"
Advances: "[[responsibility-health-fitness]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Spring Gran Fondo 2024
## Overview
This was the first of two target events for 2024, as planned in [[24q1-plan-cycling-season]]. The Granfondo di Varese — 130km with 2,200m of elevation gain — served as both a fitness benchmark and a motivational milestone for the spring training block. The event was scheduled for late May, giving a solid 12-week build phase from March through mid-May.
Preparation included structured interval training (3 sessions/week), long weekend rides (4-5 hours), and two recon rides on sections of the actual course. The goal was not to race competitively but to complete the full route in under 5 hours and 30 minutes, maintaining good form throughout. This project directly advanced [[2024-complete-two-gran-fondos]].
## Goals
- Complete the Granfondo di Varese full route (130km, 2,200m elevation) in under 5:30
- Execute the 12-week build training plan with at least 80% adherence
- Complete two course recon rides to familiarize with key climbs and descents
- Maintain race weight (72kg) through the build phase
- Log all training data for post-event analysis via [[measure-cycling-km-per-month]]
## Key decisions
- **Full route, not the medio.** The event offered a 90km "medio" option. Chose the full 130km because the training plan was designed for it, and the shorter route would not provide a meaningful fitness test. If the legs failed on race day, it would be better to know that at full distance.
- **Conservative pacing strategy.** Rather than going hard on the first climb (which is where most amateurs blow up), planned to ride the first 60km at 75% of threshold power and increase effort in the second half. This negative-split approach is less exciting but far more reliable.
- **No travel the day before.** Drove to Varese two days before the event to avoid travel fatigue. Stayed at a simple hotel near the start line. The extra cost was worth the better sleep and relaxed morning.
## Notes
- Finished in 5:17, which was well within the target. The conservative pacing strategy worked perfectly — felt strong on the final climb while many riders around were walking. Negative split confirmed: second half was 3 minutes faster than the first.
- Training adherence was about 85%, which was above target. The two weeks lost to a mild cold in April were compensated by extending the build phase slightly and cutting one recovery week short.
- [[person-paco-furiani]] rode the event as well and finished about 20 minutes ahead. His feedback on nutrition strategy (eating every 30 minutes from the start, not waiting until hungry) was a game-changer. Applied the same approach going forward.
- Biggest lesson: the mental game matters more than fitness above a certain threshold. The climbs at km 90-110 were physically manageable but mentally draining. Having a clear pacing target on the bike computer helped maintain focus.
- Post-event recovery took about 10 days before training resumed. This feeds into preparation for the autumn event, which was later scoped as a separate project.

View File

@@ -1,35 +0,0 @@
---
aliases: ["Vibe-coding a Stock Screener"]
Is A: Experiment
Belongs to: "[[24q2]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Vibe-coding a Stock Screener
Built a stock screener in Python over a weekend to test whether a simple EMA bounce strategy could surface actionable trade setups on US equities. The broader motivation was to explore whether personal tooling for investment analysis could complement the [[procedure-monthly-portfolio-review]] and eventually improve [[measure-net-worth]] trajectory.
## Hypothesis
A lightweight, self-built screener using exponential moving average crossovers would surface 2-3 viable swing trade candidates per week, outperforming manual chart scanning in both speed and consistency.
## Setup
- Wrote a Python script pulling daily OHLCV data from Yahoo Finance for the S&P 500 universe.
- Implemented a 21/50 EMA bounce filter: flag stocks where price touches the 21 EMA from above, with the 50 EMA still trending up.
- Ran the screener nightly via cron for 4 weeks, logging all flagged tickers and tracking outcomes over 10-day windows.
- Compared results against manual chart review done for the same period.
## Results
- The screener flagged an average of 5 candidates per day (higher than expected).
- Roughly 40% of flagged setups produced a positive 10-day return above 2%.
- Manual chart review caught about 60% of the same setups, but took 3x longer.
- False positives were mostly in low-volume mid-caps where the bounce pattern was noise.
## Takeaways
- Vibe-coding a quick tool like this is a high-leverage weekend project. Total effort was about 8 hours.
- The EMA bounce strategy has signal, but needs volume and volatility filters to reduce false positives.
- Automating the screener freed up time previously spent on manual scanning for the [[procedure-monthly-portfolio-review]].
- Worth maintaining as a personal tool. Not worth productizing, but could become a newsletter content piece on [[topic-personal-finance]].

View File

@@ -1,36 +0,0 @@
---
aliases: ["Video Format Experiment"]
Is A: Experiment
Belongs to: "[[24q2]]"
Status: Abandoned
Owner: "[[person-luca-rossi]]"
---
# Video Format Experiment
Tested whether short-form video (3-5 minute explainers) could work as a distribution channel alongside the newsletter. The idea was to repurpose existing [[responsibility-content-production]] into video format to reach audiences who prefer visual content, potentially feeding [[measure-subscribers]] growth from a new channel.
## Hypothesis
Publishing 2 short technical explainer videos per week on YouTube and Twitter would generate at least 500 new newsletter subscribers over 6 weeks, with a production cost under 3 hours per video once a workflow was established.
## Setup
- Produced 3 videos covering topics already published as newsletter essays: system design patterns, engineering management tips, and a career growth framework.
- Used screen recording with voiceover (no face camera) to minimize production overhead.
- Published on YouTube Shorts and Twitter/X with a newsletter CTA in the description and pinned comment.
- Tracked view counts, click-through to newsletter signup, and time spent per video.
## Results
- Total views across 3 videos: ~4,200 (mostly Twitter, YouTube was negligible).
- Newsletter signups attributed to video: 23 total.
- Average production time: 5-6 hours per video (scripting, recording, editing, captioning).
- The time-to-subscriber ratio was roughly 15x worse than writing a newsletter essay.
## Takeaways
- Video production cost was significantly underestimated. Editing and captioning alone took 2+ hours per video.
- The audience overlap between short-form video viewers and newsletter subscribers appears small.
- Abandoned after 3 videos. The ROI does not justify the effort given current team size and [[responsibility-content-production]] bandwidth.
- If revisited, would need a dedicated video editor or a fundamentally simpler format (e.g., talking head with no editing).
- Better to double down on written content and [[topic-newsletter-growth]] strategies that already work.

View File

@@ -1,32 +0,0 @@
---
aliases: ["Q2 2024"]
Is A: Quarter
Created at: "2024-04-01"
Belongs to: "[[2024]]"
Has: ["[[24q2-hire-editor]]", "[[24q2-build-podcast-landing-page]]", "[[24q2-10-pillar-articles]]", "[[24q2-spring-gran-fondo]]", "[[24q2-sponsor-crm]]"]
Status: Done
---
# Q2 2024
## Focus
Q2 was the execution quarter. With the foundations from Q1 in place, the focus shifted to scaling content production and building systems that could run without me doing everything. Hiring an editor was the biggest unlock. On the cycling side, Nove Colli was the target — months of base training coming to a head.
## Highlights
- [[24q2-hire-editor]] completed — brought on a freelance editor who now handles first-pass editing on every newsletter issue, saving me 4-5 hours per week
- Published [[24q2-10-pillar-articles]]: long-form pieces on topics like "Staff Engineer vs. Manager" and "Technical Debt Prioritization" — two of them hit 50k+ views
- Built and launched [[24q2-build-podcast-landing-page]] with episode archive, show notes, and Apple/Spotify links — podcast downloads crossed 5k/month
- Completed Nove Colli ([[24q2-spring-gran-fondo]]) in 7h42m — not my best time, but finished strong on the climbs
- Shipped [[24q2-sponsor-crm]] in Notion — tracking pipeline, renewal dates, and revenue per sponsor in one place
- Newsletter hit 42k subscribers by end of June, right on track for the [[2024-reach-50k-subscribers]] goal
- [[person-paco-furiani]] started handling more operational tasks — invoicing, sponsor logistics, calendar management
- [[measure-sponsorship-mrr]] reached EUR 8.2k, up from EUR 5.5k at start of year
## Reflections
This was the quarter where I started to feel the leverage of having a small team. With [[person-matteo-cellini]] handling sponsor relationships and [[person-paco-furiani]] taking on operations, I could actually focus on writing and recording. The editor hire was transformative — I went from spending Sunday evenings frantically editing to having a polished draft waiting for me on Monday morning.
The pillar articles strategy paid off more than expected. Instead of chasing weekly topics, I invested in fewer, deeper pieces that could rank on search and get shared. "Staff Engineer vs. Manager" alone brought in 800+ new subscribers organically. This shifted my thinking about content strategy: depth over frequency.
Nove Colli was harder than I expected. The heat in Romagna in late May was brutal, and I bonked on the fourth climb. But crossing the finish line in Cesenatico — exhausted, sunburned, grinning — reminded me why I do this. Already looking forward to Maratona in July. The cycling and the business run on the same fuel: consistent effort over months, then showing up on race day. [[2024-complete-two-gran-fondos]] is halfway done.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Speak at Codemotion Milan"]
Is A: Project
Belongs to: "[[24q3]]"
Advances: "[[responsibility-content-production]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Speak at Codemotion Milan
## Overview
Codemotion Milan 2024 invited [[person-luca-rossi]] to deliver a 30-minute talk on growing a newsletter business as a technical founder. The talk, titled "From Side Project to 50K Subscribers: Building a Content Business in Public," covered the Refactoring growth story, key inflection points, and practical lessons for engineers considering content as a career path or side business.
This was the first major conference speaking engagement and represented an important step in building personal brand credibility beyond the newsletter audience. Conference talks drive a different kind of trust than written content — they put a face and voice to the brand, which strengthens both subscriber acquisition and sponsor confidence. The talk was well-received and led to several valuable connections, including two future podcast guests.
## Goals
- Write and rehearse a 30-minute talk with slides
- Target key themes: newsletter economics, audience building, and the engineering-to-content pipeline
- Deliver the talk at Codemotion Milan in October 2024
- Record the talk for repurposing as a YouTube video and newsletter content
- Generate at least 200 new newsletter subscribers from the event
## Key decisions
- **Story-driven structure, not a how-to.** Rather than a generic "10 tips for growing a newsletter" format, structured the talk as a chronological narrative of the Refactoring journey. Stories are more memorable and more authentic than advice lists.
- **Include real numbers.** Shared actual subscriber counts, revenue milestones, and open rates. Transparency is a core Refactoring value and it differentiates the talk from vague "I grew my audience" presentations. This decision was slightly uncomfortable but generated the most positive feedback.
- **No live demo.** Considered showing the newsletter creation process live but decided the risk of technical issues on stage was not worth it. Used screenshots and short video clips instead.
## Notes
- Preparation took about 3 weeks of part-time work — mostly slide design and rehearsal. Rehearsed the full talk 5 times, including twice in front of [[person-sara-ricci]] and [[person-matteo-cellini]] who gave tough but useful feedback on pacing and clarity.
- The audience was about 300 people, skewing toward mid-career developers and engineering managers. Q&A was lively — the most common question was about time management (how to write a weekly newsletter while also doing other work). This became a topic for a future newsletter issue.
- Post-talk networking led to meeting [[person-emma-wilson]] and [[person-david-kim]], both of whom later appeared as podcast guests in [[24q3-podcast-season-2]] and [[25q2-podcast-season-3]] respectively.
- The talk recording was published on YouTube and performed modestly (~2,500 views in 3 months), but the subscriber spike from the event itself was about 180 — slightly below the 200 target but still a strong result for a single event. The real value was in credibility and relationship building, not direct conversion.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Morning Journaling Habit"]
Is A: Experiment
Belongs to: "[[24q3]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Morning Journaling Habit
Ran an 8-week experiment with daily morning journaling to evaluate its impact on focus, decision-making clarity, and overall stress management. This ties into the broader theme of personal operating systems: if running a content business and training for cycling events simultaneously, mental bandwidth becomes the bottleneck, and journaling is often cited as a way to decompress and prioritize.
## Hypothesis
Writing 10-15 minutes each morning (freeform, no template) would reduce decision fatigue and improve weekly [[measure-task-completion-rate]] by at least 10%, while also providing a subjective sense of reduced stress.
## Setup
- Journaled every morning for 8 weeks using a plain markdown file in the vault.
- No rigid template: some days were gratitude lists, some were brain dumps, some were planning sessions.
- Tracked adherence (did I journal today? yes/no), subjective stress rating (1-5), and weekly task completion rate.
- Weeks 1-4 were during a high-stress period (podcast launch prep for [[24q3-podcast-season-2]] and newsletter deadlines). Weeks 5-8 were calmer.
## Results
- Adherence: 85% in weeks 1-4 (high stress), dropped to 55% in weeks 5-8 (low stress).
- Subjective stress rating improved by ~0.8 points on average during weeks 1-4.
- Task completion rate showed no statistically meaningful change (too many confounding variables).
- The most useful entries were "decision journals" where a specific choice was weighed explicitly.
## Takeaways
- Morning journaling is most valuable during high-stress, high-decision-load periods. It acts as a pressure valve.
- During calm stretches, the habit felt forced and adherence dropped naturally. This is fine; it does not need to be daily forever.
- Decision journals (structured: "What am I deciding? What are the options? What do I fear?") were the highest-signal format.
- Will keep journaling as a situational tool rather than a rigid daily habit. Likely to resurface during intense project sprints like [[25q1-laputa-v1]].
- No direct impact on [[measure-task-completion-rate]], but the qualitative clarity benefit is real.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Expand Sponsor Verticals"]
Is A: Project
Belongs to: "[[24q3]]"
Advances: "[[responsibility-sponsorships]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Expand Sponsor Verticals
## Overview
The first two quarters of sponsorship revenue came primarily from developer tools (CI/CD, monitoring, testing platforms). While this vertical was reliable, concentration risk was high — losing one or two devtools sponsors could significantly impact [[measure-sponsorship-mrr]]. This project expanded the sponsor base into three new verticals: cloud infrastructure, AI/ML platforms, and developer education.
Expanding verticals required adapting the outreach messaging and media kit to resonate with different buyer personas. Cloud infra companies care about enterprise decision-makers in the audience; AI/ML companies want to reach early adopters; education companies want ambitious individual developers. [[person-matteo-cellini]] led much of the outreach effort, using the CRM built in [[24q2-sponsor-crm]] to manage the pipeline. This project was critical for [[2024-double-revenue]].
## Goals
- Identify 30+ target companies across cloud infrastructure, AI/ML, and developer education verticals
- Adapt the media kit with vertical-specific audience data and case studies
- Run targeted outreach campaigns for each vertical (10+ companies each)
- Close at least 5 new sponsors from the new verticals by end of [[24q3]]
- Achieve vertical diversification: no single vertical should represent more than 40% of sponsorship revenue
## Key decisions
- **Three verticals, not more.** Considered also targeting recruitment platforms and SaaS tools, but decided to focus on three verticals where the audience fit was strongest. Spreading too thin would dilute outreach quality.
- **Vertical-specific case studies.** Rather than using a generic media kit, created tailored one-pagers for each vertical showing relevant audience segments. For example, the cloud infra pitch highlighted the percentage of readers who are engineering managers or directors (the buying decision-makers for infrastructure).
- **Higher pricing for AI/ML vertical.** AI/ML companies had larger marketing budgets and higher urgency to reach developers. Tested a 20% price premium on the Deep Dive tier for this vertical, and it held without pushback.
## Notes
- Cloud infrastructure was the easiest vertical to crack — many companies were already familiar with newsletter sponsorships and had established budgets for developer marketing. Closed 3 deals in this vertical within 6 weeks.
- AI/ML was more volatile — companies had budget but were less predictable. Two deals fell through after verbal commitment because of budget freezes. The companies that did close were excellent sponsors with high renewal rates.
- Developer education was harder than expected. Many education companies preferred affiliate/commission models over flat-rate sponsorships. Only closed 1 deal in this vertical during Q3, though it renewed for 3 months.
- By end of Q3, vertical distribution was: devtools 35%, cloud infra 30%, AI/ML 25%, education 10%. This was a significant improvement from the previous 85% devtools concentration.
- The outreach templates developed during this project became part of [[procedure-quarterly-sponsor-outreach]] and continue to be used with minor updates.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Podcast Season 2"]
Is A: Project
Belongs to: "[[24q3]]"
Advances: "[[responsibility-podcast]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Podcast Season 2
## Overview
Building on the momentum from [[24q1-podcast-season-1]], Season 2 expanded the podcast to 8 episodes with a focused theme on engineering leadership and organizational design. The season explored how high-performing engineering teams are structured, how technical leaders make decisions, and the human dynamics that make or break engineering organizations.
The production workflow was significantly smoother this time — [[person-sara-ricci]] handled all post-production editing using the [[procedure-podcast-editing]] workflow established during Season 1, and [[person-paco-furiani]] helped with guest research and outreach. The landing page built in [[24q2-build-podcast-landing-page]] served as the canonical home for all episodes. Season 2 launched in July and ran through September.
## Goals
- Record and publish 8 episodes on engineering leadership and org design
- Secure higher-profile guests (target: at least 2 guests with 10K+ Twitter following)
- Grow average episode downloads to 2,000 (up from Season 1's ~1,500 average)
- Introduce a podcast sponsorship slot (mid-roll read) aligned with newsletter sponsor packages
- Improve audio quality: all episodes recorded on Riverside.fm with backup local recordings
## Key decisions
- **Themed season with a narrative arc.** Rather than random topics, structured Season 2 around a progression: from individual contributor leadership (episodes 1-2) to team dynamics (3-4) to org design (5-6) to executive leadership (7-8). This gave the season a cohesive feel and encouraged binge listening.
- **Mid-roll sponsor read, not pre-roll.** Tested sponsor placements and found that mid-roll reads (inserted at a natural break around the 15-minute mark) had significantly better recall and click-through than pre-roll. This aligned with the sponsorship packages from [[24q1-launch-sponsorship-packages]].
- **No video this season.** Dropped the video component entirely to reduce production overhead. The video clips from Season 1 generated minimal YouTube traction, and the effort-to-impact ratio was poor. Refocused entirely on audio quality.
## Notes
- Guest booking was easier in Season 2 — having a published Season 1 with real download numbers made outreach emails far more credible. Response rate improved from ~15% to ~30%.
- [[person-emma-wilson]] (VP Engineering at a Series C startup) was the standout guest — her episode on "Building Engineering Culture from Scratch" became the most downloaded episode of the season (~3,200 downloads) and was widely shared on LinkedIn.
- Average downloads per episode reached 2,100, exceeding the 2,000 target. The newsletter cross-promotion and improved show notes SEO from [[24q2-build-podcast-landing-page]] were the primary growth drivers.
- The podcast sponsorship slot generated about $1,500 in additional monthly revenue during the season, which validated the channel as a revenue stream alongside newsletter sponsorships.
- [[measure-podcast-downloads]] showed a clear pattern: downloads spike on newsletter send days and taper gradually. This confirms that the newsletter audience is the primary distribution channel for the podcast.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Launch Premium Newsletter Tier"]
Is A: Project
Belongs to: "[[24q3]]"
Advances: "[[responsibility-grow-newsletter]]"
Status: Abandoned
Owner: "[[person-luca-rossi]]"
---
# Launch Premium Newsletter Tier
## Overview
This project explored adding a paid subscription tier to the Refactoring newsletter. The hypothesis was that a segment of the audience would pay $10/month for premium content — deeper case studies, exclusive interviews, and a private community. If successful, this would create a second revenue stream alongside sponsorships and reduce dependency on a single monetization model.
The premium tier launched in mid-July with a 2-week free trial. After 6 weeks of operation, conversion rates were significantly below projections (0.3% vs. the 2% target), and the incremental content burden was unsustainable alongside the free newsletter and podcast. The project was abandoned in early September. While the outcome was a clear negative, the experiment provided valuable data about the audience's willingness to pay and the true cost of premium content production.
## Goals
- Design the premium tier: pricing, content cadence, and exclusive features
- Set up Substack paid subscriptions with a 2-week free trial
- Create 4 premium-only deep dives to serve as the initial content library
- Acquire 200 paying subscribers within the first 6 weeks
- Evaluate viability: target a sustainable $2,000/month from paid subscriptions
## Key decisions
- **$10/month, not $5 or $15.** Benchmarked against similar engineering newsletters. $5 felt too cheap to justify the effort; $15 was above what comparable newsletters charged. $10 was the market rate, but it turned out the audience was not price-sensitive — they simply did not want to pay for content that was perceived as similar to the free tier.
- **Deep dives as the core premium offering.** Rather than gating regular newsletter issues, created separate premium-only deep dives (3,000-5,000 words). This preserved the free newsletter's value while offering something genuinely different. The problem was that the deep dives took 8-10 hours each to write, making the unit economics terrible at low subscriber counts.
- **Abandoned rather than pivoted.** Considered pivoting to a community-only premium tier (no premium content, just Discord access), but decided the timing was wrong. The audience was not large enough to sustain a vibrant paid community. Revisiting community as a concept in [[25q3-community-launch]] with a different approach.
## Notes
- The 0.3% conversion rate was the clearest signal. Industry benchmarks for engineering newsletters suggest 1-3% is typical. Being at 0.3% meant the value proposition was fundamentally off, not just undermarketed.
- The 60 subscribers who did convert were highly engaged and gave positive feedback. Several said they subscribed to "support the newsletter" rather than for the premium content itself. This suggests a patronage model (like Patreon) might work better than a content-gated model.
- The biggest cost was not financial but attentional. Writing premium deep dives while maintaining the free newsletter, podcast, and sponsor obligations created unsustainable pressure during July-August. [[person-sara-ricci]] flagged this early and was right.
- Key lesson: for a sponsorship-funded newsletter, adding paid subscriptions creates a conflict. Free reach drives sponsor value; gating content behind a paywall reduces reach. The two models work against each other unless the audience is very large. Better to focus on growing [[measure-subscribers]] and increasing sponsor rates.
- This experiment informed the decision to try [[25q1-paid-newsletter-trial]] with a completely different approach (annual pricing, bundled with podcast content).

View File

@@ -1,37 +0,0 @@
---
aliases: ["Summer Reading Sprint"]
Is A: Project
Belongs to: "[[24q3]]"
Advances: "[[responsibility-learning]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Summer Reading Sprint
## Overview
July and August are traditionally lighter months for the newsletter (sponsors pause, audience engagement dips during European summer holidays), which creates space for deeper reading. This project set an intentional reading goal of 6 books in two months, focused on history, business, and philosophy of science — topics that inform the newsletter's intellectual depth without being directly "content research."
The sprint contributed to [[2024-read-24-books]] and was structured around dedicated morning reading blocks (7:00-8:30 AM, before work) and longer sessions during weekends. The mix of topics was deliberate: history for perspective, business for practical frameworks, and philosophy of science for clearer thinking about complex systems.
## Goals
- Read 6 books in July and August (3 per month)
- Topic mix: 2 history, 2 business, 2 philosophy of science
- Write brief reading notes for each book in the vault
- Identify at least 3 ideas from the reading that can be developed into newsletter issues
- Maintain the reading habit: minimum 45 minutes per day, 6 days per week
## Key decisions
- **Curated list, not discovery-driven.** Selected all 6 books before the sprint began rather than picking the next book after finishing one. This avoided the decision fatigue that usually slows down reading between books. The list was finalized in late June.
- **Physical books only.** Committed to reading physical copies rather than Kindle during the sprint. The tactile experience and absence of screen distractions improved focus and retention. Kindle is fine for casual reading, but for a concentrated sprint, physical books won.
- **Notes in the vault, not Goodreads.** All reading notes written as markdown files in the personal vault rather than on Goodreads or a public platform. The notes are for personal reference and newsletter idea generation, not social signaling.
## Notes
- Completed 5 of 6 books (83% completion). The sixth book (a dense history of the Scientific Revolution) was about 60% finished by end of August and completed in early September. Close enough to call it a success.
- The standout read was "The Innovator's Dilemma" (re-read) — it directly influenced thinking about the newsletter's competitive dynamics and informed two Q4 newsletter issues on disruption in developer tools.
- Morning reading blocks worked exceptionally well. The consistency of a fixed time slot made the habit nearly automatic by the third week. This approach has been maintained beyond the sprint.
- [[person-marco-bianchi]] recommended "The Structure of Scientific Revolutions" (Kuhn), which turned out to be the most intellectually challenging and rewarding book of the sprint. The concept of paradigm shifts maps surprisingly well onto how engineering organizations evolve.
- Three newsletter issues in Q4 drew directly from the reading: one on Christensen's disruption theory applied to DevOps tools, one on historical patterns in technology adoption, and one on mental models from philosophy of science. The reading-to-content pipeline works, but with a 2-3 month delay.

View File

@@ -1,32 +0,0 @@
---
aliases: ["Q3 2024"]
Is A: Quarter
Created at: "2024-07-01"
Belongs to: "[[2024]]"
Has: ["[[24q3-premium-tier]]", "[[24q3-codemotion-talk]]", "[[24q3-summer-reading-sprint]]", "[[24q3-podcast-season-2]]", "[[24q3-new-sponsor-verticals]]"]
Status: Done
---
# Q3 2024
## Focus
Q3 was about diversifying revenue and expanding reach. The premium tier launch was the biggest bet — adding a paid subscription layer on top of the free newsletter. I also committed to a speaking slot at Codemotion and kicked off Season 2 of the podcast. On the personal side, summer meant a reading sprint and Maratona dles Dolomites.
## Highlights
- Launched [[24q3-premium-tier]] with deep-dive case studies, templates, and a monthly AMA — 320 paid subscribers in the first 6 weeks
- Delivered [[24q3-codemotion-talk]] on "Scaling Engineering Culture" to ~400 attendees — led to 3 inbound sponsor inquiries and a burst of new subscribers
- Completed [[24q3-summer-reading-sprint]]: read 8 books in July-August including "An Elegant Puzzle", "Team Topologies", and "The Ride of a Lifetime"
- Recorded and shipped [[24q3-podcast-season-2]] (episodes 13-24) — introduced guest interviews, with [[person-sara-ricci]] helping coordinate guests
- Opened [[24q3-new-sponsor-verticals]] targeting DevOps tooling and cloud platforms, expanding beyond the original developer tools niche
- Newsletter hit 46k subscribers — [[2024-reach-50k-subscribers]] within reach
- Completed Maratona dles Dolomites in 9h15m — [[2024-complete-two-gran-fondos]] achieved
- [[measure-podcast-downloads]] reached 8k/month, nearly 4x since launch
## Reflections
The premium tier launch taught me a lot about pricing psychology. I initially set it at EUR 8/month, then bumped to EUR 12/month after the first week when conversion was higher than expected. The people who pay aren't price-sensitive — they want signal, not savings. The AMA calls turned out to be the most valued feature, which I didn't expect. Hearing directly from engineering leaders about their challenges gives me better content ideas than any amount of keyword research.
Codemotion was a turning point for visibility. Speaking on stage to 400 people, then having a line of folks wanting to chat afterward — it hit me that Refactoring had become a real brand, not just "Luca's newsletter." Three sponsors reached out within a week of the talk. [[person-matteo-cellini]] converted two of them.
Finishing Maratona was the personal highlight of the year. The Dolomites are otherworldly — Passo Giau at dawn, with the peaks turning pink, was one of those moments where you forget about pace and just exist. Both gran fondos done, [[2024-complete-two-gran-fondos]] checked off. The [[24q3-summer-reading-sprint]] kept me intellectually fueled during the quieter August weeks. Heading into Q4 feeling strong on all fronts.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Team Annual Review"]
Is A: Project
Belongs to: "[[24q4]]"
Advances: "[[responsibility-team-management]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Team Annual Review
## Overview
As the Refactoring team grew from a solo operation to a small but functional team, it became clear that informal check-ins were not sufficient for aligning on expectations, giving structured feedback, and planning development. This project designed and executed the first formal annual review process for the three team members: [[person-matteo-cellini]] (sponsorship outreach), [[person-paco-furiani]] (operations and podcast support), and [[person-sara-ricci]] (editing).
The goal was not to create a heavyweight corporate HR process but to establish a lightweight, honest, and useful review cycle that fits a small indie team. Reviews happened in December 2024, covering the full calendar year, and set the stage for [[25q1]] planning. The process was designed to be reusable and will run annually going forward.
## Goals
- Design a review template covering: role scope, key accomplishments, areas for growth, and goals for next year
- Write a self-assessment prompt for each team member to complete before the review meeting
- Conduct 1-hour review conversations with each team member
- Document agreed-upon goals and any compensation adjustments for 2025
- Establish this as a repeatable annual process with a calendar reminder and documented procedure
## Key decisions
- **Conversation-first, not form-first.** The review is structured around a 1-hour conversation, not a lengthy written form. The self-assessment is a short (half-page) prompt to help team members reflect before the meeting. The conversation is where the real feedback happens.
- **Bidirectional feedback.** Each team member was explicitly asked to give feedback on working with [[person-luca-rossi]] — what works, what does not, what they need more of. This was uncomfortable but produced the most actionable insights of the entire process.
- **Compensation adjustments tied to review, not negotiation.** Rather than waiting for team members to ask for raises, proactively included compensation review as part of the process. All three team members received adjustments based on expanded scope and demonstrated value.
## Notes
- [[person-sara-ricci]]'s review was the most impactful. She articulated clearly that the editing workflow needed more lead time — receiving drafts 24 hours before publish was creating unnecessary stress and reducing edit quality. This led to adjusting the [[procedure-weekly-newsletter]] timeline to give her 48 hours minimum.
- [[person-matteo-cellini]] expressed interest in taking on more strategic work beyond outreach execution. This was a growth signal — his Q4 performance on [[24q3-new-sponsor-verticals]] showed he could handle strategic thinking, not just tactical execution. Agreed to expand his role in 2025.
- [[person-paco-furiani]] was the most straightforward review — he is happy with his current role scope and compensated fairly. His main feedback was wanting more visibility into the business roadmap, which is a fair ask. Started sharing quarterly plans with the full team afterward.
- The process took about 6 hours total (prep + 3 conversations + documentation). This is very manageable for a small team. The key learning: do this every year, not just when it feels necessary. Scheduled the 2025 review for December in advance.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Black Friday Newsletter Campaign"]
Is A: Project
Belongs to: "[[24q4]]"
Advances: "[[responsibility-grow-newsletter]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Black Friday Newsletter Campaign
## Overview
Black Friday represents a unique opportunity for a technical newsletter: readers are actively looking for tool and resource recommendations, and companies are offering significant discounts. This project designed and executed a curated Black Friday campaign — a special edition newsletter with hand-picked tool recommendations for engineering leaders and developers, paired with a subscriber acquisition push through social media and cross-promotions.
The campaign ran from November 25-29, 2024, and generated +1,200 new subscribers in 3 days — the single largest acquisition spike of the year. The key insight was that curation is the value: readers do not want to sift through hundreds of generic deals. They want a trusted voice telling them which 10-15 tools are actually worth buying. This directly supported [[2024-reach-50k-subscribers]] and pushed the subscriber count past the 48K mark.
## Goals
- Curate a list of 15-20 Black Friday deals on developer tools, SaaS products, and learning resources
- Negotiate exclusive or enhanced discount codes with at least 5 companies
- Publish a special-edition newsletter issue on Black Friday week
- Run a social media campaign (Twitter, LinkedIn) driving traffic to the newsletter signup with the deal list as the lead magnet
- Acquire 1,000+ new subscribers during the campaign window
## Key decisions
- **Curated recommendations, not affiliate links.** While affiliate revenue was tempting, prioritized trust over short-term monetization. Every recommendation was a tool that [[person-luca-rossi]] personally uses or has thoroughly evaluated. Included honest caveats alongside recommendations. This authenticity drove shares and word-of-mouth signups.
- **Gated the full list behind email signup.** Shared 5 deals publicly on social media with a CTA to "get the full list of 20 deals" via newsletter signup. This was the primary acquisition mechanism and converted at approximately 35% of landing page visitors.
- **Separate from regular sponsor placements.** Black Friday sponsors paid a premium rate for a dedicated section, clearly labeled as "sponsored picks." This maintained editorial integrity while monetizing the high-attention window. Three sponsors participated at $3,000 each.
## Notes
- The +1,200 subscribers in 3 days was remarkable, but retention was the real test. At the 30-day mark, 78% of Black Friday subscribers were still active (industry benchmark is ~60% for acquisition spikes). This suggests the deal-seekers were genuinely interested in the newsletter's regular content, not just the deals.
- Negotiating exclusive discount codes with tool companies was easier than expected. Most developer tool companies are eager for newsletter placement during Black Friday and will create custom codes quickly. Started these conversations in early November — next year, will start in October.
- [[person-matteo-cellini]] handled sponsor coordination for the campaign, managing the three premium placements. His work on the CRM ([[24q2-sponsor-crm]]) made tracking these deals seamless.
- The social media push was amplified significantly by retweets/reshares from the tools featured in the list. This organic amplification was not planned but accounted for an estimated 30-40% of the total reach. Will be more intentional about asking for shares in future campaigns.
- [[measure-subscribers]] jumped from ~46,800 to ~48,000 during the campaign. Combined with organic growth, this put the year-end total within striking distance of the 50K target set in [[2024-reach-50k-subscribers]].

View File

@@ -1,38 +0,0 @@
---
aliases: ["Cycling Year in Review 2024"]
Is A: Project
Belongs to: "[[24q4]]"
Advances: "[[responsibility-health-fitness]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Cycling Year in Review 2024
## Overview
This project reviewed the full 2024 cycling season — training volumes, event results, fitness progression, and gear decisions — and used the analysis to plan the 2025 season. The review covered everything from the spring training plan established in [[24q1-plan-cycling-season]] through the two gran fondos and into the autumn detraining period.
The review was conducted in November-December, using data from Strava, TrainingPeaks, and personal notes. It combined quantitative analysis (volume, intensity distribution, power data) with qualitative reflection (what felt good, what caused fatigue, where motivation dipped). The output was both a retrospective document and a set of concrete recommendations for [[25q1-strength-program]] and the 2025 race calendar.
## Goals
- Compile and analyze full-year training data: total distance, climbing, hours, and TSS
- Review performance at both gran fondos (Varese in May, Appennino in September)
- Assess the effectiveness of the polarized training model adopted in Q1
- Identify 3-5 specific areas for improvement in 2025
- Draft a preliminary 2025 season plan including target events and training philosophy adjustments
## Key decisions
- **Add structured strength training for 2025.** The year review revealed that upper body and core fatigue was a limiter on long climbs. Pure cycling volume is not enough — need complementary strength work. This directly led to [[25q1-strength-program]].
- **Target the Stelvio in 2025.** The two 2024 gran fondos were rewarding but relatively conservative choices. For 2025, the stretch goal is the Stelvio — one of cycling's most iconic climbs. This requires a step up in training specificity and altitude preparation. Supports [[2025-ride-stelvio]].
- **Hire a coach for 2025.** Self-coaching worked reasonably well in 2024 (both events completed within targets), but the Stelvio goal requires more sophisticated periodization and real-time plan adjustments. Will look for a coach in Q1.
## Notes
- Full-year numbers: 8,200 km ridden, 98,000m elevation gain, approximately 320 hours on the bike. This was above target (7,500 km) and sustainable — no overtraining symptoms or significant injuries.
- The polarized model (80% easy / 20% hard) was broadly successful, though the actual distribution ended up closer to 75/25 because "easy" rides often crept into moderate territory. Discipline on easy days is something to improve.
- [[person-paco-furiani]] completed the same two events and his season review is a useful comparison point. His higher-volume approach (10,500 km) yielded better absolute results but also led to a knee issue in October. Reinforces the value of sustainability over pure volume.
- The Campagnolo Bora WTO 45 wheels purchased pre-season were the best equipment decision of the year. Noticeable improvement on climbs and crosswind stability. No regrets on prioritizing wheels over frame upgrade.
- [[measure-cycling-km-per-month]] data shows clear seasonal pattern: low in Jan-Feb (800-900km/month), building through Mar-May (1,000-1,200km), peaking in Jun-Aug (1,200-1,400km), and tapering in Sep-Nov. December was deliberately rest-focused (~400km).
- The retrospective feeds directly into [[topic-cycling-training]] notes and sets the direction for 2025 season planning.

View File

@@ -1,37 +1,17 @@
---
aliases: ["Start Laputa App Project"]
Is A: Project
Belongs to: "[[24q4]]"
Advances: "[[responsibility-learning]]"
Status: Done
Owner: "[[person-luca-rossi]]"
type: Project
aliases:
- "[[Start Laputa App Project]]"
belongs_to: "[[24q4]]"
owner: "[[person-luca-rossi]]"
status: Done
---
# Start Laputa App Project
## Overview
The original spike that proved Tolaria could read a markdown vault, render note metadata, and support keyboard-first navigation.
After years of managing a personal knowledge vault in Obsidian and finding it increasingly insufficient for the structured, frontmatter-heavy workflow that had evolved, this project kicked off the development of Laputa — a custom desktop application built specifically for the vault's unique requirements. The app is built with Tauri v2 (Rust backend) and React (TypeScript frontend), designed to read and write the same markdown files with YAML frontmatter that the vault already uses.
- Set the initial four-panel layout.
- Proved the note list, editor, and inspector could coexist in one flow.
- Led directly into [[25q1-laputa-v1]].
The motivation was not to replace Obsidian for general note-taking, but to build a purpose-built tool for the structured data layer — types, relationships, properties, and views that Obsidian handles poorly. Laputa treats the vault as a lightweight database of markdown files, offering a four-panel UI with type-aware navigation, property editing, and relational browsing. This project covers the initial spike: architecture decisions, proof of concept, and first working prototype. It advances [[2025-ship-laputa]] as the longer-term goal.
## Goals
- Define the technical architecture: Tauri v2, React 18, CodeMirror 6, Vite
- Build a proof of concept that reads markdown files from a directory and displays them in a panel UI
- Implement frontmatter parsing in Rust (using serde and gray_matter)
- Create the basic four-panel layout: type sidebar, note list, editor, and property panel
- Validate that the app can handle the full vault (~9,200 markdown files) without performance issues
## Key decisions
- **Tauri v2, not Electron.** Tauri produces much smaller binaries, uses less memory, and leverages native webview rather than shipping a full Chromium instance. For a single-user desktop app, Tauri's trade-offs (no cross-platform webview consistency, newer ecosystem) are acceptable. The Rust backend is also more natural for file I/O heavy workloads.
- **Files as the source of truth, not a database.** Laputa reads and writes markdown files directly. No SQLite, no JSON store, no separate database. This means the vault remains portable, version-controllable with git, and readable in any text editor. The cost is that some queries are slower than they would be with a database, but for ~10K files, filesystem operations are fast enough.
- **CodeMirror 6, not ProseMirror or TipTap.** CodeMirror 6 provides the best foundation for a reveal-on-focus markdown editor — showing rendered markdown by default but revealing raw syntax when the cursor enters a block. This is the editing model that feels most natural for power users.
## Notes
- The initial proof of concept took about 3 weeks of focused evening/weekend work. Getting Tauri v2 set up and communicating between the Rust backend and React frontend required working through several documentation gaps — Tauri v2 was still relatively new and the ecosystem was evolving.
- Performance testing with the full vault (~9,200 files) showed that initial directory scanning takes about 2 seconds, and frontmatter parsing adds another 1.5 seconds. This is acceptable for app startup, though indexing will need optimization for real-time search.
- [[person-david-kim]] (who works on developer tools) provided useful early feedback on the UI concept during a podcast conversation. His perspective on tool-building for personal use versus building products helped frame the project's scope.
- The mock data layer (`src/mock-tauri.ts`) was built early and proved invaluable for browser-based testing. Being able to develop and test the React frontend without the Rust backend running dramatically accelerated UI iteration.
- This project transitions directly into [[25q1-laputa-v1]] for the first real milestone of the app.

View File

@@ -1,37 +0,0 @@
---
aliases: ["LinkedIn Cross-posting Experiment"]
Is A: Experiment
Belongs to: "[[24q4]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# LinkedIn Cross-posting Experiment
Tested whether repurposing newsletter essays as LinkedIn posts could drive meaningful follower growth and referral traffic back to the newsletter. LinkedIn's algorithm favors native content, so the hypothesis was that adapted versions of existing essays would perform well without requiring net-new writing effort, supporting both [[responsibility-grow-newsletter]] and [[topic-newsletter-growth]].
## Hypothesis
Posting 3 adapted newsletter essays per week on LinkedIn for 6 weeks would generate at least 500 new LinkedIn followers and drive 200+ clicks to the newsletter signup page, at under 30 minutes of incremental work per post.
## Setup
- Selected the top-performing newsletter essays from [[24q3]] and [[24q4]] based on [[measure-open-rate]].
- Adapted each essay into LinkedIn-native format: shorter paragraphs, hook-first structure, no external links in the body (link in first comment instead).
- Posted 3x per week (Monday, Wednesday, Friday) for 6 weeks.
- Tracked LinkedIn follower growth, post impressions, engagement rate, and UTM-tagged clicks to the newsletter signup page.
## Results
- LinkedIn followers: +812 over 6 weeks (from ~2,400 to ~3,200).
- Average post impressions: ~6,500 (range: 1,200 to 28,000).
- Newsletter signups via LinkedIn referral: 147 (below the 200 target but still meaningful).
- Time per post: approximately 20 minutes (mostly reformatting, not rewriting).
- Top-performing posts were contrarian takes and personal stories; tactical how-to posts underperformed.
## Takeaways
- LinkedIn cross-posting is a high-ROI channel for newsletter growth. The effort-to-output ratio is excellent since the content already exists.
- Follower growth exceeded the target. The platform rewards consistency and native formatting.
- Referral traffic was modest. LinkedIn users tend to engage on-platform rather than clicking out. The real value is brand awareness and top-of-funnel reach.
- Will continue cross-posting as a permanent part of the [[procedure-weekly-newsletter]] workflow.
- Consider having [[person-matteo-cellini]] handle the reformatting to free up time for [[responsibility-content-production]].

View File

@@ -1,37 +0,0 @@
---
aliases: ["Build Sponsor Dashboard"]
Is A: Project
Belongs to: "[[24q4]]"
Advances: "[[responsibility-sponsorships]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Build Sponsor Dashboard
## Overview
As the sponsor base grew through 2024, reporting became a bottleneck. After each newsletter issue, sponsors would ask for performance data — click counts, open rates, and placement impressions. This data was being compiled manually from ConvertKit analytics and sent via email, which was time-consuming and error-prone. This project built an Airtable-based dashboard that gives sponsors self-serve access to real-time performance data for their placements.
The dashboard connects to the CRM built in [[24q2-sponsor-crm]] and pulls performance data from ConvertKit's API via a Zapier integration. Each sponsor gets a unique link to a filtered Airtable view showing only their placements, metrics, and invoice history. This project was a significant step toward professionalizing the sponsor experience and supporting [[2024-double-revenue]] through improved retention and upsell conversations.
## Goals
- Build an Airtable dashboard with per-sponsor performance data (clicks, CTR, impressions)
- Set up a Zapier integration to pull click and open data from ConvertKit after each newsletter send
- Create unique, filtered dashboard links for each sponsor (no login required, link-based access)
- Include historical performance trends (week-over-week, month-over-month)
- Reduce sponsor reporting time from ~2 hours/week to near-zero
## Key decisions
- **Airtable interface, not a custom web app.** Considered building a custom dashboard with Next.js and a database, but the engineering effort was not justified for ~20 active sponsors. Airtable's Interface Designer provides 80% of the functionality at 5% of the development cost. Can always migrate to a custom solution if the sponsor base grows significantly.
- **Self-serve access via unique links.** Rather than requiring sponsors to log into a portal, each sponsor gets a unique Airtable shared view URL. This is simpler for both parties — no password management, no onboarding friction. The trade-off is that anyone with the link can see the data, but the risk is low for non-sensitive marketing metrics.
- **Automated data ingestion, manual quality checks.** The Zapier integration pulls data automatically after each send, but [[person-matteo-cellini]] reviews the numbers weekly before they appear on sponsor dashboards. This catches any data anomalies (e.g., bot clicks, tracking issues) before sponsors see them.
## Notes
- The dashboard launch received very positive feedback from sponsors. Several mentioned it was the most transparent reporting they had seen from any newsletter sponsorship. This transparency became a competitive advantage in renewal conversations.
- The Zapier integration was the most fragile part of the system. ConvertKit's API rate limits and occasional data delays caused the integration to fail about once a month. Building retry logic and error notifications helped, but this remains a maintenance burden.
- Sponsor self-serve reporting reduced [[person-matteo-cellini]]'s weekly reporting workload from approximately 2 hours to about 15 minutes (just the quality review). This freed significant time for outreach and relationship management.
- The dashboard data also proved valuable for internal analysis. Being able to see click-through rates across all sponsors and placements revealed which types of copy and positioning perform best, informing both editorial decisions and sponsor pitch strategy.
- One unexpected benefit: sponsors who could see their own performance data were more likely to experiment with different ad copy and CTAs. This improved their results and, by extension, their satisfaction and renewal rates. The dashboard supported [[measure-sponsorship-mrr]] growth indirectly through improved retention.

View File

@@ -1,32 +1,16 @@
---
aliases: ["Q4 2024"]
Is A: Quarter
Created at: "2024-10-01"
Belongs to: "[[2024]]"
Has: ["[[24q4-annual-review-process]]", "[[24q4-sponsor-dashboard]]", "[[24q4-laputa-start]]", "[[24q4-black-friday-campaign]]", "[[24q4-cycling-year-review]]"]
Status: Done
type: Quarter
aliases:
- "[[Q4 2024]]"
status: Done
has:
- "[[24q4-laputa-start]]"
---
# Q4 2024
## Focus
The quarter where the Laputa prototype became real enough to replace sketches and notes.
Q4 was about closing the year strong and planting seeds for 2025. The Black Friday campaign was the commercial centerpiece. But the most exciting thing was starting work on [[24q4-laputa-start]] — the personal knowledge management tool I'd been sketching in notebooks for years. The quarter also meant wrapping up the annual review process and reflecting on a transformative cycling season.
- Started [[24q4-laputa-start]] as the first working app spike.
- Captured the initial panel layout and editor decisions.
## Highlights
- [[24q4-black-friday-campaign]] drove 2,800 new subscribers in 10 days and converted 180 free readers to premium — best single campaign ever
- Built [[24q4-sponsor-dashboard]] giving sponsors real-time access to their campaign metrics — reduced back-and-forth emails by ~70%
- Started [[24q4-laputa-start]]: initial Tauri + React prototype, basic markdown reading, YAML frontmatter parsing — the MVP of the MVP
- Designed and documented [[24q4-annual-review-process]] — a structured template for reviewing goals, projects, and metrics across the year
- Completed [[24q4-cycling-year-review]]: 6,200 km ridden, two gran fondos, FTP up 12% year-over-year
- Newsletter crossed 50k subscribers in November — [[2024-reach-50k-subscribers]] achieved
- [[measure-sponsorship-mrr]] closed the year at EUR 11.4k, more than doubling from January — [[2024-double-revenue]] hit
- [[2024-read-24-books]] finished with 26 books total — exceeded target
## Reflections
Hitting 50k subscribers felt like a milestone that validated the whole year's strategy. When I started 2024 at 35k, doubling the growth rate felt ambitious. But the combination of pillar content, podcast reach, the Codemotion talk, and the Black Friday push all compounded. Each channel fed the others in ways I couldn't have predicted at the start of the year.
Starting Laputa was the sleeper highlight of Q4. I'd been frustrated with Obsidian's limitations for months — specifically around structured data, relationships between notes, and the inability to build custom views. So I started building my own tool. The early prototype was rough, but the moment I saw my own vault rendered in a four-panel layout I'd designed, something clicked. This could be more than a side project.
The sponsor dashboard was [[person-matteo-cellini]]'s idea, and it was brilliant. Sponsors love transparency, and giving them self-serve access to metrics reduced our support burden dramatically. [[person-paco-furiani]] set up the automated reporting pipeline. Looking back at [[2024]] as a whole, every major goal was hit or exceeded. That doesn't happen often, and I don't take it for granted. Now the question is: how do we scale this in [[2025]] without burning out?

View File

@@ -1,37 +1,17 @@
---
aliases: ["Laputa App V1"]
Is A: Project
Belongs to: "[[25q1]]"
Advances: "[[responsibility-learning]]"
Status: Done
Owner: "[[person-luca-rossi]]"
type: Project
aliases:
- "[[Laputa App V1]]"
belongs_to: "[[25q1]]"
owner: "[[person-luca-rossi]]"
status: Done
---
# Laputa App V1
## Overview
The first usable release for daily browsing, quick open, and note-property editing.
Following the proof of concept built in [[24q4-laputa-start]], this project delivered the first working version of Laputa with a complete four-panel layout, a property inspector, and a quick-open command palette. V1 represents the transition from "can this work?" to "I can actually use this daily" — the app became the primary interface for browsing and editing the vault, replacing Obsidian for structured data workflows.
- Shipped the working command palette.
- Made the inspector practical for real frontmatter editing.
- Captured enough confidence to continue with [[25q2-laputa-v2]].
The four-panel layout includes a type sidebar (showing all note types like Project, Person, Quarter), a filterable note list, the main editor panel with CodeMirror 6, and a property inspector for viewing and editing YAML frontmatter fields. Quick Open (Cmd+K) enables instant navigation across all ~9,200 vault files. This milestone is a key step toward [[2025-ship-laputa]] and was completed during [[25q1]].
## Goals
- Implement the four-panel layout with resizable panels and responsive behavior
- Build the property inspector panel for viewing and editing frontmatter properties
- Implement Quick Open (Cmd+K) with fuzzy search across all vault files
- Add type-aware sidebar navigation with collapsible sections and file counts
- Achieve sub-200ms navigation between notes (read file, parse frontmatter, render editor)
## Key decisions
- **CodeMirror 6 with reveal-on-focus editing.** The editor shows rendered markdown by default but reveals raw syntax when the cursor enters a block. This hybrid approach was chosen over a pure WYSIWYG editor (TipTap/ProseMirror) because power users need direct access to markdown syntax, especially for frontmatter and wiki-links. The reveal-on-focus model provides the best of both worlds.
- **Frontmatter parsing in Rust, rendering in React.** The Rust backend parses YAML frontmatter and provides structured data to the frontend via Tauri commands. This keeps the parsing fast and consistent, while React handles all presentation logic. The alternative — parsing in JavaScript — would be slower for bulk operations and harder to keep consistent with the Rust-side file operations.
- **Mock layer for browser development.** Built `src/mock-tauri.ts` to return realistic test data when running in a browser without the Tauri backend. This allows UI development and testing in Chrome without starting the full Rust backend, which dramatically accelerated the development loop.
## Notes
- The biggest engineering challenge was making the editor performant with large files. Some vault files are 5,000+ lines, and CodeMirror 6's default configuration struggled with these. Enabling viewport-based rendering and lazy decoration computation solved the performance issues.
- Quick Open search across 9,200 files needed careful optimization. The initial naive approach (filter on every keystroke) was too slow. Implemented a pre-built search index that loads at startup and supports fuzzy matching with sub-50ms response times.
- [[person-david-kim]] tested an early build and identified several UX issues with the property inspector — particularly around editing array-type frontmatter fields (like tags and aliases). His feedback led to a redesigned array editor with inline add/remove controls.
- The mock data layer proved its value immediately. It allowed running Playwright E2E tests and visual verification in Chrome without any Rust toolchain setup. This made CI testing feasible and significantly reduced the feedback loop for UI changes.
- V1 was usable but rough around the edges. The editor lacked many creature comforts (undo/redo state persistence, find-and-replace, syntax highlighting for YAML). These were deferred to [[25q2-laputa-v2]] to keep V1 scope manageable.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Newsletter SEO Sprint"]
Is A: Project
Belongs to: "[[25q1]]"
Advances: "[[responsibility-grow-newsletter]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Newsletter SEO Sprint
## Overview
Building on the pillar article strategy from [[24q2-10-pillar-articles]], this project was a concentrated 30-day sprint to boost organic search traffic and subscriber acquisition. The sprint had two components: writing 5 new high-traffic articles targeting keywords identified in an updated Ahrefs analysis, and overhauling the internal linking structure across all existing content to improve site-wide SEO authority.
The sprint ran through February 2025 and was timed to take advantage of the post-holiday traffic recovery in the engineering content space. By this point, the blog had about 40 published articles, but many lacked internal links and had suboptimal meta descriptions. The combination of new content and structural improvements was designed to create a compounding traffic effect that would support [[2025-reach-85k-subscribers]] through organic growth.
## Goals
- Write and publish 5 new SEO-optimized articles (2,500-4,000 words each)
- Audit and update internal links across all existing blog posts (target: every article links to at least 3 other articles)
- Update meta descriptions and title tags for the 20 highest-traffic existing articles
- Improve site-wide Core Web Vitals scores (target: all pages in "Good" range)
- Achieve a 30% increase in organic search traffic within 90 days of sprint completion
## Key decisions
- **Concentrated sprint, not ongoing drip.** Rather than publishing one SEO article per month, concentrated the effort into a 30-day window. This allowed full focus on SEO without the context-switching cost of alternating between newsletter writing and SEO writing. The trade-off was that the regular newsletter required extra preparation in advance to cover the sprint period.
- **Internal linking overhaul as a force multiplier.** The internal linking audit was actually higher-impact than the new articles. Many existing articles had zero internal links, meaning Google could not discover or weight them properly. Adding contextual links between related articles created a proper content topology that improved rankings site-wide.
- **Content refresh over new content for some keywords.** For 3 keywords where existing articles were already ranking on page 2-3, chose to refresh and expand those articles rather than write new ones. Refreshing existing URLs preserves accumulated link equity and is faster than starting from scratch.
## Notes
- The 30% organic traffic increase was achieved in about 75 days (ahead of the 90-day target). The internal linking overhaul was the primary driver — several existing articles jumped from page 2 to page 1 within weeks of gaining internal links.
- [[person-sara-ricci]] edited all 5 new articles and the refreshed content. The editing workload during the sprint was significant, and she flagged that future sprints should give her more lead time. Noted for next time.
- The article on "How to Run Effective Engineering All-Hands" became the strongest performer, reaching #3 on Google for its target keyword and driving ~1,500 monthly organic visits. This single article generated an estimated 200 new subscribers per month through its email capture CTA.
- Core Web Vitals optimization required some technical work — lazy loading images, reducing JavaScript bundle size, and optimizing font loading. These changes improved site speed across the board and had a modest positive effect on rankings.
- This sprint validated the content pillar strategy and demonstrated that concentrated SEO investment compounds over time. The articles written in [[24q2-10-pillar-articles]] continued to grow in traffic alongside the new content, suggesting that the internal linking network creates a rising-tide effect. Feeds into [[topic-newsletter-growth]] and [[topic-content-strategy]].

View File

@@ -1,36 +0,0 @@
---
aliases: ["Paid Newsletter Trial"]
Is A: Experiment
Belongs to: "[[25q1]]"
Status: Abandoned
Owner: "[[person-luca-rossi]]"
---
# Paid Newsletter Trial
Tested a Substack-style paid tier to evaluate whether the newsletter audience would convert to a direct subscription model. The motivation was to explore revenue diversification beyond [[measure-sponsorship-mrr]], which currently accounts for the majority of income. A paid tier could provide more predictable, audience-aligned revenue while reducing dependence on the sponsorship sales cycle.
## Hypothesis
Offering a paid tier at $10/month (or $100/year) with one exclusive deep-dive essay per month and early access to all posts would convert at least 2% of the free subscriber base within 8 weeks, generating $2,000+ MRR.
## Setup
- Built a paid tier on the existing newsletter platform with two perks: one exclusive monthly deep-dive and 48-hour early access to all free posts.
- Announced the paid tier via a dedicated newsletter issue, a pinned tweet, and a LinkedIn post.
- Ran a 2-week launch promotion at 50% off the annual plan.
- Tracked conversion rate, churn after first month, and qualitative feedback from subscribers who did and did not convert.
## Results
- Total paid subscribers after 8 weeks: 89 (approximately 0.2% of the free base, well below the 2% target).
- MRR generated: ~$620 (mix of monthly and discounted annual).
- Churn after first month: 18% of monthly subscribers cancelled.
- Qualitative feedback: most free subscribers said the free content was already "more than enough." Those who paid cited "supporting the creator" as the primary reason, not exclusive content.
## Takeaways
- The audience does not perceive enough differentiation between free and paid content to justify a subscription. The newsletter's value proposition is built on free, high-quality essays.
- Conversion was 10x below target. The paid tier would need a fundamentally different value prop (e.g., community, tools, templates) to work.
- Abandoned the paid tier to avoid splitting focus. The sponsorship model ([[responsibility-sponsorships]], [[24q1-launch-sponsorship-packages]]) delivers significantly higher revenue per hour of effort.
- If revisited, would explore a premium community model (see [[25q3-discord-community-soft]]) rather than paywalled content.
- Key lesson: do not assume that "more content" is a compelling paid offering. Access, community, and tools tend to convert better for this audience segment.

Some files were not shown because too many files have changed in this diff Show More