Compare commits

...
Author SHA1 Message Date
thePR0M3TH3AN fb54cea3a2 land: area/dispatch ee7391b4d2 (dispatch) 2026-10-20 22:41:29 -04:00
thePR0M3TH3AN ee7391b4d2 client-reliability-8: retain next HEAD investigation handoff across quiet hours 2026-10-20 22:41:29 -04:00
thePR0M3TH3AN 63a55e66e5 land: area/dispatch 3a7730d5e8 (dispatch) 2026-10-20 22:40:51 -04:00
thePR0M3TH3AN 3a7730d5e8 client-reliability-8: finalize closure and restart handoff 2026-10-20 22:40:51 -04:00
thePR0M3TH3AN 9cbb53f821 land: area/dispatch f233eaf2b3 (dispatch) 2026-10-20 17:06:20 -04:00
thePR0M3TH3AN f233eaf2b3 client-reliability-8: record landing preview and bounded native qualification 2026-10-20 17:06:20 -04:00
thePR0M3TH3AN 2ea6f0682b land: area/client-reliability cfc20107bb (client-reliability) 2026-10-20 17:06:02 -04:00
thePR0M3TH3AN cfc20107bb Merge branch 'unstable' into area/client-reliability 2026-10-20 17:05:54 -04:00
thePR0M3TH3AN c03ad2c003 [client-reliability] client-reliability-8 record native browser lifecycle qualification 2026-10-20 17:05:27 -04:00
thePR0M3TH3AN 892c7f5256 [client-reliability] client-reliability-8 record verified landing and close item 2026-10-20 17:03:39 -04:00
thePR0M3TH3AN 43cb1bafe6 land: area/client-reliability 3474668a83 (client-reliability) 2026-10-20 16:57:26 -04:00
thePR0M3TH3AN 3474668a83 [client-reliability] client-reliability-8 cancel active URL media after final owner disposal 2026-10-20 16:56:07 -04:00
thePR0M3TH3AN 3dac0afb8d land: area/dispatch 488d874a97 (dispatch) 2026-10-20 16:51:26 -04:00
thePR0M3TH3AN 488d874a97 client-reliability-8: route measured active media cancellation and spec correction 2026-10-20 16:51:26 -04:00
thePR0M3TH3AN 12f6d1d0b0 land: area/dispatch 8b04a69fb4 (dispatch) 2026-10-20 16:48:39 -04:00
thePR0M3TH3AN 8b04a69fb4 client-reliability-8: record independent active resource reproduction 2026-10-20 16:48:39 -04:00
thePR0M3TH3AN 5b77fd2029 land: area/dispatch c8e07cc508 (dispatch) 2026-10-20 16:47:33 -04:00
thePR0M3TH3AN c8e07cc508 client-reliability-8: route active URL-media cancellation investigation 2026-10-20 16:47:32 -04:00
thePR0M3TH3AN 6609269ea1 land: area/dispatch 15d2fe996a (dispatch) 2026-10-20 16:43:14 -04:00
thePR0M3TH3AN 15d2fe996a fleet update: remove resolved TODO template sidecar 2026-10-20 16:43:14 -04:00
thePR0M3TH3AN e7a103a8fa fleet init 2026-10-20 16:41:38 -04:00
thePR0M3TH3AN 7813070216 fleet update 2026-10-20 16:39:49 -04:00
thePR0M3TH3AN 0ed41bb66f land: area/dispatch 9b8ec5539c (dispatch) 2026-10-20 12:31:50 -04:00
thePR0M3TH3AN 9b8ec5539c dispatch-2 close local-review handoff with explicit known finding 2026-10-20 12:31:50 -04:00
thePR0M3TH3AN 3fd0ab594d land: area/dispatch f3dc7d1355 (dispatch) 2026-10-20 12:31:12 -04:00
thePR0M3TH3AN f3dc7d1355 dispatch-2 prepare evidenced local-review handoff and stop optional work 2026-10-20 12:31:12 -04:00
thePR0M3TH3AN c017069cc0 land: area/playback-qa 98cde500b6 (playback-qa) 2026-10-20 12:30:44 -04:00
thePR0M3TH3AN 98cde500b6 Merge branch 'unstable' into area/playback-qa 2026-10-20 12:30:38 -04:00
thePR0M3TH3AN 60707d2a8a playback-qa-2: close bounded qualification with mobile overflow finding open 2026-10-20 12:30:35 -04:00
thePR0M3TH3AN 06e920e77e land: area/playback-qa 17163984f7 (playback-qa) 2026-10-20 12:29:45 -04:00
thePR0M3TH3AN 17163984f7 Merge branch 'unstable' into area/playback-qa 2026-10-20 12:29:29 -04:00
thePR0M3TH3AN de81198a68 playback-qa-2: retain cold-feed verification and mobile overflow evidence 2026-10-20 12:29:23 -04:00
thePR0M3TH3AN 09b132191a land: area/client-reliability 7e8ef49dca (client-reliability) 2026-10-20 12:22:17 -04:00
thePR0M3TH3AN 7e8ef49dca Merge branch 'unstable' into area/client-reliability 2026-10-20 12:22:15 -04:00
thePR0M3TH3AN cc72ea37ba client-reliability-7: record normal gate landing and QA handoff 2026-10-20 12:22:08 -04:00
thePR0M3TH3AN b117416355 land: area/client-reliability f39622da3b (client-reliability) 2026-10-20 12:15:12 -04:00
thePR0M3TH3AN f39622da3b Merge branch 'unstable' into area/client-reliability 2026-10-20 12:14:40 -04:00
thePR0M3TH3AN fecf42948f client-reliability-7: notify the current feed on live video updates 2026-10-20 12:14:21 -04:00
thePR0M3TH3AN d7d632f88b land: area/dispatch 092d9c7046 (dispatch) 2026-10-20 12:11:31 -04:00
thePR0M3TH3AN 092d9c7046 dispatch-2 require current-consumer lifecycle controls for client7 2026-10-20 12:11:31 -04:00
thePR0M3TH3AN d07f471454 land: area/playback-qa e661e6a3b1 (playback-qa) 2026-10-20 12:10:04 -04:00
thePR0M3TH3AN e661e6a3b1 Merge branch 'unstable' into area/playback-qa 2026-10-20 12:10:02 -04:00
thePR0M3TH3AN 48ceb2805c docs(playback): playback-qa-2 preserve demonstrated cold feed render gap 2026-10-20 12:09:59 -04:00
thePR0M3TH3AN 4e90434992 land: area/dispatch 4bda9aab44 (dispatch) 2026-10-20 12:09:10 -04:00
thePR0M3TH3AN 4bda9aab44 dispatch-2 route measured cold-feed local-review blocker to client7 2026-10-20 12:09:10 -04:00
thePR0M3TH3AN 4a1d182653 docs(playback): playback-qa-2 locate actual feed pipeline render boundary 2026-10-20 12:03:13 -04:00
thePR0M3TH3AN 16cc80e1eb docs(playback): playback-qa-2 record measured zero-card pipeline blocker 2026-10-20 11:59:28 -04:00
thePR0M3TH3AN 386cc60e1f land: area/client-reliability 36daa5257e (client-reliability) 2026-10-20 11:41:44 -04:00
thePR0M3TH3AN 36daa5257e Merge branch 'unstable' into area/client-reliability 2026-10-20 11:41:41 -04:00
thePR0M3TH3AN b2564bc977 client-reliability-6: record normal gate evidence and close history audit 2026-10-20 11:41:34 -04:00
thePR0M3TH3AN fa358eb4c1 docs(playback): playback-qa-2 verify genuine local media fixture 2026-10-20 11:40:16 -04:00
thePR0M3TH3AN 4b74da7372 docs(playback): playback-qa-2 preserve noninteractive baseline action plan 2026-10-20 11:33:46 -04:00
thePR0M3TH3AN b3fab0326b land: area/client-reliability ec41d3381c (client-reliability) 2026-10-20 11:33:02 -04:00
thePR0M3TH3AN ec41d3381c Merge branch 'unstable' into area/client-reliability 2026-10-20 11:32:36 -04:00
thePR0M3TH3AN ee588ba6a1 docs(playback): playback-qa-2 record incomplete second pass and driver correction 2026-10-20 11:29:01 -04:00
thePR0M3TH3AN c5818cfe67 land: area/storage-cors aacb0d147b (storage-cors) 2026-10-20 11:23:48 -04:00
thePR0M3TH3AN aacb0d147b Merge branch 'unstable' into area/storage-cors 2026-10-20 11:23:39 -04:00
thePR0M3TH3AN d9b0cb510e storage-cors-2 close after durable storage cleanup landing 2026-10-20 11:23:37 -04:00
thePR0M3TH3AN 1b3ef49cbb land: area/dispatch 4900fa95f7 (dispatch) 2026-10-20 11:22:48 -04:00
thePR0M3TH3AN c4aac993d7 land: area/playback-qa 25942a156c (playback-qa) 2026-10-20 11:22:48 -04:00
thePR0M3TH3AN 4900fa95f7 Merge remote-tracking branch 'origin/unstable' into area/dispatch 2026-10-20 11:18:40 -04:00
thePR0M3TH3AN d9aef54d3b Merge branch 'unstable' into area/dispatch 2026-10-20 11:18:40 -04:00
thePR0M3TH3AN a7e6687f00 dispatch-2: record explicit local-review milestone and reconcile playback backlog evidence 2026-10-20 11:18:40 -04:00
thePR0M3TH3AN 25942a156c Merge branch 'unstable' into area/playback-qa 2026-10-20 11:16:30 -04:00
thePR0M3TH3AN 612ad83f9d docs(playback): playback-qa-2 record approved mirror snapshot provenance 2026-10-20 11:16:28 -04:00
thePR0M3TH3AN e7b9afb309 land: area/storage-cors 1650b3439f (storage-cors) 2026-10-20 11:16:23 -04:00
thePR0M3TH3AN 1650b3439f storage-cors-2 record merged 430-file gate before normal landing 2026-10-20 11:16:19 -04:00
thePR0M3TH3AN 4c386631b9 docs(playback): playback-qa-2 propose verified cached asset adapter 2026-10-20 11:14:15 -04:00
thePR0M3TH3AN 9afe48a5b5 test: protect actual profile history removal wiring (client-reliability-6) 2026-10-20 11:13:42 -04:00
thePR0M3TH3AN 8d7e60ebf1 docs(playback): playback-qa-2 record actual-app offline boot blocker 2026-10-20 11:10:09 -04:00
thePR0M3TH3AN 020f0577ee Merge branch 'unstable' into area/storage-cors 2026-10-20 11:09:45 -04:00
thePR0M3TH3AN 7b8100a6a1 storage-cors-2 skip malformed metadata during best-effort storage cleanup 2026-10-20 11:09:39 -04:00
thePR0M3TH3AN 0535b3dbfc land: area/dispatch 8abbaa77b4 (dispatch) 2026-10-20 11:07:53 -04:00
thePR0M3TH3AN 8abbaa77b4 Merge branch 'unstable' into area/dispatch 2026-10-20 11:07:52 -04:00
thePR0M3TH3AN ff89f000bb dispatch-2: integrate completed QA trial output and next-scope pickups 2026-10-20 11:07:52 -04:00
thePR0M3TH3AN 5de86e7578 Merge branch 'unstable' into area/client-reliability 2026-10-20 11:06:50 -04:00
thePR0M3TH3AN ac99ae9a6b land: area/dispatch cae1c5321b (dispatch) 2026-10-20 11:05:29 -04:00
thePR0M3TH3AN cae1c5321b Merge branch 'unstable' into area/dispatch 2026-10-20 11:05:29 -04:00
thePR0M3TH3AN 47176efe1b dispatch-2 client-reliability-6 storage-cors-2 playback-qa-2: route verified next quality work 2026-10-20 11:05:29 -04:00
thePR0M3TH3AN 8356f27d48 docs: record client-reliability-6 real history wiring coverage audit 2026-10-20 11:04:49 -04:00
thePR0M3TH3AN bbf57388cb land: area/playback-qa 7030d73259 (playback-qa) 2026-10-20 11:03:27 -04:00
thePR0M3TH3AN 7030d73259 Merge branch 'unstable' into area/playback-qa 2026-10-20 11:03:24 -04:00
thePR0M3TH3AN 308ddd5a46 docs(playback): playback-qa-1 close verified fallback qualification 2026-10-20 11:03:22 -04:00
thePR0M3TH3AN 3f9c26e361 land: area/dispatch 291e653839 (dispatch) 2026-10-20 11:02:17 -04:00
thePR0M3TH3AN e8fd120afa land: area/dispatch 402500d3d2 (dispatch) 2026-10-20 11:02:17 -04:00
thePR0M3TH3AN 291e653839 dispatch-2 storage-cors-2: apply latest storage-safety steering; retain deferred feed audit 2026-10-20 10:58:59 -04:00
thePR0M3TH3AN 402500d3d2 Merge branch 'unstable' into area/dispatch 2026-10-20 10:56:40 -04:00
thePR0M3TH3AN 119ba62934 dispatch-2 client-reliability-6 storage-cors-2: continue evidence-backed quality work 2026-10-20 10:56:40 -04:00
thePR0M3TH3AN 1bb3a4fc45 land: area/playback-qa 64143f42ed (playback-qa) 2026-10-20 10:56:11 -04:00
thePR0M3TH3AN 64143f42ed docs(playback): playback-qa-1 record merged full gate evidence 2026-10-20 10:56:08 -04:00
thePR0M3TH3AN d88716e3c6 land: area/dispatch e1ce387059 (dispatch) 2026-10-20 10:51:04 -04:00
thePR0M3TH3AN e1ce387059 dispatch-2 storage-cors-1: verify storage landing, preview and measured wake delay 2026-10-20 10:51:04 -04:00
thePR0M3TH3AN 124be14310 Merge branch 'unstable' into area/playback-qa 2026-10-20 10:50:00 -04:00
thePR0M3TH3AN 7f99c04569 land: area/storage-cors 032f76bcdb (storage-cors) 2026-10-20 10:10:49 -04:00
thePR0M3TH3AN 032f76bcdb Merge branch 'unstable' into area/storage-cors 2026-10-20 10:10:38 -04:00
thePR0M3TH3AN 6cf426130f storage-cors-1 close after durable normal landing and ancestry proof 2026-10-20 10:10:35 -04:00
thePR0M3TH3AN d3f4c2155f land: area/storage-cors 699d72927b (storage-cors) 2026-10-20 10:04:20 -04:00
thePR0M3TH3AN 699d72927b storage-cors-1 record full 428-file gate before normal landing 2026-10-20 10:04:14 -04:00
thePR0M3TH3AN fc259786af land: area/dispatch 4a3aee9e0b (dispatch) 2026-10-20 09:59:39 -04:00
thePR0M3TH3AN 4a3aee9e0b Merge branch 'unstable' into area/dispatch 2026-10-20 09:59:39 -04:00
thePR0M3TH3AN 1320bedac7 dispatch-2 client-reliability-5: verify first trial landing and refresh preview 2026-10-20 09:59:39 -04:00
thePR0M3TH3AN dc8525973a land: area/client-reliability 2515681216 (client-reliability) 2026-10-20 09:58:45 -04:00
thePR0M3TH3AN bfa36a3f35 Merge branch 'unstable' into area/storage-cors 2026-10-20 09:58:44 -04:00
thePR0M3TH3AN 2515681216 Merge branch 'unstable' into area/client-reliability 2026-10-20 09:58:43 -04:00
thePR0M3TH3AN 62ed17e29c docs: close client-reliability-5 after verified landing and slot release 2026-10-20 09:58:41 -04:00
thePR0M3TH3AN 0bd0b1c3e3 land: area/client-reliability 4bc52f9ded (client-reliability) 2026-10-20 09:51:50 -04:00
thePR0M3TH3AN 4bc52f9ded Merge branch 'unstable' into area/client-reliability 2026-10-20 09:51:47 -04:00
thePR0M3TH3AN 6944d6f9df docs: record client-reliability-5 merged-tree validation and slot timing 2026-10-20 09:51:45 -04:00
thePR0M3TH3AN 95a77a7fd7 docs(playback): playback-qa-1 record merged verification checkpoint 2026-10-20 09:49:41 -04:00
thePR0M3TH3AN e9d8886c3a storage-cors-1 record merged-tree checks and pending coordinated gate 2026-10-20 09:44:53 -04:00
thePR0M3TH3AN acb2fdef3b Merge branch 'unstable' into area/playback-qa 2026-10-20 09:44:30 -04:00
thePR0M3TH3AN 025771a84d test(playback): playback-qa-1 qualify bounded fallback orchestration 2026-10-20 09:44:14 -04:00
thePR0M3TH3AN 0919be930c Merge branch 'unstable' into area/storage-cors 2026-10-20 09:44:13 -04:00
thePR0M3TH3AN c82ce8759e land: area/dispatch 4b8c074698 (dispatch) 2026-10-20 09:44:11 -04:00
thePR0M3TH3AN 4b8c074698 dispatch-2: record parallel ready results and exclusive validation handoff 2026-10-20 09:44:11 -04:00
thePR0M3TH3AN f2a392fa3b Merge branch 'unstable' into area/client-reliability 2026-10-20 09:44:06 -04:00
thePR0M3TH3AN 2fb6a53656 storage-cors-1 preserve existing bucket CORS rules before adding origins 2026-10-20 09:44:01 -04:00
thePR0M3TH3AN b922c18601 land: area/dispatch 2a5ba463c4 (dispatch) 2026-10-20 09:43:18 -04:00
thePR0M3TH3AN ff66ebf55a land: area/dispatch 4c7a897ce3 (dispatch) 2026-10-20 09:43:17 -04:00
thePR0M3TH3AN 2a5ba463c4 dispatch-2: record parallel pickups, outputs and resource interventions 2026-10-20 09:42:28 -04:00
thePR0M3TH3AN 4c7a897ce3 dispatch-2 storage-cors-1: route independently reproduced CORS preservation fix 2026-10-20 09:40:26 -04:00
thePR0M3TH3AN 371e79027a land: area/dispatch f3c0ffd3ba (dispatch) 2026-10-20 09:37:27 -04:00
thePR0M3TH3AN 12f927321f fix: stop orphaned URL mirror continuation (client-reliability-5) 2026-10-20 09:37:04 -04:00
thePR0M3TH3AN f3c0ffd3ba dispatch-2 client-reliability-5: route reproduced orphan mirror guard; record trial intervention 2026-10-20 09:32:10 -04:00
thePR0M3TH3AN 140618f88f docs: record client-reliability-5 active URL continuation reproduction 2026-10-20 09:31:03 -04:00
thePR0M3TH3AN bbc1be79e9 land: area/dispatch 47953970ec (dispatch) 2026-10-20 09:30:35 -04:00
thePR0M3TH3AN 47953970ec dispatch-2 storage-cors-1 playback-qa-1: staff approved bounded parallel trial 2026-10-20 09:26:20 -04:00
thePR0M3TH3AN 0c1dd7e86b land: area/dispatch 8c80ae2027 (dispatch) 2026-10-20 09:20:23 -04:00
thePR0M3TH3AN 8c80ae2027 dispatch-2 client-reliability-5: continue bounded active probe investigation 2026-10-20 09:20:23 -04:00
thePR0M3TH3AN 3f627f5df3 land: area/dispatch 320a992249 (dispatch) 2026-10-19 23:30:12 -04:00
thePR0M3TH3AN 320a992249 Merge branch 'unstable' into area/dispatch 2026-10-19 23:30:10 -04:00
thePR0M3TH3AN aef4501c4c dispatch-2 client-reliability-4: verify URL fix landing and refresh preview 2026-10-19 23:30:07 -04:00
thePR0M3TH3AN f116f7c838 land: area/client-reliability 94676453fe (client-reliability) 2026-10-19 23:29:21 -04:00
thePR0M3TH3AN 94676453fe Merge branch 'unstable' into area/client-reliability 2026-10-19 23:29:19 -04:00
thePR0M3TH3AN 374a8aff91 docs: close client-reliability-4 after verified normal landing 2026-10-19 23:29:16 -04:00
thePR0M3TH3AN 89ccb907c9 land: area/client-reliability 7055496b30 (client-reliability) 2026-10-19 23:23:16 -04:00
thePR0M3TH3AN 7055496b30 docs: record client-reliability-4 merged-tree validation 2026-10-19 23:23:16 -04:00
thePR0M3TH3AN ce1381f500 fix: cancel orphaned queued URL media probes (client-reliability-4) 2026-10-19 23:17:03 -04:00
thePR0M3TH3AN d9bd2df6e8 Merge branch 'unstable' into area/client-reliability 2026-10-19 23:10:17 -04:00
thePR0M3TH3AN 77d486fb56 docs: record client-reliability-4 URL queue reproduction 2026-10-19 23:10:15 -04:00
thePR0M3TH3AN 84bb183ab9 land: area/dispatch 50a791f486 (dispatch) 2026-10-19 23:09:35 -04:00
thePR0M3TH3AN 50a791f486 Merge branch 'unstable' into area/dispatch 2026-10-19 23:09:20 -04:00
thePR0M3TH3AN 434d6a9559 client-reliability-4 dispatch-2: route reproduced queued URL cancellation fix 2026-10-19 23:09:14 -04:00
thePR0M3TH3AN 4c54858c98 land: area/dispatch 967448caea (dispatch) 2026-10-19 23:05:14 -04:00
thePR0M3TH3AN 967448caea client-reliability-4 dispatch-2: route bounded source-health cancellation investigation 2026-10-19 23:05:13 -04:00
thePR0M3TH3AN 604c8c61f4 land: area/dispatch fe3a6a83af (dispatch) 2026-10-19 22:59:37 -04:00
thePR0M3TH3AN fe3a6a83af Merge branch 'unstable' into area/dispatch 2026-10-19 22:59:35 -04:00
thePR0M3TH3AN 6cd77767e1 dispatch-2 client-reliability-3: verify normal landing and refresh owner preview 2026-10-19 22:59:33 -04:00
thePR0M3TH3AN c206e0a3c0 fleet init 2026-10-19 22:58:13 -04:00
thePR0M3TH3AN 6d4ed9200c fleet init 2026-10-19 22:57:58 -04:00
thePR0M3TH3AN 7fde734d87 land: area/client-reliability 1c49eb3b2c (client-reliability) 2026-10-19 18:14:58 -04:00
thePR0M3TH3AN 1c49eb3b2c Merge branch 'unstable' into area/client-reliability 2026-10-19 18:14:56 -04:00
thePR0M3TH3AN b815446a52 client-reliability-3: close verified landed lifecycle fix and record dispatch handoff 2026-10-19 18:14:53 -04:00
thePR0M3TH3AN 7d20a509ca land: area/client-reliability e522efc191 (client-reliability) 2026-10-19 18:08:52 -04:00
thePR0M3TH3AN e522efc191 client-reliability-3: record full 426-file and native browser validation 2026-10-19 18:08:49 -04:00
thePR0M3TH3AN 607d5ce1a1 client-reliability-3: preserve shared badge callbacks and guard reentrant disposal 2026-10-19 18:02:46 -04:00
thePR0M3TH3AN 712b05cf87 client-reliability-3: cancel queued torrent probes after last card owner disposes 2026-10-19 17:56:25 -04:00
thePR0M3TH3AN 66832dc3a9 land: area/dispatch 03766d39ef (dispatch) 2026-10-19 17:47:52 -04:00
thePR0M3TH3AN 03766d39ef dispatch-2 client-reliability-3: verify landings and route reproduced obsolete probe bug 2026-10-19 17:47:47 -04:00
thePR0M3TH3AN f9adfc8f53 land: area/client-reliability af16ee1d09 (client-reliability) 2026-10-19 16:35:24 -04:00
thePR0M3TH3AN af16ee1d09 Merge branch 'unstable' into area/client-reliability 2026-10-19 16:35:22 -04:00
thePR0M3TH3AN cee4962166 client-reliability-1 client-reliability-2: close verified landed items 2026-10-19 16:35:21 -04:00
thePR0M3TH3AN de6918cee8 land: area/client-reliability 683cedf0bf (client-reliability) 2026-10-19 16:29:12 -04:00
thePR0M3TH3AN 683cedf0bf client-reliability-1 client-reliability-2: record full 425-file validation and unblock landing 2026-10-19 16:29:08 -04:00
thePR0M3TH3AN 181eae95d9 client-reliability-2: make concurrent-init test hermetic and close feed transport 2026-10-19 16:22:57 -04:00
thePR0M3TH3AN 3968a97922 Merge branch 'unstable' into area/client-reliability 2026-10-19 16:20:34 -04:00
thePR0M3TH3AN 3770eb4186 land: area/dispatch 4a6978ebc4 (dispatch) 2026-10-19 16:20:13 -04:00
thePR0M3TH3AN 4a6978ebc4 Merge branch 'unstable' into area/dispatch 2026-10-19 16:20:12 -04:00
thePR0M3TH3AN 16f6dca41e dispatch-2 client-reliability-2: route retained relay cleanup to existing worker 2026-10-19 16:20:10 -04:00
thePR0M3TH3AN 6da037f59b client-reliability-1: record checked fix and newly exposed full-gate blocker 2026-10-19 16:13:49 -04:00
thePR0M3TH3AN dbdac18df1 client-reliability-1: preserve native hidden-attribute behavior in style fixture 2026-10-19 16:09:26 -04:00
thePR0M3TH3AN d02240786d Merge branch 'unstable' into area/client-reliability 2026-10-19 16:07:49 -04:00
thePR0M3TH3AN be6058b0af client-reliability-1: preserve prior integrity notes alongside modal entry 2026-10-19 16:07:46 -04:00
thePR0M3TH3AN c3fee19059 client-reliability-1: bound modal fixture style work with native focus contract 2026-10-19 16:07:36 -04:00
thePR0M3TH3AN e106a15ea5 land: area/dispatch f303ab0e72 (dispatch) 2026-10-19 16:03:01 -04:00
thePR0M3TH3AN f303ab0e72 dispatch-2: record verified worker launch and preserved local preview 2026-10-19 16:03:01 -04:00
thePR0M3TH3AN 9e6c310d80 land: area/dispatch c312399117 (dispatch) 2026-10-19 16:01:33 -04:00
thePR0M3TH3AN c312399117 Merge branch 'unstable' into area/dispatch 2026-10-19 16:01:11 -04:00
thePR0M3TH3AN 3514c0570d dispatch-2: register approved client-reliability worker and scoped bootstrap gate decision 2026-10-19 16:01:10 -04:00
thePR0M3TH3AN 3e451aac0b land: area/dispatch 8b204687d2 (dispatch) 2026-10-19 15:52:32 -04:00
thePR0M3TH3AN 8b204687d2 dispatch-1: record verified startup landing and close preparation 2026-10-19 15:52:32 -04:00
thePR0M3TH3AN fa538097bd land: area/dispatch 386cf14bbe (dispatch) 2026-10-19 15:52:15 -04:00
thePR0M3TH3AN 386cf14bbe dispatch-1: record fleet goal, baseline, and first worker proposal 2026-10-19 15:52:05 -04:00
thePR0M3TH3AN d90591163f chore: initialize Codex agent fleet on unstable 2026-10-19 15:41:55 -04:00
PR0M3TH3AN 8d4622bb7d docs(todo): add playback reliability items from Nostr feedback 2026-10-14 10:38:33 -04:00
thePR0M3TH3AN 5fdf53deb8 chore: pause Bitvid automated design audit 2026-09-14 18:54:18 -04:00
thePR0M3TH3ANandClaude Fable 5 4dd3eb0330 Add repo memory layer and agent governance
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-12 20:57:52 -04:00
thePR0M3TH3ANandClaude Fable 5 911afb1c05 Split AGENTS.md reference sections into docs/; keep thin agent constitution
Extracts Playwright harness tables and Dark Factory testing standard to
docs/testing/, and dated incident writeups to docs/lessons/, bringing
AGENTS.md under the 32KiB context budget agent harnesses eagerly load.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-12 15:20:08 -04:00
thePR0M3TH3ANandClaude Fable 5 6eb89afcec Dedupe CLAUDE.md testing rules into AGENTS.md pointer; untrack agent memory scratch
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-12 15:15:08 -04:00
thePR0M3TH3ANandClaude Fable 5 c098612c94 Make 'npm test' run the real unit suite
package.json had no "test" script, so `npm test` failed with
"Missing script" and the obvious fallback -- `node --test tests/*.test.mjs`
-- reports ~10 bogus failures. Those files are not broken: the runner
sets NODE_ENV=test (so nostrToolsBootstrap uses the mock toolkit) and
imports tests/test-helpers/setup-localstorage.mjs, and without that
harness they fail on missing globals. tests/modal-accessibility.test.mjs,
for example, is 8/8 under the runner and 0/1 under a bare node --test.

Aliasing test -> run-unit-tests.mjs means the obvious command is also the
correct one, so nobody re-derives that false failure list. CI is
unaffected (it calls test:unit and the shards, which are unchanged).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-04 16:44:47 -04:00
thePR0M3TH3AN 1ddfa95a84 [auth] Vendor BitLogin 30b4f34 and allow WASM in the app CSP
Picks up BitLogin's login-surface expansion (NIP-07 extension, NIP-46
remote signer with nostrconnect QR, passkey sign-in), three rounds of
adversarial-audit fixes, and the mandatory recovery-phrase ceremony for
passkey-created accounts. Vendored via scripts/build-bitlogin-widget.mjs,
which pins the source commit in vendor/bitlogin.integrity.json; the
shared-chunk hashes changed, so three old chunks are removed and three
new ones added.

CSP fix, required for sign-in to work at all: BitLogin's crypto worker
compiles an Argon2id WASM module, and index.html's script-src had no
'wasm-unsafe-eval', so WebAssembly.compile would be blocked and every
BitLogin password flow would fail -- silently, because the page still
loads. This exact omission broke sign-in on bitlogin.network for four
days. The keyword permits WASM compilation only; JS eval stays blocked,
and the CSP comment's stale "no bundle uses wasm" claim is corrected.

tests/csp-policy.test.mjs now pins the keyword for index.html so it
cannot be dropped again. Baseline verified unchanged: npm run test:unit
green, npm run lint clean, and the raw tests/*.test.mjs glob fails the
same 10 pre-existing files before and after this change.

test_integrity_note:
  change_type: ["new_tests"]
  scenarios:
    - id: SCN-csp-allows-wasm-for-bitlogin
      given: "index.html mounts <bitlogin-auth>, whose worker compiles an Argon2id WASM module"
      when: "the page's Content-Security-Policy script-src is evaluated"
      then: "'wasm-unsafe-eval' is present, so WebAssembly.compile is permitted while 'unsafe-eval' remains absent"
  observable_outcomes:
    - "script-src directive parsed from the index.html CSP meta tag contains 'wasm-unsafe-eval'"
    - "existing assertions still forbid 'unsafe-inline' and 'unsafe-eval'"
  determinism_controls:
    - "pure static parse of a committed HTML file; no clock, network, or browser"
  anti_cheat_rationale:
    prevents:
      - "silently relaxing script-src to make sign-in work"
      - "dropping the keyword and shipping a login page that cannot run Argon2id"
  relaxation:
    did_relax_any_assertion: false
    if_true_explain_spec_basis: ""
2026-08-04 16:13:09 -04:00
thePR0M3TH3ANandClaude Opus 5 fbac54dd48 Re-vendor BitLogin at 205b067, and record its provenance
Updates the vendored widget to BitLogin 205b067, which includes the
second-audit hardening the previous bytes predated -- phrase-gated sudo key,
deterministic migration, the pinned derivation chain -- plus the entropy
work: the closed nonce/IV surface, fail-closed RNG tests, and the
package-wide Math.random ban.

Also ports the provenance recording from BitUnlock's vendoring script. The
manifest now carries repository, commit, commit date and toolchain, and the
script refuses to vendor from a BitLogin checkout with uncommitted changes,
resolved before vendorDir is deleted so the refusal leaves the committed
files intact. Hashes prove the bytes have not changed since they were
committed; only the commit says which BitLogin produced them, and version
"0.1.0" has covered many substantive changes.

EXISTING SIGN-INS ARE UNAFFECTED, which matters most here since this is the
one consumer with real users. normalize.ts, argon2id.ts, hkdf.ts and
scalarExpand.ts are byte-identical between the previously vendored build and
205b067, so login name + password derive the same locator, signing scalar
and capsule key as before. Accounts created with a hand-typed password still
sign in: the manual-password removal took away the creation, recovery and
rotation CONTROLS, not the acceptance, and nothing on the login path
inspects the password's shape. BitLogin's legacyPassword.e2e.test.ts pins
that, including the migration path of signing in with an old typed password
and rotating onto a generated one.

What does change for users: creating a new account, recovering, or rotating
now hands out a generated six-word passphrase instead of accepting a typed
one.

2354 unit tests pass, including the 10 bitlogin-specific ones in
auth-providers-bitlogin and bitlogin-modal-integration.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 22:17:08 -04:00
thePR0M3TH3ANandClaude Fable 5 58922e55c3 Record the caching decision: content-hash filenames, then immutable
Option A chosen. Noting explicitly that Option B should not ship as an interim
step -- the two are not stackable, and a bounded max-age shipped now is the
thing that never gets revisited.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 14:53:07 -04:00
thePR0M3TH3ANandClaude Fable 5 5e69fa4a7d Todo: static asset caching, and why "just add immutable" would break it
/css/* and /js/* have no header rule at all, so every stylesheet and script
revalidates on every page load. The obvious fix is a trap: asset URLs are not
content-hashed, so a long max-age with immutable would pin returning users to
stale CSS/JS after each deploy with no way to bust it. Records both real
options and the measured live headers.

Also corrects an earlier hand-off note claiming ~10 failing tests here; all
three suites pass on unstable as of today.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 12:31:36 -04:00
thePR0M3TH3ANandClaude Fable 5 8d40d85079 Delete netlify.toml; the site deploys on Vercel
Vestigial. Verified dead before removing: the live headers on
bitvid.network come from vercel.json, not from this file -- /js/* is
served with Vercel's default `max-age=0, must-revalidate` rather than
the `immutable, max-age=31536000` netlify.toml asks for, which is proof
the file is not consulted by anything deploying this site. It cost real
time today by making the deploy target look like Netlify.

Two rules it carried that vercel.json does NOT, both deliberately left
out rather than silently dropped:
- `Content-Security-Policy: frame-ancestors *` on /*. No behaviour
  change: absence is equally permissive, and the embed page relies on
  being framable, not on the header.
- Long-lived immutable caching for /css/* and /js/*. This one is a real
  (small) performance regression that predates this commit -- the live
  site has been serving those with max-age=0 for as long as it has been
  on Vercel. Worth porting into vercel.json separately; deleting a file
  nothing reads is not the change that should carry it.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 10:14:43 -04:00
thePR0M3TH3ANandClaude Fable 5 847a855ec8 Update the vendored BitLogin widget to the security-audit build
Was on the 2026-07-25 build. Carries an adversarial audit's fixes, the
ones that matter most for a site that hands the widget real identities:

- Origin binding is enforced INSIDE the crypto worker and derived from
  self.location instead of a caller-supplied field, so a page cannot
  read or adopt a wallet connection bound to a different site.
- Phrase recovery no longer trips the rollback alarm on every later
  login (the mark is keyed by the everyday pubkey, which recovery
  preserves, while the recovered capsule restarts at generation 0) --
  users who had ever rotated a password previously saw "possibly
  revoked credentials" forever, with no way to clear it.
- quorumMet counts relays that ANSWERED rather than ones that merely
  accepted a socket, and relay responses are now checked against the
  address requested, so a single junk event can no longer block
  registration, rotation, and recovery.
- Password rotation checks the new capsule reached quorum BEFORE
  publishing the tombstone that destroys the old one; a failed rotation
  used to be able to leave neither password working.
- Derived secrets (Argon2id output, both PRKs, the BIP-39 seed) are now
  wiped at their derivation site, as the spec always claimed.
- The worker RPC validates message shapes, times out, and rejects
  pending calls when the worker dies, so a torn-down worker surfaces as
  an error instead of a permanent hang.
- Synthetic (script-dispatched) clicks no longer drive consent UI.

Verified: the 10 pre-existing test failures in this suite are unchanged
by this update (checked by stashing the vendor directory and re-running
-- 1330 pass / 10 fail either way), and the BitLogin auth-provider
tests pass. dist/ is gitignored build output and regenerates.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-31 10:02:48 -04:00
thePR0M3TH3ANandClaude Fable 5 5cce3b726a docs: close stale innerHTML TODO — ShareNostrModal has no innerHTML left
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHH3wPBsoruwCaJo8Rpf6a
2026-07-30 11:45:23 -04:00
thePR0M3TH3ANandClaude Fable 5 975b996f4c [nostr-core] Pin the NIP-46 multi-account lifecycle contract (TODO #33)
The 2026-06-25 audit blamed "logout logs out everyone" / broken profile
switching / unknown-user-after-refresh on the single shared nip46Client and
single-slot session storage. Re-auditing the current code shows every named
suspect has since been fixed piecemeal: per-account v2 session map with v1
last-connected default, keepStored logout, scoped no-arg clear (v1 slot only),
reuseStored+expectPubkey switch wired end-to-end into
useStoredRemoteSigner({pubkey}), and a validator-enforced boot restore.

None of that was pinned at the manager level, so any layer could regress
silently. tests/nip46-signer-lifecycle.test.mjs now exercises the real
SignerManager against the real signer registry and real (polyfilled) storage:

- logging out the active NIP-46 account destroys its live client exactly once
  while BOTH accounts' stored sessions and the other account's registry signer
  survive
- a failed targeted restore (passphrase-locked or missing session) fails typed
  and deletes nothing
- disconnectRemoteSigner with and without keepStored never touches the
  per-account map

Mutation-checked: reinjecting the historical bugs (logout without keepStored;
no-arg clear wiping the v2 map) fails 2/6; real code passes 6/6.

TODO #33 updated: remaining work is the on-phone Amber verification the item
already gates on — and if that still misbehaves, the failure is now known to
live ABOVE this contract (UI/state reconciliation on restore failure), which
narrows the search considerably.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHH3wPBsoruwCaJo8Rpf6a
2026-07-30 11:33:26 -04:00
thePR0M3TH3ANandClaude Fable 5 2eb7d09865 docs: note that visual baselines are CI-canonical now
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHH3wPBsoruwCaJo8Rpf6a
2026-07-30 11:23:10 -04:00
thePR0M3TH3ANandClaude Fable 5 e99ba0e817 docs: refresh KNOWN_ISSUES against current test reality
- jsdom entry: resolved long ago (28.1.0 installed, in devDeps) — was still
  marked Active with an install remediation.
- webseed-playbook.spec.ts:31: resolved by f48150ed; documents the actual fix
  (record attached srcs instead of asserting the final cleared src).
- uploadModal-reset: not reproducible across 2026-07-28..30 sweeps; left on
  file with instructions if the ordering-dependent hang recurs.
- login-flows.spec.ts:149: corrected — it is NOT a passes-in-isolation flake;
  it failed in isolation on 2026-07-28 (local + CI e2e-headless) while passing
  in the e2e job. Notes the reverted fix attempt and links it to pre-launch
  TODO #33, which is the same subsystem.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHH3wPBsoruwCaJo8Rpf6a
2026-07-30 11:22:21 -04:00
github-actions[bot] ab120a03e1 test(visual): regenerate baselines on ubuntu-latest
Captured by the update-visual-baselines workflow so the baseline environment matches the visual-regression job that compares against them.
2026-07-30 15:16:54 +00:00
thePR0M3TH3ANandClaude Fable 5 5d0709f5f4 [ci] Fix baseline workflow YAML — literal block was terminated early [update-visual-baselines]
The multi-line git commit message inside the run block dropped below the
literal block's indentation, ending the block mid-command and failing the
whole workflow file at parse time (0s failure, no jobs). Use two -m flags on
one shell line instead. YAML now validated locally before push.

Token included so this push performs the regeneration.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHH3wPBsoruwCaJo8Rpf6a
2026-07-30 11:15:26 -04:00
thePR0M3TH3ANandClaude Fable 5 350feae907 [ci] Allow triggering baseline regeneration from unstable [update-visual-baselines]
workflow_dispatch only works for workflows on the default branch, which this
file will not reach until the next promotion. Gate the job on an explicit
[update-visual-baselines] commit-message token instead so it is dispatchable
from unstable now; the workflow's own commit omits the token, so it cannot
retrigger itself.

This commit carries the token, so pushing it performs the first regeneration.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHH3wPBsoruwCaJo8Rpf6a
2026-07-30 11:14:17 -04:00
thePR0M3TH3ANandClaude Fable 5 31be84ba82 [ci] Add a workflow to regenerate visual baselines on the CI runner
The visual-regression job pixel-compares tests/visual/baselines.json on
ubuntu-latest, but the committed baselines were captured on developer machines.
Chromium builds and font fallbacks differ between environments — the token
stack's "Inter"/"Fira Code" are not shipped or installed anywhere, so every
machine renders its own system fallback — and the kitchen-sink default-theme
diff drifted past MAX_DIFF_RATIO on CI while passing 13/13 locally. ci.yml
documents exactly this mismatch as the reason the job is quarantined from
blocking deploys.

The CI diff artifact confirms it: baseline and actual render identically except
for ghosted/offset text in the high-contrast lower half — font-metric drift,
not a design change. (The dark upper half diffs too little to trip the 0.2
pixelmatch threshold, which is why only the default theme fails.)

Fix the class of problem instead of the instance: capture baselines in the SAME
runner image, browser install and font stack that the comparison job uses.
workflow_dispatch only — regenerating baselines is an explicit decision made
when a visual change is intentional, never a side effect of a push. The
workflow re-runs the comparison against its own output before committing, so a
bad capture cannot land.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHH3wPBsoruwCaJo8Rpf6a
2026-07-30 11:11:14 -04:00
thePR0M3TH3ANandClaude Opus 5 f48150ed6b [ci] Fix the stale tests that had CI red
CI has been failing on unstable for weeks, across four unit files and a set of
e2e specs. None of it was a product regression: every failure is a test that
encodes behavior a later commit deliberately changed. Each is corrected against
the spec that superseded it, not adjusted until it passed.

Unit:

- video-modal-moderation: the fixture set flat `blurThumbnail`/`trustedMuteCount`,
  but normalizeVideoModerationContext derives shouldShow from
  `original.blurThumbnail`/`trustedCount`, so the assertion ran against a
  context whose early return had just deleted the blur it was checking. 4525c1c6
  also made a lone trusted mute a non-blocking ranking signal (its sibling test
  was renamed to say exactly that; this one was missed). Fixture now mirrors
  what moderationDecorator emits.
- trustBootstrap: asserted a subscription to onWhitelistChange that 18e08bf2
  deliberately removed -- buildTrustedSeeds derives seeds from the super admin,
  editors and fallback seeds, so a whitelist change cannot alter them. The
  removal is now pinned so it is not reinstated by reflex.
- moderation-service: 18e08bf2 removed `status.whitelisted ||` from
  isTrustedReporter (being allowed to publish is not a statement that your
  reports carry weight). A whitelisted reporter now counts toward the report
  total but not toward trusted.
- playbackService_forcedSource: forced P2P now falls back to a hosted URL when
  the torrent definitively fails -- playbackService documents the change and its
  rollback. The old assertion did not merely fail, it HUNG: forced sources skip
  the start timeout, so the now-reachable hosted attempt waited forever on a
  <video> that never emits.

E2E:

- ui-modals: asserted exactly one `.bv-modal` per component page, but
  profile-modal.html also bundles #storageCorsModal (d7ad1516). Requires at
  least one and drives the primary, which is what the test's own injected
  querySelector already used.
- coverage-utility-modules: used `author:abc123`, which 9afbc0cb correctly began
  rejecting as undecodable. Uses a real pubkey for the success path and pins the
  rejection separately.
- webseed-playback: asserted the FINAL src equals the CDN URL while also
  tolerating "No playable source found" in its status check -- contradictory,
  since a failed attempt calls resetVideoElement() and clears src. Headless CI
  cannot decode the archive.org H.264 sample. Now records src attachments and
  asserts the URL was attached, which is the behavior under test.
- nostr-runtime-coverage: asserted a legacy plaintext session-actor entry reads
  back as null. Since 8692dac6 added per-account storage, readV1Entry PURGES the
  plaintext entry and the reader falls back to the sole remaining encrypted
  account -- so null no longer describes the security property. Asserts the
  property directly: no plaintext key survives, in the entry or in raw storage.

Also drops the syncAuthUiState share-gate wiring added in d141c316. It was
redundant: Application.updateShareNostrAuthState() already fires on init, login,
logout, pubkey-change and signer-change and forwards to
videoModal.setShareNostrAuthState(). That call short-circuited on its own
`if (!this.videoModal?.setShareNostrAuthState) return;` guard purely because the
method did not exist -- adding it (d141c316) was the whole fix. The tests now
pin that contract instead of a second, competing code path.

All 416 unit files, all three CI shards, both DM suites and lint are green.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 17:37:24 -04:00
thePR0M3TH3ANandClaude Opus 5 244f527cf6 [share] Put author and relay hints in the shared nevent
buildShareUrlFromEventId encoded a BARE event id: no author, no relay hints. A
viewer whose relay set did not happen to carry that event could not resolve it,
so bitvid fell back to the home feed -- the "the shared link just opens
bitvid.network" report.

It is not a rare edge case. The reported video was present on nos.lol but
absent from relay.damus.io, relay.snort.social and relay.primal.net -- three of
bitvid's four bootstrap relays -- so most viewers arriving cold could not open
it at all.

The pointer now carries:

- the video's author, pulled from videosMap, which also lets clients look the
  event up by author+kind if the id itself has aged out of a relay
- up to three relay hints from the client's write relays (falling back to its
  general relay list), deduped and scheme-filtered

Both are best-effort: an unknown video or a malformed pubkey still produces the
old bare-id pointer rather than an invalid one, and no relays configured omits
the hint instead of encoding an empty array.

Also restores the path separator. The base URL has its trailing slash stripped,
so shared links read `https://bitvid.network?v=...`. That is legal and browsers
normalize it, but it looks like a typo in a note and some link parsers
mis-detect where the URL ends.

New tests fail 6/9 without the change.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 16:08:34 -04:00
thePR0M3TH3ANandClaude Opus 5 99f8e88969 [playback] Don't yank blocked autoplay into WebTorrent
On mobile a bitvid share link opens with no user gesture, so the browser
refuses autoplay. bitvid handled that correctly -- it showed "Press play to
start the hosted video." and armed a no-stall watchdog to wait -- but the 3s
start timeout kept running underneath and fired anyway. For a video with both a
hosted URL and a magnet that meant:

  Press play to start the hosted video.
  Switching to WebTorrent...

The element was reset out from under the prompt the viewer was about to act on,
and the swarm (often seedless, the webseed being the very URL we just
abandoned) played nothing. Desktop usually hides this because an established
Media Engagement score lets autoplay through, so the same link plays there.

A blocked autoplay is not a stalled source. Suspend the start timeout while we
are waiting on a user gesture and let the attempt settle on its own -- it
resolves when they press play, or when a real media error or stall fires. The
guard is cleared on the `play` gesture so a genuine stall afterwards still
fails over normally.

Reproduced on an emulated phone against the reported video
(v.nostr.build/1eNLjw5el05Uc3KU.mp4, magnet with a webseed): before, statuses
end "Press play..." then "Switching to WebTorrent..." with the P2P path
entered; after, they end at "Press play..." and playback starts on tap. The new
tests fail 2/4 without the guard.

Note for future test authors: createSession() overwrites any
playbackStartTimeout passed in its options with the service's value, so a
per-session override is silently ignored -- set it on the PlaybackService
constructor or the test races the real 3s default and proves nothing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 15:26:44 -04:00
thePR0M3TH3ANandClaude Opus 5 c0d006a5fc [og] Derive the preview origin from the request
SITE_ORIGIN is a constant defaulting to https://bitvid.network, so the unstable
deployment advertised production's canonical URL and, worse, a twitter:player
pointing at production's embed -- testing a preview link silently exercised the
live site.

Build shareUrl, embedUrl and the fallback card from the request's own
proto+host instead, so every deployment (production, unstable, preview) is
self-consistent. BITVID_SITE_ORIGIN remains the fallback when the host header
is absent.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 14:48:32 -04:00
thePR0M3TH3ANandClaude Opus 5 04075ff56b [og] Route crawlers via middleware, not a rewrite
The OG function deployed and worked (`/api/og?v=<nevent>` returns the real
title, description, thumbnail and player card), but crawlers hitting
`/?v=<nevent>` still got the site-wide card: Vercel evaluates `rewrites` AFTER
the filesystem, and `/` matches the static index.html, so the rewrite never
fired.

Middleware runs before the filesystem check, so that is where the routing
belongs. Replace the dead `rewrites` block with a root middleware that matches
only `/`, bails immediately when there is no `v` param or the user-agent is not
a known preview crawler, and otherwise rewrites to /api/og.

Real viewers are unaffected -- they fall through to the untouched SPA. Note the
one behavioral difference from the rewrite design: middleware now executes on
homepage requests (matcher is `/` only) rather than being invoked solely for
crawlers. It returns next() after two cheap checks.

Verified on the live unstable deployment that /api/og already renders
`x-bitvid-og: video` with og:type video.other for the real note; this only
fixes how crawlers reach it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 14:40:20 -04:00
thePR0M3TH3ANandClaude Opus 5 e5784893a8 [build] Drop the stale pnpm-lock.yaml that was failing deploys
Adding api/ made Vercel run a second install phase to bundle the serverless
function. That phase auto-detects the lockfile and ignores the project's
`installCommand: npm install`, so it picked pnpm and failed:

  ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because
  pnpm-lock.yaml is not up to date with package.json

The static build itself succeeded every time -- version injected, deploy
artifact verified -- but the failed function install aborted the whole
deployment, so unstable kept serving the last good build from two days prior.

pnpm-lock.yaml has not been touched since 2026-02-08 and was already stale by
7 added and 4 mismatched dependencies BEFORE this branch (bitcoin-connect,
blossom-client-sdk, cssnano, nostr-wasm, postcss-cli, postcss-import, esbuild,
nostr-tools, jsdom, stylelint); hls.js merely joined the list. It never
mattered because nothing installed from it until now.

Nothing references pnpm: package.json scripts, .github/workflows and
CONTRIBUTING all use npm, and vercel.json already pins
`installCommand: npm install`. package-lock.json is the maintained lockfile.
Keeping a second, unmaintained one only guarantees the two disagree again.

Restore with `git revert` (and regenerate it with `pnpm install
--lockfile-only`) if pnpm is ever adopted deliberately.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 14:31:01 -04:00
thePR0M3TH3ANandClaude Opus 5 d141c316a9 [ui] Make "Share on Nostr" a working button
The share menu's "Share on Nostr" entry was never clickable, no matter who was
logged in -- it rendered as inert, dimmed text.

VideoModal initialized `shareNostrAuthState` to { isLoggedIn: false, hasSigner:
false } and nothing ever wrote to it. createVideoShareMenuPanel gates the item
on `isLoggedIn && hasSigner`, so it was always disabled, and the click
delegation in bindShareMenuActions() bails on `button.disabled` -- so the whole
share-to-Nostr flow behind it was unreachable.

The renderer and the publish path were both already correct; only the gate was
missing:

- VideoModal.setShareNostrAuthState() records the state and closes an already
  open share menu when it changes, so a menu opened before login does not keep
  showing the stale disabled item. It is a no-op when nothing changed, to avoid
  slamming the menu shut on every sync.
- syncAuthUiState() computes the gate from isUserLoggedIn() plus a
  signing-capable active signer, mirroring ShareNostrController.handleShare():
  a signer without signEvent would only produce an error toast on click.
- syncAuthUiState() was only ever called at bootstrap, so logging in
  mid-session left the gate closed. It now self-installs a bitvid:auth-changed
  listener on first run (bound once, re-entrancy safe) rather than adding a
  call site in js/app.js, which is at its file-size cap.

Verified in a browser that the rendered item is disabled when logged out or
signer-less and enabled + click-dispatching with both; the new tests fail 5/9
with the gate push removed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 14:16:18 -04:00
thePR0M3TH3ANandClaude Opus 5 b84558e928 [og] Render per-video OpenGraph tags for crawlers
index.html carries one set of site-wide OG tags, so every /?v=<nevent> link
previewed as the same generic bitvid card -- never the video's title or
thumbnail. bitvid is a static SPA and crawlers do not run JS, so the client
cannot fix this.

api/og.js renders the tags on demand. Nothing is pre-generated and nothing is
stored: the video is a query param, not a path, so one stateless function
serves every video and dist/ is byte-identical with or without it. The only
caching is the CDN edge (s-maxage=300) plus a per-container memo of the admin
lists.

This is never load-bearing. Only crawler user-agents are rewritten to it
(vercel.json), humans get a 302 to the untouched SPA, and a static/nsite deploy
with no functions keeps working -- it just serves the site-wide card. Rollback
is deleting the `rewrites` block.

- Emits twitter:player / og:video:url pointing at embed.html?pointer=<nevent>,
  so a shared link plays inline on Twitter/Discord/Telegram rather than showing
  a still. frame-ancestors * already permits the embed.
- Fails closed. A crawler never authenticates, so anything emitted is public
  forever in third-party caches: deleted, isPrivate, isNsfw/content-warning,
  blacklisted authors, and -- while whitelist mode is on -- an unavailable or
  empty whitelist all fall back to the generic card.
- Escapes every attribute. Titles come from arbitrary nostr events and this HTML
  is served from bitvid's own origin; only http(s) URLs reach a meta tag.
- Parses BOTH event shapes: bitvid's kind-30078 v3 JSON and the NIP-71 tag shape
  (kinds 21/22/34235/34236) that nostube/slidestr publish, where the title is a
  `title` tag and the thumbnail an `imeta image` entry. Reading only the JSON
  shape made every NIP-71 video fall back to the generic card.
- Relay budget is 4s: a cold pool's first read measured ~3s, and 1.5s silently
  degraded every preview. Warm containers answer in <600ms.

Admin-list identity and the relay set are imported from js/config.js,
js/nostr/toolkit.js and js/nostrEventSchemas.js rather than duplicated -- a
hardcoded copy of the namespace gated every preview during development, and a
test now fails if any npub or wss:// literal reappears in the handler.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 13:59:25 -04:00
thePR0M3TH3ANandClaude Opus 5 38b51f13a5 [share] Give share-to-Nostr notes a real preview
"Share on Nostr" published a kind-1 note whose body ended with a bare thumbnail
URL. Many clients only inline such a URL when a NIP-92 `imeta` entry describes
it, so the viewer often saw a plain link instead of a preview image.

- buildShareEvent now emits an `imeta` entry for the thumbnail. The media type
  is inferred from the extension and OMITTED when unknown -- a wrong `m` is
  worse than a missing one, since clients use it to decide whether to render
  inline at all, so extensionless Blossom URLs ship url + alt only.
- inferImageMimeTypeFromUrl lives in a SEPARATE map from EXTENSION_MIME_MAP:
  that map gates video upload/publish paths, and folding image types into it
  would let an image pass a video check.
- shareNostrController was passing only { id, pubkey } to buildShareEvent, so
  the tag could never be built; it now forwards title and thumbnail.

When a video has a NIP-71 mirror, the note instead quotes it with
`nostr:naddr1...`, which nostr clients render as a native video quote card --
the closest thing to a rich preview that does not depend on any client fetching
OpenGraph tags (most never do).

- The mirror is addressable (kind 34235/34236, author = video pubkey,
  d = videoRootId), so js/nostr/mirrorPointer.js derives the coordinate with no
  relay lookup, and the pointer survives the author editing the video where a
  plain `nevent` would rot on the next replace.
- The pointer is only embedded after nip71MirrorService.findMirror() confirms
  the mirror exists. A pointer to a missing event renders as a broken/empty
  quote box, which is worse than the plain-link fallback, so a disabled
  service, a throwing lookup, a 2.5s timeout or a missing videoRootId all fall
  back to the thumbnail note.
- resolveMirrorKind prefers the kinds actually observed on relays and only then
  falls back to the same portrait rule the mirror builder uses, so a derived
  pointer matches what would be published rather than guessing independently.
- While quoting, the raw thumbnail URL is dropped from the body and the imeta
  tag is skipped: the quote card already shows the thumbnail, and emitting both
  rendered the same image twice. An `a` tag links the note to the coordinate.

Known limitation: a client rendering the quote card still has to play the
video, so mirrors whose imeta url is an .m3u8 hit the same HLS wall -- title and
thumbnail render, playback depends on the client.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 13:59:10 -04:00
thePR0M3TH3ANandClaude Opus 5 4404728410 [playback] Add HLS support and stop blaming stale MediaErrors
bitvid attached every hosted source with `videoEl.src = url`, which only plays
HLS on Safari-family browsers. Firefox 146 fails with MEDIA_ERR_SRC_NOT_SUPPORTED
(canPlayType returns ""), so every .m3u8 note -- e.g. anything published by
nostube/slidestr -- was unplayable there and the URL-health badge showed
"offline". Chrome/Brave 149 happen to play HLS natively, which is why this
looked browser-specific.

Vendor hls.js (npm run build:hls) and route HLS sources through it:

- js/services/hlsPlayback.js owns detection, engine choice and the hls.js
  lifecycle. Engine choice keys off MediaSource, NOT canPlayType: Chrome
  returns "maybe" for every HLS type, so a native-first branch would hang.
  Native is used only when MSE is genuinely unavailable.
- The bundle is the FULL build, not hls.js/light -- light drops the alternate
  audio stream controller and real ladders demux audio into an EXT-X-MEDIA
  group. It is lazy-imported only when a .m3u8 is encountered, so ordinary
  MP4/WebTorrent playback never pays the ~500KB.
- modalCoordinator tears the instance down first; clearing `src` alone leaves
  the transmuxing worker and segment fetches running after the modal closes.
- urlHealthController probes HLS by fetching the playlist for #EXTM3U. The old
  bare-<video> probe could not load it and on Chrome emitted no events at all,
  burning the full timeout on every HLS card.
- HLS_PLAYBACK_START_TIMEOUT widens the 3s start window, which fired
  mid-startup: the bundle, master playlist, variant playlist, init segment and
  first media segment are four-plus serial round trips.

Also fixes two bugs found while verifying this:

- A stale MediaError was reported as the current failure. Teardown clears `src`
  and calls load(), leaving MEDIA_ERR_SRC_NOT_SUPPORTED ("Empty src attribute")
  on the element; a later watchdog fallback rendered that as "Hosted playback
  failed: source not supported or blocked." over a stream that was playing
  fine. Only a MediaError produced by the current attempt is surfaced now.
  Sources are also bound before the watchdogs are armed, so hls.js's own
  attachMedia() src swap cannot read as a playback failure.
- `audio/x-mpegurl` is a legacy alias for an HLS playlist of any kind, not an
  audio-only assertion. isAudioOnlyVideoObject misfiled HLS video ladders
  published with it as podcasts and dropped them from every video feed.

Verified end-to-end in Chromium, Chrome 149, Brave 149 and Firefox 146 against
a live HLS stream: Firefox goes from "No playable source found." to playing.
Rollback: stop calling attachHlsSource() and the previous `src` assignment
returns; see docs/playback-fallback.md.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 13:58:55 -04:00
thePR0M3TH3AN 726c0d26ed Update performance plan progress 2026-07-26 18:41:34 -04:00
thePR0M3TH3AN bb6614e3d3 Render cached feeds before recommendation refresh 2026-07-26 18:38:03 -04:00
thePR0M3TH3AN 687f8c438d Optimize feed lifecycle and navigation rendering 2026-07-26 18:27:57 -04:00
thePR0M3TH3AN 2c4aefe872 Document channel follower count scope 2026-07-26 17:59:50 -04:00
thePR0M3TH3AN b774de6d76 Harden profile data loading 2026-07-26 17:59:50 -04:00
thePR0M3TH3AN 5f439d6863 Lazy-load recommendation indexes 2026-07-26 17:59:45 -04:00
thePR0M3TH3AN fe976b3ab7 Harden Nostr startup performance 2026-07-26 17:20:52 -04:00
thePR0M3TH3AN 4525c1c657 Improve moderation restoration and mobile card actions 2026-07-26 15:35:20 -04:00
thePR0M3TH3AN 3735d57410 fix(mobile): keep modal actions above browser chrome 2026-07-26 14:58:09 -04:00
thePR0M3TH3AN 4dd150b010 fix(reactions): load legacy event-tagged video reactions 2026-07-26 14:45:38 -04:00
thePR0M3TH3AN c200c84fc5 test(visual): refresh mobile video modal baseline 2026-07-26 14:38:48 -04:00
thePR0M3TH3AN d19040c625 chore(deploy): enable unstable Vercel deployments 2026-07-26 14:32:31 -04:00
thePR0M3TH3AN 5764482f2a feat(mobile): redesign profile settings navigation 2026-07-26 13:50:17 -04:00
thePR0M3TH3AN 4b975c17dc Merge unstable into beta before NIP-71 promotion 2026-07-26 12:53:47 -04:00
thePR0M3TH3AN 87b3012e34 Merge main into beta before NIP-71 promotion 2026-07-26 12:53:44 -04:00
thePR0M3TH3AN e8e2b04de7 fix(nip71): keep upload mirrors in sync 2026-07-26 12:51:00 -04:00
thePR0M3TH3AN 2f3028f2e4 fix: keep autoplay fallback from saving mute
(cherry picked from commit f373bb9ce7)
2026-07-26 12:34:15 -04:00
thePR0M3TH3AN f0a3ede3e5 Preserve autoplay preference on fallback; reload open comment thread after login
### Motivation

- Prevent a browser-policy muted-autoplay retry from overwriting a viewer's explicit mute/unmute preference and ensure fallback mutes are transient.
- Ensure that when a user logs in while a video comment modal is open, the open thread is reloaded (not just refreshed) so comments continue to display correctly after relay-backed services are reconfigured.

### Description

- Add `js/utils/autoplayPreference.js` with `muteForAutoplayFallback` and `consumeAutoplayFallbackMute` helpers to mark and consume transient fallback mutes.
- Update `js/services/playbackService.js` to treat sound as default unless the viewer explicitly chose mute, persist explicit choices via `localStorage`, ignore fallback-generated volume changes using `consumeAutoplayFallbackMute`, and expose `muteForAutoplayFallback` delegating to the new util.
- Replace direct `videoElement.muted = true` calls with `playbackService.muteForAutoplayFallback` in `js/app/playbackCoordinator.js` and `js/webtorrent.js` so fallback mutes are marked as transient.
- Change `js/app/authSessionCoordinator.js` to reload the currently open video comment thread with `commentController.load(activeModalVideo)` after login if a modal video is active, instead of only calling `refreshAuthState`.
- Update tests in `tests/services/playbackService.test.mjs` and add a test in `tests/app/login-defers-dms.test.mjs` to cover the login behavior that reloads an open comment thread.

### Testing

- Ran the `PlaybackService` unit tests (`tests/services/playbackService.test.mjs`) which assert `prepareVideoElement` behavior, explicit mute persistence, and the transient nature of fallback mute, and they passed.
- Ran the auth login unit tests including `tests/app/login-defers-dms.test.mjs` which verifies DM deferral and reload of an open comment thread, and they passed.

(cherry picked from commit ce14004725)
2026-07-26 12:33:57 -04:00
thePR0M3TH3AN d8e5113efd fix: keep autoplay fallback from saving mute
(cherry picked from commit f373bb9ce7)
2026-07-26 12:33:57 -04:00
thePR0M3TH3AN 04c8511a50 Merge pull request #2787 from PR0M3TH3AN/codex/enable-sound-by-default-in-player-2ks9wb
Whitelist-scoped Most Zapped feed, autoplay-fallback mute handling, BitLogin DOM hardening, and auth-thread reload
2026-07-26 09:23:35 -04:00
thePR0M3TH3AN ce14004725 Preserve autoplay preference on fallback; reload open comment thread after login
### Motivation

- Prevent a browser-policy muted-autoplay retry from overwriting a viewer's explicit mute/unmute preference and ensure fallback mutes are transient.
- Ensure that when a user logs in while a video comment modal is open, the open thread is reloaded (not just refreshed) so comments continue to display correctly after relay-backed services are reconfigured.

### Description

- Add `js/utils/autoplayPreference.js` with `muteForAutoplayFallback` and `consumeAutoplayFallbackMute` helpers to mark and consume transient fallback mutes.
- Update `js/services/playbackService.js` to treat sound as default unless the viewer explicitly chose mute, persist explicit choices via `localStorage`, ignore fallback-generated volume changes using `consumeAutoplayFallbackMute`, and expose `muteForAutoplayFallback` delegating to the new util.
- Replace direct `videoElement.muted = true` calls with `playbackService.muteForAutoplayFallback` in `js/app/playbackCoordinator.js` and `js/webtorrent.js` so fallback mutes are marked as transient.
- Change `js/app/authSessionCoordinator.js` to reload the currently open video comment thread with `commentController.load(activeModalVideo)` after login if a modal video is active, instead of only calling `refreshAuthState`.
- Update tests in `tests/services/playbackService.test.mjs` and add a test in `tests/app/login-defers-dms.test.mjs` to cover the login behavior that reloads an open comment thread.

### Testing

- Ran the `PlaybackService` unit tests (`tests/services/playbackService.test.mjs`) which assert `prepareVideoElement` behavior, explicit mute persistence, and the transient nature of fallback mute, and they passed.
- Ran the auth login unit tests including `tests/app/login-defers-dms.test.mjs` which verifies DM deferral and reload of an open comment thread, and they passed.
2026-07-26 09:23:05 -04:00
thePR0M3TH3AN 4458b269be Merge pull request #2786 from PR0M3TH3AN/codex/enable-sound-by-default-in-player
Preserve autoplay preference on fallback; reload open comment thread after login
2026-07-26 08:59:35 -04:00
thePR0M3TH3AN f373bb9ce7 fix: keep autoplay fallback from saving mute 2026-07-26 08:58:58 -04:00
thePR0M3TH3AN 2cda5025d4 fix(zaps): scope discovery to approved creators 2026-07-25 23:24:49 -04:00
thePR0M3TH3AN 7739a934f9 docs: update zap progress tracking 2026-07-25 23:18:09 -04:00
395 changed files with 49282 additions and 6135 deletions

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
# Repo Memory
Curated, reviewed knowledge for coding agents working in this repository.
Memory is an accelerator, not a second source of truth.
## Precedence (highest first)
1. Source code and tests
2. Executable configuration
3. Accepted design records / docs
4. `AGENTS.md`
5. This memory
If memory conflicts with anything above it, the memory is stale — propose a fix.
## Topics
- `memory/architecture.md` — durable architectural decisions and constraints
- `memory/conventions.md` — validated conventions not obvious from the code
- `memory/lessons.md` — hard-won lessons and recurring landmines
- `memory/decisions.jsonl` — structured decision records (one JSON object per line)
## Write policy
Agents MAY read everything here and MAY add candidates under `proposals/`
(one file per proposal, named `YYYY-MM-DD-<slug>.md`, with evidence links).
Agents MUST NOT edit `memory/` directly; trusted memory changes only through
reviewed commits that promote a proposal.
## Prohibited content
Credentials or secrets, personal data, raw transcripts or tool logs, session
TODOs, high-frequency agent state, unverified external claims.
+17
View File
@@ -0,0 +1,17 @@
# Architecture
Durable architectural decisions and constraints for bitvid.
## AGENTS.md is a thin constitution; long-form standards live in docs/
On 2026-08-12 AGENTS.md was split into a thin agent constitution plus
reference docs under `docs/testing/` and `docs/lessons/`, because the file had
outgrown the 32 KiB harness context budget. AGENTS.md keeps only the invariant
and a pointer per topic; new long-form material belongs in `docs/`, not in
AGENTS.md.
Authoritative source:
- docs/testing/test-integrity.md
- docs/testing/agent-playwright-harness.md
- docs/lessons/
- git commit 911afb1c ("Split AGENTS.md reference sections into docs/; keep thin agent constitution")
+5
View File
@@ -0,0 +1,5 @@
# Conventions
Validated conventions not obvious from the code.
No entries yet.
+1
View File
@@ -0,0 +1 @@
{"kind":"decision","subject":"docs/agents-md-split","summary":"AGENTS.md was split on 2026-08-12 into a thin agent constitution plus reference docs under docs/testing/ and docs/lessons/ to stay within the 32 KiB harness context budget; AGENTS.md keeps only invariants and pointers, and new long-form material belongs in docs/.","status":"active","evidence":[{"type":"repository","ref":"docs/testing/test-integrity.md"},{"type":"repository","ref":"docs/testing/agent-playwright-harness.md"},{"type":"repository","ref":"docs/lessons/"},{"type":"git","ref":"911afb1c"}],"created":"2026-08-12","supersedes":[]}
+24
View File
@@ -0,0 +1,24 @@
# Lessons
Hard-won lessons and recurring landmines.
## Cloudflare/S3 upload-path gotchas
`js/services/r2Service.js` and `js/services/s3UploadService.js` are near-identical
copies that must be changed in lockstep, and storage keys must stay
content-addressed (`computeStorageContentHash`) or URL-first uploads silently
overwrite objects. Full audited writeup in the doc below.
Authoritative source:
- docs/lessons/cloudflare-s3-upload-gotchas.md
## NIP-07 signer reliability
"DMs / lists / watch-history won't load after login" is almost always an
unresponsive NIP-07 signer extension, not bitvid — diagnose with a raw
`window.nostr` probe first, and never regress the five resilience invariants
(capped relay fan-out, NIP-07 circuit breaker, re-thrown transient decrypt
errors, generous decrypt budgets, lazy modal population).
Authoritative source:
- docs/lessons/nip07-signer-reliability.md
+8
View File
@@ -0,0 +1,8 @@
# Agent plans
Durable, multi-session execution plans only — the canonical backlog stays in
the repo's TODO file or GitHub Issues, and session-local checklists are never
committed. One plan per task, named `<ticket-or-slug>.md`, in `active/`;
move finished plans to `archive/`. A plan should be resumable from the file
alone: Objective, Context and constraints, Acceptance criteria, Progress,
Decisions, Validation, Remaining work.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+7
View File
@@ -0,0 +1,7 @@
# Memory proposals
Agents: drop candidate memories here, one file per proposal, named
`YYYY-MM-DD-<slug>.md`. State the claim, the evidence (file paths, commits,
test results), and why it is durable. A human reviews and either promotes it
into `../memory/` in a reviewed commit or deletes it. Nothing in this
directory is trusted context.
+29
View File
@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Repo Memory Decision Record",
"type": "object",
"additionalProperties": false,
"required": ["kind", "subject", "summary", "status", "evidence", "created"],
"properties": {
"kind": {"enum": ["decision", "constraint", "lesson", "observation", "workaround"]},
"subject": {"type": "string", "minLength": 1},
"summary": {"type": "string", "minLength": 1, "maxLength": 4096},
"status": {"enum": ["proposed", "active", "superseded", "expired"]},
"evidence": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["type", "ref"],
"properties": {
"type": {"enum": ["repository", "git", "issue", "test", "external"]},
"ref": {"type": "string", "minLength": 1}
}
}
},
"created": {"type": "string"},
"expires": {"type": ["string", "null"]},
"supersedes": {"type": "array", "items": {"type": "string"}}
}
}
+12
View File
@@ -0,0 +1,12 @@
---
name: agent-fleet
description: Use fleet-mcp tools for backlog, mailbox, landing queue, and area ownership. Do not use it to start sessions or push main.
---
Follow the fleet rules in `AGENTS.md`. Call the `agent-fleet` MCP tools for backlog, mailbox, landing-queue, area questions, and commits. Editing those files directly is also fine. Sandboxed Codex cannot write Git's index: use the `commit` tool (or `fleet commit -m "..." --area <id> -- <paths>`), not `git commit`. `submit: true` queues the landing and starts a drain. Go back to work. At your next natural checkpoint, `land_status` (or `fleet landq status <id>`) can report that submission. Waiting is optional. Do not infer the lander or the land result from silence. Other providers may use either commit path.
If you installed this fleet, you stay on as the interpreter. Read `docs/agents/prompts/interpreter.md`. Run `fleet up` yourself. Talk to the session manager with `tell` and read replies with `hear`. Do not scrape the tmux pane, and do not send the user to a terminal. A routine yes you relay counts. Money, pushes, deploys, deletes, secrets, outgoing messages, and a bigger structural or directional change go to the user. Uncommitted setup files use `setup_commit`, not `git commit`. Workers do not start the fleet. `fleet crontab` stays unused unless the user asks.
You may use this harness's own subagents for parallel research, investigation, test runs, or review. You coordinate edits in a worktree so subagents do not edit the same files at the same time. Landing still goes through your `commit` and the queue.
To update the kit: `git pull` in `~/agent-fleet-kit`, reinstall, run `fleet update`, and restart agents after handoff if it says so. Do not re-run init to upgrade.
+6
View File
@@ -0,0 +1,6 @@
# Agent instruction and memory surfaces require maintainer review
/AGENTS.md @PR0M3TH3AN
/CLAUDE.md @PR0M3TH3AN
/.agents/ @PR0M3TH3AN
/SECURITY.md @PR0M3TH3AN
/.github/workflows/agent-governance.yml @PR0M3TH3AN
+65
View File
@@ -0,0 +1,65 @@
name: Agent governance
on:
push:
paths:
- "AGENTS.md"
- "CLAUDE.md"
- ".agents/**"
pull_request:
paths:
- "AGENTS.md"
- "CLAUDE.md"
- ".agents/**"
jobs:
lint-agent-artifacts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate agent artifacts
run: |
python3 - <<'EOF'
import json, os, sys
hard_fail = False
# decisions.jsonl: every line must be valid JSON with required keys
path = ".agents/memory/decisions.jsonl"
if os.path.exists(path):
required = {"kind", "subject", "summary", "status", "evidence", "created"}
for i, line in enumerate(open(path, encoding="utf-8"), 1):
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except json.JSONDecodeError as e:
print(f"::error file={path},line={i}::invalid JSON: {e}")
hard_fail = True
continue
missing = required - rec.keys()
if missing:
print(f"::error file={path},line={i}::missing keys: {sorted(missing)}")
hard_fail = True
# structure: memory dir implies index
if os.path.isdir(".agents") and not os.path.exists(".agents/MEMORY.md"):
print("::error::.agents/ exists but .agents/MEMORY.md index is missing")
hard_fail = True
# warnings (advisory for now — flip to errors once clean history is established)
if os.path.exists("AGENTS.md"):
size = os.path.getsize("AGENTS.md")
if size > 32768:
print(f"::warning file=AGENTS.md::{size} bytes exceeds the 32 KiB context budget")
if os.path.exists("CLAUDE.md"):
lines = sum(1 for _ in open("CLAUDE.md", encoding="utf-8"))
if lines > 40:
print(f"::warning file=CLAUDE.md::{lines} lines — expected a thin wrapper over AGENTS.md")
for f in ("architecture.md", "conventions.md", "lessons.md"):
p = f".agents/memory/{f}"
if os.path.exists(p) and os.path.getsize(p) > 25 * 1024:
print(f"::warning file={p}::exceeds 25 KB — curate or split")
sys.exit(1 if hard_fail else 0)
EOF
@@ -1,8 +1,6 @@
name: Daily Design System Audit
on:
schedule:
- cron: '0 9 * * *' # Run daily at 09:00 UTC
workflow_dispatch:
permissions:
@@ -0,0 +1,95 @@
# Regenerate the visual-regression baselines IN the environment that compares
# them.
#
# The baselines in tests/visual/baselines.json are pixel-compared on
# ubuntu-latest CI, but they were historically captured on a developer machine.
# Chromium builds and font fallbacks differ between the two ("Inter"/"Fira
# Code" are not shipped or installed anywhere, so every environment renders its
# own system fallback), so the kitchen-sink diff drifted past MAX_DIFF_RATIO on
# CI while passing locally — ci.yml even documents the mismatch as the reason
# visual-regression must not block deploys. Capturing the baselines on the same
# runner image, browser install and font stack as the comparison removes the
# environment variable from the equation entirely.
#
# Two explicit triggers, both requiring deliberate human intent — regenerating
# baselines must never be a side effect of ordinary work:
#
# 1. workflow_dispatch — only usable once this file reaches the default
# branch (GitHub does not index dispatchable workflows on other branches).
# 2. A push whose head commit message contains [update-visual-baselines] —
# works from unstable today, before any promotion. The workflow's own
# commit omits the token, so it cannot retrigger itself.
name: Update visual baselines
on:
workflow_dispatch:
push:
branches: [unstable]
permissions:
contents: write
jobs:
update-baselines:
if: >-
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, '[update-visual-baselines]')
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Check out repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Node.js 22
uses: actions/setup-node@v5
with:
node-version: 22
# Mirror the visual-regression job's caches so the regenerated baselines
# come from the same dependency tree the comparison job will use.
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-22-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-22-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-22-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-22-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Regenerate baselines
run: npm run test:visual:update
- name: Verify the fresh baselines pass in this environment
run: npx playwright test --project=visual
- name: Commit updated baselines
run: |
if git diff --quiet -- tests/visual/baselines.json; then
echo "Baselines unchanged; nothing to commit."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add tests/visual/baselines.json
git commit \
-m "test(visual): regenerate baselines on ubuntu-latest" \
-m "Captured by the update-visual-baselines workflow so the baseline environment matches the visual-regression job that compares against them."
git push
+12
View File
@@ -22,3 +22,15 @@ TEST_LOG.mdtest_logs/nip-hits.txt
# Local site archive for manual nsite upload (npm run archive)
bitvid-site.zip
.env*
# Agent runtime state (local only)
memory-update.md
memory-update.json
# Agent-local memory state (never committed)
.agents/private/
.agents/state/
fleet/inbox/
.task
.fleet/
+24 -247
View File
@@ -65,12 +65,8 @@ For any nostr related work, please review the nip documentation located in /docs
### 4a. Cloudflare/S3 upload-path gotchas (audited 2026-06-16)
* **Two near-identical upload services exist** — `js/services/r2Service.js` (Cloudflare R2) and `js/services/s3UploadService.js` (generic S3). They share `buildR2Key`, `resolveUploadIdentifier`, and the magnet build but are *copies*. **Any fix to one must be mirrored in the other** or they silently drift.
* **Storage-key collisions = data loss — FIXED 2026-06-16.** `buildR2Key` (`js/r2.js`) builds `u/<npub>/<namespace>/<slug>.<ext>` where `namespace` used to fall back to the literal `"uploads"` and `slug` to `"video"` (e.g. a non-ASCII filename, or no info-hash) — so a URL-first upload with no info-hash and a duplicate filename overwrote the previous object while the old note still pointed at that URL. WebTorrent uploads were always safe (info-hash namespaces the key = content-addressed). Fix: `computeStorageContentHash(file)` (`js/r2.js`) derives a content-based namespace when no info-hash is available (full SHA-256 for ≤512 MB, sampled metadata+edge fingerprint above that to avoid buffering huge files), wired into **both** `r2Service` and `s3UploadService`. Test: `tests/storage-key-collision.test.mjs`. **If you add a third upload path, reuse `computeStorageContentHash`.**
* **Whole-file hash before upload.** When the modal doesn't pre-seed an info-hash, `resolveUploadIdentifier` runs `calculateTorrentInfoHash(file)`, which reads the *entire* file before the upload begins and before any progress is emitted — on multi-GB files this looks like a hang. Emit a "preparing/hashing" status or parallelize.
* **Publish can report success unconfirmed.** `publishEventToRelay` (`js/nostrPublish.js`) has an optimistic-success fallthrough when the relay handle exposes no `on()`/`then()` — a note may show "Published" with no relay ACK. Don't tighten blindly (legacy `seen`-only relays rely on it), but log it.
* **Silent partial failures.** Thumbnail and `.torrent` upload failures are caught and `warn`-only; the video note still publishes with no user signal. Empty (0-byte) files hit `CompleteMultipartUpload` with `Parts:[]` and surface a cryptic error — guard `file.size === 0`.
* **Object cleanup (deletes/edits).** `R2Service.deleteVideoStorage()` removes the backing objects so "deleted" videos aren't left publicly downloadable. Contract to preserve: it's **best-effort and never throws** (must not block note deletion), it derives keys via `collectVideoStorageKeys()` and **only ever touches URLs under the owner's bucket base** (never external/3rd-party URLs), and it skips cleanly when storage is locked/credential-less. Wired into delete (`app.handleDeleteModalConfirm`) and edit-with-replaced-URL (`editModalController.handleSubmit`). Edit cleanup intentionally does **not** delete thumbnails (the new note may still reference them) and only fires when both old and new URLs are present and different.
**Invariants:** `js/services/r2Service.js` and `js/services/s3UploadService.js` are near-identical copies — any fix to one must be mirrored in the other; storage keys must stay content-addressed (reuse `computeStorageContentHash` in `js/r2.js` for any new upload path); `R2Service.deleteVideoStorage()` stays best-effort, never throws, and only touches URLs under the owner's bucket base.
Full writeup: [`docs/lessons/cloudflare-s3-upload-gotchas.md`](docs/lessons/cloudflare-s3-upload-gotchas.md).
---
@@ -324,127 +320,8 @@ Three automated lint checks protect the codebase from common growth problems. Th
## 14. Playwright Agent Testing Infrastructure
bitvid includes a test harness and fixtures that let Playwright CLI agents programmatically log in, seed relay data, and inspect app state — no browser extensions or real relays required.
### Activating Test Mode
Add `?__test__=1` to the URL, or set `localStorage.__bitvidTestMode__ = "1"` via `addInitScript`. This installs `window.__bitvidTest__` on the page.
### Overriding Relays
Point the app at a local mock relay instead of production:
```
?__test__=1&__testRelays__=ws://127.0.0.1:8877
```
Or set `localStorage.__bitvidTestRelays__` to a JSON array of relay URLs.
### Test Harness API (`window.__bitvidTest__`)
| Method | Returns | Purpose |
|--------|---------|---------|
| `loginWithNsec(hexKey)` | `Promise<string>` (pubkey) | Programmatic login, bypasses the modal |
| `logout()` | `void` | Clear active signer |
| `getAppState()` | `{ isLoggedIn, activePubkey, relays, ... }` | Inspect current state |
| `getFeedItems()` | `Array<{ title, pubkey, dTag, hasUrl, hasMagnet }>` | Scrape video cards from DOM |
| `waitForFeedItems(n, ms)` | `Promise<Array>` | Wait for N cards to appear |
| `waitForSelector(sel, ms)` | `Promise<true>` | Wait for a DOM element |
| `getRelayHealth()` | `{ relays, unreachable, backoff }` | Relay connection status |
| `applyRelayOverrides(urls)` | `boolean` | Redirect relay connections |
| `setTestRelays(urls)` | `{ ok, relays }` | Align relayManager + nostrClient to a deterministic relay set |
| `setSignerDecryptBehavior(mode, opts?)` | `{ ok, mode, ... }` | Force decrypt behavior (`passthrough`, `timeout`, `error`, `delay`) for signer-based list sync tests |
| `getListSyncEvents()` | `Array<{ source, at, detail }>` | Read captured sync timeline (`bitvid:auth-loading-state` + `userBlocks` status) |
| `clearListSyncEvents()` | `void` | Reset captured sync events between assertions |
| `waitForListSyncEvent(criteria, ms?)` | `Promise<Event>` | Wait for sync milestones (`status`, `reason`, `source`) |
| `nostrClient` | `NostrClient` | Direct access for advanced use |
### Mock Relay (`scripts/agent/simple-relay.mjs`)
Start with `startRelay(port, { httpPort })`. Alongside the Nostr WebSocket protocol, exposes an HTTP API:
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/seed` | `POST` | Inject signed events (single object or array) |
| `/events` | `GET` | List all stored events |
| `/events` | `DELETE` | Clear all events |
| `/health` | `GET` | Event count + connection count |
Programmatic API: `relay.seedEvent(event)`, `relay.clearEvents()`, `relay.getEvents()`.
### Playwright Fixture (`tests/e2e/helpers/bitvidTestFixture.ts`)
Import `test` and `expect` from the fixture for tests that need the full stack:
```typescript
import { test, expect } from "./helpers/bitvidTestFixture";
test("agent can seed and view videos", async ({ page, gotoApp, loginAs, seedEvent }) => {
await seedEvent({ title: "Test Video", url: "https://example.com/v.mp4" });
await gotoApp();
await loginAs(page);
// ... assertions
});
```
**Available fixture values:**
| Fixture | Type | Purpose |
|---------|------|---------|
| `relay` | Relay instance | Auto-started/stopped per test |
| `seedEvent(video)` | `(TestVideoEvent) => Promise` | Create a signed kind 30078 event and inject it |
| `seedRawEvent(event)` | `(any) => Promise` | Inject a pre-built event |
| `clearRelay()` | `() => Promise` | Wipe all relay events |
| `gotoApp(path?)` | `(string?) => Promise` | Navigate with test mode + relay overrides |
| `loginAs(page)` | `(Page) => Promise<string>` | Login with deterministic test key |
| `setTestRelays(page, relays)` | `(Page, string[]) => Promise<{ok, relays}>` | Force relay alignment through harness + relayManager |
| `setDecryptBehavior(page, mode, opts?)` | `(Page, mode, opts?) => Promise` | Force signer decrypt behavior (`passthrough`, `timeout`, `error`, `delay`) |
| `startDiagnostics(page, opts?)` | `(Page, opts?) => Promise<{stop()}>` | Capture console/page errors and list-sync telemetry for assertions |
| `testPubkey` | `string` | Hex pubkey of the test key |
| `relayUrl` | `string` | WebSocket URL of the mock relay |
### `data-testid` Selectors
Use these for stable element targeting:
| Selector | Element |
|----------|---------|
| `[data-testid="login-button"]` | Header login button |
| `[data-testid="upload-button"]` | Header upload button (hidden until logged in) |
| `[data-testid="profile-button"]` | Header profile button (hidden until logged in) |
| `[data-testid="profile-permission-prompt"]` | Profile list-sync permission CTA container |
| `[data-testid="profile-permission-prompt-button"]` | Profile list-sync permission/retry CTA button |
| `[data-testid="search-input"]` | Header search field |
| `[data-testid="login-modal"]` | Login modal container |
| `[data-testid="login-provider-button"]` | Login provider option buttons |
| `[data-testid="nsec-secret-input"]` | Private key textarea in nsec login |
| `[data-testid="nsec-submit"]` | Nsec login submit button |
| `[data-testid="upload-modal"]` | Upload modal container |
| `[data-testid="upload-title"]` | Video title input |
| `[data-testid="upload-url"]` | Video URL input |
| `[data-testid="upload-magnet"]` | Magnet link input |
| `[data-testid="upload-submit"]` | Publish button |
| `[data-testid="hashtags-sync-status"]` | Profile hashtag sync status message |
| `[data-testid="subscriptions-sync-status"]` | Profile subscriptions sync status message |
| `[data-testid="blocked-refresh-button"]` | Profile blocked creators refresh button |
| `[data-testid="blocked-sync-status"]` | Profile blocked creators sync status message |
| `[data-testid="blocked-list"]` | Blocked creators list container |
| `[data-testid="blocked-empty-state"]` | Blocked creators empty state |
| `[data-testid="video-modal"]` | Video player modal |
| `[data-testid="video-card"]` | Individual video card in the feed |
| `[data-testid="video-list"]` | Video feed grid container |
### Key Files
| File | Purpose |
|------|---------|
| `js/testHarness.js` | Test harness module (installs `window.__bitvidTest__`) |
| `scripts/agent/simple-relay.mjs` | Mock relay with HTTP seeding API |
| `tests/e2e/helpers/bitvidTestFixture.ts` | Reusable Playwright fixture |
| `tests/e2e/agent-testability.spec.ts` | Infrastructure validation tests |
| `scripts/playwright/run-extension-persistent.mjs` | Dedicated persistent-context launcher for extension smoke tests |
| `docs/testing/playwright-integration-recommendations-2026-02-22.md` | Playwright/extension integration recommendations and follow-ups |
| `docs/testing/playwright-function-coverage.md` | Playwright coverage integration, latest baseline metrics, and improvement plan |
bitvid ships a test harness (`window.__bitvidTest__`, enabled via `?__test__=1`), a mock relay (`scripts/agent/simple-relay.mjs`), and a reusable Playwright fixture (`tests/e2e/helpers/bitvidTestFixture.ts`) so agents can programmatically log in, seed relay data, and inspect app state — no browser extensions or real relays required.
Full reference (harness API, mock relay, fixtures, `data-testid` selectors): [`docs/testing/agent-playwright-harness.md`](docs/testing/agent-playwright-harness.md).
---
@@ -553,137 +430,37 @@ When finishing a task or handing off, provide:
## 16. Scenario-First Tests & Test Integrity (Dark Factory Standard)
In this repo, **validation replaces code review**. That means our test/scenario system is the *only* thing standing between “working software” and “green-but-worthless software.”
Agents will naturally optimize for short-term goals (e.g., “get CI green”) unless we constrain them. If we ever allow “edit tests until they pass,” we destroy the signal, and over time the suite becomes meaningless.
### Core philosophy: scenarios > checklists
We treat tests as **behavioral specifications** expressed as **scenarios**:
- “User stories” / end-to-end behaviors (Given/When/Then or equivalent)
- Assertions focus on **externally observable outcomes** at boundaries (API responses, persisted state, emitted events, CLI output, UI state), not internal call sequences.
- We prefer *minimal coupling* to implementation details so refactors don’t require rewriting “truth.”
(StrongDM reached the same conclusion: repo-local tests are easy to reward-hack; scenarios and satisfaction-based validation reduce “teaching to the test.”)
### Non-negotiable constitution (anti-cheat)
**Hard rules (always on):**
1) **Never weaken, delete, or rewrite a test just to make CI pass.**
2) **Never change expected behavior to match buggy output** (“rubber-stamp the snapshot”, “update golden”, “adjust assertion”) unless it is a **spec correction** (see below).
3) Do not “fix” flaky tests with retries, sleeps, timeouts, or looser assertions. **Remove nondeterminism** instead (control time, randomness, IO, network).
4) Prefer black-box assertions at system boundaries; avoid tests that merely mirror internal logic.
### Definitions
- **Scenario**: a behavioral spec (often end-to-end) describing what must happen for a user/system in a real environment.
- **Invariant**: a property that must always hold across many inputs/states (great for property-based or metamorphic testing).
- **Holdout scenario**: an evaluation scenario kept outside the agent-editable area to reduce overfitting / reward-hacking.
### When tests may change (spec correction protocol)
Changing a test expectation is allowed **only** when the test was enforcing the wrong behavior.
If you believe a test should change:
1) Identify the **scenario/spec** that defines correct behavior.
2) Explain precisely why the old expectation was wrong.
3) Replace it with an **equally strict or stricter** behavior-based check.
4) Record a **Test Integrity Note** (below) and, if applicable, a short spec note (e.g., `docs/spec_changes/<date>-<slug>.md`).
If you cannot point to scenario/spec truth, **do not change the test**. Add a “Needs Spec Clarification” note instead.
### “Test Integrity Note” (required for any PR touching tests)
Every PR that adds/changes tests must include this machine-readable block in the PR description (or in a `TEST_INTEGRITY.md` entry referenced by the PR):
```yaml
test_integrity_note:
change_type: ["new_tests" | "refactor_tests" | "spec_correction" | "flake_fix"]
scenarios:
- id: SCN-<slug>
given: "<preconditions>"
when: "<stimulus>"
then: "<observable outcomes>"
observable_outcomes:
- "<what the user/system can observe at the boundary>"
determinism_controls:
- "<fake clock / fixed seed / hermetic env / service virtualization>"
anti_cheat_rationale:
prevents:
- "hard-coded return value"
- "over-mocking internal logic"
- "snapshot rubber-stamping"
- "retry/sleep-based flake masking"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
### Test portfolio guidance (scenario-first, still layered)
We want a balanced suite:
- **Scenario/E2E**: user-story validation at boundaries (few, high value, high fidelity)
- **Integration/contract**: service boundaries, serialization, persistence, workflows
- **Unit/invariants**: properties that kill trivial cheats (“return true” shouldn’t survive)
For cheat resistance, strongly prefer:
- invariants/property-based tests for critical logic
- metamorphic relations where “exact expected output” is hard
- periodic mutation testing to “test the tests” (surviving mutants indicate missing assertions)
### External dependency realism (service virtualization / “digital twins”)
Where third-party services are involved, we prefer deterministic “behavioral clones” (mocks/stubs at the API boundary that reproduce edge cases and contracts) over live calls. This keeps scenarios realistic without being flaky or rate-limited, and it makes high-volume scenario validation affordable.
### Role: Test Integrity & Scenario Spec Agent (Enforcer)
This repo includes (or assumes) a dedicated agent role whose only job is to protect validation integrity.
This agent’s goal is NOT “make CI green.” Its goal is “make the suite reflect reality and reject fake passes.”
**Authority:**
- May add new scenarios, invariants, and tests.
- May refactor tests to be more behavioral and less procedural.
- May improve determinism (fixed seeds, fake clocks, hermetic env).
- May only change expectations via the Spec Correction Protocol above.
**Prohibitions:**
- Must never weaken tests to pass.
- Must never edit holdout scenarios (if configured).
- Must never solve flakiness via retries/sleeps/loosening.
**Deliverables each run:**
- Scenario list (Given/When/Then)
- Proposed test diffs
- Test Integrity Note
- “Cheat vectors blocked” summary (what trivial implementations it prevents)
**Invariant:** validation replaces code review in this repo — never weaken, delete, or rewrite a test merely to make it pass, and never mask flakiness with retries/sleeps/looser assertions. Expectation changes are allowed only via the Spec Correction Protocol with an accompanying Test Integrity Note.
Read the full standard before adding or modifying any test: `docs/testing/test-integrity.md`.
---
## 17. NIP-07 Signer Reliability & Encrypted-List Loading (hard-won, 2026-06-16)
The single biggest source of "DMs / hashtags / watch-history / block & subscription lists won't load after login" is **not** bitvid — it's an **unresponsive NIP-07 signer**. The extension's MV3 background service-worker can die or its content-script↔worker channel can orphan, after which raw `window.nostr` calls hang forever. No client change can force a dead signer to answer.
**Invariant:** the #1 cause of "DMs / hashtags / watch-history / lists won't load after login" is an unresponsive NIP-07 signer, not bitvid — diagnose with a raw `window.nostr` probe first, and do not regress the five resilience invariants (capped relay fan-out, NIP-07 circuit breaker, re-thrown transient decrypt errors, generous decrypt budgets, lazy modal population).
Full writeup: `docs/lessons/nip07-signer-reliability.md`. See also `docs/KNOWN_BUGS.md` #0.
* **Diagnose first, don't guess.** Run a single raw `window.nostr` probe in the page console (`getPublicKey → nip04.encrypt → nip04.decrypt`) that bypasses bitvid entirely. If *that* hangs, it's the extension/environment. Recommend a well-maintained signer (nos2x, Alby); KeysBand's dead worker was the root cause and switching to nos2x fixed everything.
* **Resilience invariants — do not regress these** (see `docs/KNOWN_BUGS.md` #0 for the full history and the files):
1. **Cap relay fan-out** (`js/nostr/toolkit.js` `capReadRelays`, ≤8, user-relays-first + 2 reserved default slots). An uncapped cold-login REQ storm to ~20 dead NIP-65 relays starves the single-threaded signer's postMessage round-trips.
2. **Circuit breaker** on the NIP-07 channel (`js/nostr/nip07Permissions.js`): after N consecutive *timeouts* fast-fail instead of hanging ~15s each. Channel-death errors must count toward opening (not reset it); interactive permission prompts bypass it; one periodic probe detects recovery.
3. **Never swallow a transient decrypt error as an empty result.** Re-throw channel-death/timeout sub-errors so the retry path runs — returning `[]` turns "signer is slow" into "user has no blocks/lists" and kills retries.
4. **Generous decrypt budget** (~25–30s/call, ~60s backoff cap); a 6s timeout kills slow-but-responsive signers mid-decrypt. Handshake variant timeout is ~20s for the same reason.
5. **One actionable user notice** (`js/utils/signerHealthNotice.js`) after a few timeouts with `signerStatus: "present"` — not a silent forever-retry.
* **Don't render into closed modals at login.** Once a responsive signer makes decryption instant, eagerly populating every profile panel at login (friends avatars, subscriptions, blocks, DM summaries) freezes the main thread ~10–15s. Populate each pane lazily on open (`selectPane`) and gate data-change re-render listeners on "is the view open".
* **Deterministic testing.** Reproduce signer-dependent bugs headlessly with a fake `window.nostr` (Playwright `exposeBinding` + `addInitScript`) + a mock relay + configurable channel models (healthy/slow/overload/dead, latency override). See `scripts/perf/nip07-channel-sim.mjs`.
---
## Repo memory
Curated agent memory lives in `.agents/` (index `.agents/MEMORY.md`); read it first. Propose additions in `.agents/proposals/`; `.agents/memory/` changes only via reviewed commits. Code/tests/config outrank memory.
---
## Next
Please read these documents next.
Read `docs/nostr-event-schemas.md` and `js/nostrEventSchemas.js` next; keep new nostr kinds centralized there and the doc up to date.
* `docs/nostr-event-schemas.md`
* `js/nostrEventSchemas.js`
# Agent fleet
And if you need to create new nostr kinds please keep the logic centralized there in `nostrEventSchemas.js` and the `nostr-event-schemas.md` up to date.
This repo uses agent-fleet-kit. The `agent-fleet` MCP server (`fleet-mcp`) is a convenience. Editing backlog JSON and mailbox files directly is fine. Do not hand-edit the landing queue while a drain is merging, and do not push main.
**End of AGENTS.md**
- `area_info` for the owner of a path.
- `backlog_claim` with an item id marks it in-progress. Pick the item from PROGRAM.md. Without an id the tool does not choose one. Idle is a correct result.
- `mailbox_read` at the start of a turn; `mailbox_send` to reach a peer. Mail is always delivered. The prompt says when to act.
- `commit` to stage and commit in your area worktree. Codex `workspace-write` keeps `.git` read-only, so do not run `git commit` yourself. Pass `message` and the file paths (or `all: true`), and `cwd` or `area` when this server is not already in the worktree. `submit: true` also queues the tip. A doesNotOwn path warns and still commits. The same operation is `fleet commit -m "..." --area <id> [--submit] -- <paths>`.
- `landq_submit` queues the committed tip and starts a drain. A dirty worktree warns; only HEAD is queued. Then go back to work. At your next natural checkpoint, `land_status` (or `fleet landq status <id>`) is a quick lookup, and its wait is optional. Never infer the lander or the land result from silence. States: queued, gating, landed with the main sha, rejected with the reason, or unknown. If no drain is alive, the answer says so and names `fleet lander start`.
- A scarce machine is a prompt convention: say so in `.task` and wait if a peer has it. There is no slot lock.
Sessions, worktrees, and cron are the `fleet` CLI. Submit starts the drain; do not run a second merge by hand.
+6 -75
View File
@@ -2,6 +2,8 @@
This document provides context and guidance for AI assistants working with the bitvid codebase. It explains the project structure, development workflows, conventions, and key architectural decisions.
**Canonical guidance:** `AGENTS.md` is the canonical agent guidance for this repo; `CLAUDE.md` holds Claude-oriented codebase orientation that layers on top of it.
---
## Project Overview
@@ -312,7 +314,7 @@ const dTag = event.tags.find(t => t[0] === 'd')?.[1];
### NIP-07 Signer Reliability (hard-won)
The #1 cause of "DMs / hashtags / watch-history / lists won't load after login" is an **unresponsive NIP-07 signer**, not bitvid. The extension's MV3 background worker can die and `window.nostr` calls then hang forever — no client change fixes a dead signer. **Diagnose with a raw `window.nostr` probe** (`getPublicKey → nip04.encrypt → nip04.decrypt`) in the console; if that hangs, it's the extension (recommend nos2x/Alby). See `AGENTS.md` §17 and `docs/KNOWN_BUGS.md` #0.
The #1 cause of "DMs / hashtags / watch-history / lists won't load after login" is an **unresponsive NIP-07 signer**, not bitvid. The extension's MV3 background worker can die and `window.nostr` calls then hang forever — no client change fixes a dead signer. **Diagnose with a raw `window.nostr` probe** (`getPublicKey → nip04.encrypt → nip04.decrypt`) in the console; if that hangs, it's the extension (recommend nos2x/Alby). See `docs/lessons/nip07-signer-reliability.md` (summarized in `AGENTS.md` §17) and `docs/KNOWN_BUGS.md` #0.
Resilience invariants — **do not regress**:
@@ -326,78 +328,7 @@ Resilience invariants — **do not regress**:
## Testing & Validation: Scenario-First, Cheat-Resistant (Dark Factory Rules)
You are operating inside a “Dark Factory” workflow where **validation replaces human code review**. Passing tests is only meaningful if tests are truthful.
### Prime directive
**Never alter a test merely to make it pass.**
If you change tests to match buggy behavior, you are destroying the only feedback signal and producing code with no real-world value.
StrongDM describes the exact failure mode we are preventing: agents discovering shortcuts (“return true”) and narrow tests being reward-hacked, leading to a shift from “tests” to scenario-based validation.
### How to think about tests here
Tests are not procedural checklists. They are **behavioral specifications**:
- Write tests as **scenarios** (Given/When/Then or equivalent).
- Assert **externally observable outcomes** at boundaries.
- Avoid “testing the implementation” (internal call sequences, private methods, mirrored logic).
- Prefer deterministic/harmonic execution: fixed seeds, controlled clocks, hermetic IO, virtualized dependencies.
### Prohibited behaviors (reward-hacking patterns)
Do NOT:
- weaken assertions, broaden tolerances, or reduce coverage to get green
- update snapshots/goldens without proving the change is intended behavior
- add retries/sleeps/timeouts to hide flakes
- mock so heavily that tests can pass with a broken system
- “hard-code to the test” (single fixtures, single input paths, brittle constants)
### Allowed test changes: Spec Correction Protocol
You may change test expectations only if the test is wrong relative to intended behavior.
Required steps:
1) Identify the scenario/spec that defines correct behavior.
2) Explain why the previous expectation was incorrect.
3) Replace it with an equally strict (or stricter) behavioral assertion.
4) Provide a **Test Integrity Note** (machine-readable) documenting the change.
If you cannot justify a change with scenario/spec truth: **do not change the test**. Instead, output a “Needs Spec Clarification” note and propose candidate scenarios.
### Required output when you touch tests
Include this YAML block in your PR description (or reference it from a `TEST_INTEGRITY.md` entry):
```yaml
test_integrity_note:
change_type: ["new_tests" | "refactor_tests" | "spec_correction" | "flake_fix"]
scenarios:
- id: SCN-<slug>
given: "<preconditions>"
when: "<stimulus>"
then: "<observable outcomes>"
observable_outcomes:
- "<boundary/output asserted>"
determinism_controls:
- "<fixed seed / fake clock / hermetic env / service virtualization>"
anti_cheat_rationale:
prevents:
- "hard-coded return value"
- "over-mocking internal logic"
- "snapshot rubber-stamping"
- "retry/sleep-based flake masking"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
### If external services are involved
Prefer boundary-level service virtualization (“digital twins” / behavioral clones) so scenario tests remain:
- high fidelity (realistic edge cases)
- deterministic and replayable
- not rate-limited / not flaky
(StrongDM’s “Digital Twin Universe” is the reference pattern.)
The testing and validation invariant is stated in `AGENTS.md` §16 "Scenario-First Tests & Test Integrity (Dark Factory Standard)", and the full standard lives in [`docs/testing/test-integrity.md`](docs/testing/test-integrity.md). In short: validation replaces human code review here, tests are behavioral scenarios that assert externally observable outcomes at boundaries, and you must **never alter a test merely to make it pass** — expectation changes are allowed only via the Spec Correction Protocol with an accompanying Test Integrity Note. Read `docs/testing/test-integrity.md` in full before adding or modifying any test.
---
@@ -438,7 +369,7 @@ View with: `./scripts/show-artifacts.sh`
### Agent E2E Testing (Playwright)
bitvid includes a test harness for programmatic Playwright testing without browser extensions or real relays. See `AGENTS.md` Section 14 for the full reference. Quick start:
bitvid includes a test harness for programmatic Playwright testing without browser extensions or real relays. See `docs/testing/agent-playwright-harness.md` for the full reference (summarized in `AGENTS.md` §14). Quick start:
```typescript
import { test, expect } from "./helpers/bitvidTestFixture";
@@ -457,7 +388,7 @@ Key capabilities:
- **Mock relay**: Each test gets an isolated in-memory relay with HTTP seeding
- **Relay override**: `?__test__=1&__testRelays__=ws://localhost:8877` redirects all connections
- **State inspection**: `window.__bitvidTest__.getAppState()` returns login status, relays, etc.
- **Stable selectors**: All key elements have `data-testid` attributes (see AGENTS.md Section 14)
- **Stable selectors**: All key elements have `data-testid` attributes (see `docs/testing/agent-playwright-harness.md`)
---
+9
View File
@@ -242,3 +242,12 @@ If you find a vulnerability, please email **[[email protected]](mailto:sec
## Trademark
The **bitvid** name and logos are trademarks (see `TRADEMARKS.md`). Don’t use them in ways that imply official sponsorship or confuse users. “Powered by bitvid” attribution is encouraged.
## AI-assisted contributions
AI-assisted changes are subject to the same testing, security, and review
requirements as any other change; the contributor remains responsible for
understanding what they submit. Do not submit unvalidated generated code,
credentials, or agent-generated trusted memory without evidence and review.
The canonical backlog and planning rules are in `AGENTS.md`; durable agent
plans live under `.agents/plans/`.
+26 -14
View File
@@ -3,11 +3,10 @@
## Test Infrastructure
### Missing `jsdom` Dependency
- **Status:** Active
- **Status:** Resolved (2026-07-30)
- **Detected:** 2026-02-26
- **Description:** Multiple unit tests fail with `ERR_MODULE_NOT_FOUND` because `jsdom` is imported but not installed in the project.
- **Impact:** Prevents running unit tests that rely on DOM simulation.
- **Remediation:** `npm install --save-dev jsdom`
- **Description:** Unit tests failed with `ERR_MODULE_NOT_FOUND` because `jsdom` was imported but not installed.
- **Resolution:** `jsdom` has been a devDependency for some time (28.1.0 installed and in `package.json`); the jsdom-based suites run clean.
## Run Notes (2026-03-04)
@@ -22,19 +21,32 @@
- **Description:** `tests/nostr-send-direct-message.test.mjs` must pass `Uint8Array` secret keys to `getPublicKey/finalizeEvent` for current `nostr-tools`.
- **Impact:** Passing hex strings causes failures like `expected Uint8Array, got type=string`.
## Run Notes (2026-06-20)
## Run Notes (2026-06-20, updated 2026-07-30)
### Additional E2E specs flake under full parallel load (pass in isolation)
### `login-flows.spec.ts:149` — upload/profile buttons stay visible after logout
- **Status:** Active — NOT merely a parallel-load flake
- **Description:** Previously filed as a worker-contention flake that "passes deterministically in isolation." On 2026-07-28 it failed **in isolation** as well (chromium, local and CI `e2e-headless`), while passing in the CI `e2e-tests (e2e)` job — environment-sensitive, not load-sensitive. Direct probes of the same flow (raw `__bitvidTest__.loginWithNsec` → `logout()` against a mock relay) hide the button correctly, so the failure is specific to something in the Playwright fixture path. An attempted fix (running `applyLoggedOutUiState()` before the awaited NWC/profile teardown in `_executeAuthLogout`) did not change the outcome and was reverted rather than shipped unverified.
- **Impact:** One e2e failure in `e2e-headless`; logout hides gated UI correctly in manual testing and direct harness probes.
- **Next step:** Investigate alongside pre-launch TODO #33 ("logout logs out everyone" / NIP-46 session persistence) — same subsystem.
### `video-upload-publish-lifecycle.spec.ts:49` / `:79` flake under full parallel load
- **Status:** Active
- **Description:** Under the full `test:e2e` run (378 tests, 4 workers) these intermittently fail on `not.toBeVisible`/feed-appearance timeouts, but pass deterministically when re-run in isolation (`--workers=1`): `login-flows.spec.ts:149` (upload/profile buttons hide after logout), `video-upload-publish-lifecycle.spec.ts:49` and `:79` (URL-first publish appears / owner delete). Same root cause as the Firefox feed-hydration flakes above — worker resource contention, not an app regression (verified: pass in isolation on the current branch).
- **Impact:** Full-suite e2e is flaky; isolated reruns are green.
- **Description:** Under the full `test:e2e` run these intermittently fail on feed-appearance timeouts but pass on retry (CI marks them flaky). Worker resource contention.
- **Impact:** Full-suite e2e occasionally needs retries; isolated reruns are green.
### `webseed-playback.spec.ts:31` fails on Firefox headless (CDN mode)
- **Status:** Active (pre-existing — reproduced on pre-refactor commit `1b11cb1b`)
- **Description:** "plays directly from HTML link (CDN mode)" fails on `--project=e2e-firefox` with `#modalVideo` `src` empty instead of the test's archive.org URL. The Chromium variant and the WebTorrent-fallback variant (`:63`) both pass. Firefox-headless-specific (external URL reachability/codec), not an app regression.
- **Impact:** One Firefox-only e2e failure; CDN playback works on Chromium and in manual testing.
- **Status:** Resolved (2026-07-28, `f48150ed`)
- **Description:** The test asserted the **final** `src` equals the CDN URL while simultaneously tolerating "No playable source found" in its status assertion — contradictory, because a failed hosted attempt calls `resetVideoElement()`, which clears `src`. Headless CI cannot decode the archive.org H.264 sample, so the cleared-src path was routine there.
- **Resolution:** The test now records every `src` the player attaches via a MutationObserver and asserts the CDN URL was attached — the actual behavior under test. Green in both `e2e` and `e2e-firefox` CI jobs.
### `uploadModal-reset.test.mjs` ("UploadModal Reset Logic") hangs/cancels
- **Status:** Active (pre-existing — reproduced on pre-refactor commit `1b11cb1b`)
- **Description:** The jsdom-based UploadModal reset suite intermittently fails with `cancelledByParent` / "Promise resolution is still pending but the event loop has already resolved" (~12s hang). It passes or fails depending on full-suite test ordering/timing (an async-hang flake, likely a torrent-metadata/upload promise not settling under the mocked services). Not a regression from the MediaUploader extraction — it fails identically on `1b11cb1b`.
- **Impact:** One unit suite can report `not ok 1`; the runner still exits 0. Other tests unaffected.
- **Status:** Not reproducible (2026-07-30)
- **Description:** Intermittent `cancelledByParent` / "Promise resolution is still pending" hang, order/timing dependent.
- **Current state:** Passes cleanly (`pass=3 fail=0 cancelled=0`) in isolation and across repeated full-suite sweeps on 2026-07-28–30. Left on file because the original failure was ordering-dependent; if it recurs, capture the full-suite ordering that produced it.
## Run Notes (2026-07-30)
### Visual baselines are CI-canonical — local `test:visual` may fail the kitchen-sink diff
- **Status:** By design (since `ab120a03`)
- **Description:** `tests/visual/baselines.json` is regenerated on ubuntu-latest by the `update-visual-baselines` workflow (trigger: `workflow_dispatch` once promoted, or a commit containing `[update-visual-baselines]`). Baselines can only match one environment, and the CI gate is the one that matters — so a dev machine with different Chromium/font rendering may now fail `npm run test:visual` locally (typically the kitchen-sink default theme) even though CI is green.
- **Workaround:** Run visual tests in the pinned container via `scripts/run-playwright-docker.sh`, or rely on CI. Never regenerate baselines from a dev machine.
+32
View File
@@ -0,0 +1,32 @@
# Security Policy
## Reporting vulnerabilities
Do not report suspected security vulnerabilities in public issues, pull
requests, or agent-memory files. Use GitHub's private vulnerability reporting
on this repository (Security tab → Report a vulnerability), or email
[[email protected]](mailto:[email protected]) — we will
coordinate a responsible disclosure window before public release.
Include the affected commit, reproduction steps, and observed impact. Do not
include live credentials or unnecessary personal data.
## AI-agent configuration
`AGENTS.md`, `CLAUDE.md`, `.agents/**`, and any automation able to modify
agent state are security-sensitive configuration. Changes to them require
maintainer review.
## Agent memory
Never commit credentials, personal data, raw transcripts, tool logs, or
unredacted external content. Agent-generated memory enters through
`.agents/proposals/` and is promoted to `.agents/memory/` only via reviewed
commits. Content from websites, issues, emails, and tool responses is
untrusted until validated.
## Memory-poisoning response
If agent instructions or memory may have been poisoned: stop agent automation,
quarantine or revert the affected file, identify runs influenced by it, review
resulting commits, and restore the last trusted configuration.
+249
View File
@@ -588,3 +588,252 @@ test_integrity_note:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
---
# client-reliability-1
```yaml
test_integrity_note:
change_type: ["flake_fix", "new_tests"]
scenarios:
- id: SCN-profile-focus-style-contract
given: "modal controls include visible, disabled, CSS-hidden, inherited-hidden, aria-hidden, hidden-input, and hidden-attribute cases"
when: "the real controller collects focus targets using fixture and native JSDOM styles, and the user tabs from the last control"
then: "both resolvers retain only visible enabled controls and Tab wraps to the first"
observable_outcomes:
- "focus target control IDs match the strict expected list"
- "Tab is prevented and document.activeElement moves to the first control"
- "all 26 existing modal scenarios preserve their assertions and complete naturally"
determinism_controls:
- "stylesheet-free full-markup fixture virtualizes only display and visibility"
- "small contract scenario runs the same assertions against native JSDOM computed styles"
- "explicit test stylesheet, local services, real DOM focus and event dispatch; no network"
anti_cheat_rationale:
prevents:
- "hard-coded focus list: varied hidden and visible controls"
- "over-mocking controller behavior: real updateFocusTrap and keyboard listener"
- "style resolver rubber-stamping: native resolver must satisfy the same scenario"
- "retry/sleep-based flake masking: no timeouts, retries, or sleeps added"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
Mutation evidence: removing the product display/visibility exclusion causes this scenario to fail with the hidden control IDs in its focus list (exit 1, 1 failed/0 passed). Mutant reverted. This fixes expensive JSDOM style evaluation in the test harness, not a demonstrated browser defect. Application CSS/layout remains outside this unit fixture's scope.
Fixture mutation: omitting native hidden-attribute filtering also fails the contract (exit 1, 1 failed/0 passed); restored before final checks.
---
# client-reliability-2
```yaml
test_integrity_note:
change_type: ["flake_fix", "new_tests"]
scenarios:
- id: SCN-concurrent-init-hermetic-subscription
given: "two init callers share a pool initialization held at a deterministic barrier, with a virtual successful relay connection"
when: "pool initialization is released and the second caller opens then closes a video subscription"
then: "the second caller waits for pool readiness, real L1 feed setup reaches the fake pool, and unsubscribe closes the transport exactly once with no reconnect timer"
observable_outcomes:
- "second init promise remains pending until the pool barrier is released"
- "pool is ready after awaiting the second caller; both init promises settle"
- "post-init subscription requests video events from a nonempty relay set"
- "subscription transport closes once, including repeated unsubscribe"
- "the process exits naturally with no public relay IO or reconnect retries"
determinism_controls:
- "deferred pool barrier replaces the old 50ms delay"
- "fake pool at the actual transport boundary; real init, L1, and feed cleanup"
- "manager connection boundary reports success rather than triggering background reconnect"
- "local-data/signer restore are virtualized; teardown awaits init, closes feed and resets reconnect state even after failure"
anti_cheat_rationale:
prevents:
- "early-return init mutant fails while pool initialization is held"
- "no-op subscription cannot satisfy the observed transport request"
- "no-op cleanup cannot satisfy exact close count"
- "failure fixture cannot hide a reconnect timer behind a passing assertion"
- "retry/sleep/forced-exit masking: no timeout changes or forced exit"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
Original concurrent-promise, pool-ready and post-init no-throw assertions remain. The old empty connection result accidentally modeled failure; init correctly scheduled manager-owned retries that bypassed its client-level stub. The fixture now models successful init at the manager boundary and releases its feed subscription. This is harness cleanup, not a Nostr product change.
Mutants: a test-local historical early-return init variant fails the pending-barrier assertion (exit1, 1 failed); a connection-failure fixture variant fails the no-retry assertion (exit1, 1 failed). Both exit naturally and are reverted. No product file was edited for either mutant.
---
# client-reliability-3
```yaml
test_integrity_note:
change_type: ["new_tests"]
scenarios:
- id: SCN-last-probe-owner-disposal
given: "five real DOM cards register through actual gridHealth/cardObserver; four probes are active and one queued"
when: "the feed is disposed/removed and active probes finish"
then: "zero obsolete probes start"
- id: SCN-shared-queued-owner
given: "two cards share one queued torrent source"
when: "one registration disposes while its DOM remains connected"
then: "the live owner still probes and becomes healthy; disposed owner receives no stale UI/callback"
- id: SCN-cancelled-replacement-inflight
given: "last owner cancels queued work then a replacement requests the same source before old settlement"
when: "old completion drains and another owner joins before a slot frees"
then: "no unhealthy cancellation cache is written, only one new probe starts, and both replacements become healthy/cacheable"
- id: SCN-probe-priority-concurrency
given: "four active probes, queued mixed priorities and a cancelled source"
when: "slots free one at a time"
then: "four-slot maximum and stable priority order remain; cancelled work never starts"
- id: SCN-hidden-healthy-reveal
given: "a hide-all card is hidden pending liveness"
when: "registration eagerly probes and returns healthy"
then: "raw magnet/webseed bytes reach probe IO and the card reveals without an intersection"
- id: SCN-active-shared-late-entry
given: "an active source loses its owner, receives a late observer entry and gains a live owner"
when: "the unchanged active probe completes"
then: "no duplicate starts or disposed result writes; live owner becomes healthy"
- id: SCN-repeat-disposal-reattach
given: "a disposed container has a new queued registration"
when: "the old disposer is called again"
then: "new owner survives and becomes healthy"
- id: SCN-ownerless-active-cache
given: "an already active probe has no remaining owner"
when: "it finishes before a later live card requests that source"
then: "the active result remains cached; no new probe or stale UI write occurs"
- id: SCN-existing-probe-cache-ttl
given: "completed healthy and negative results"
when: "cards request before, at, and after the existing TTL boundary"
then: "both cached outcomes reuse until the existing inclusive TTL expires, then both probe again"
observable_outcomes:
- "actual probePeers start count/order, arguments and maximum active count"
- "real DOM badge health/hidden state, observer disposal and callback counts"
- "storage cache absence for cancellation and reuse/expiry for completed probes"
determinism_controls:
- "actual gridHealth/cardObserver and DOM, controllable observer and deferred probePeers IO"
- "explicit viewport geometry, unique sources and isolated storage; fake Date.now only for TTL scenario"
- "task-turn promise draining without wall-clock sleeps, polling or retries"
- "native Chromium/Firefox observer smoke against built dist, external traffic blocked"
anti_cheat_rationale:
prevents:
- "unconditional queue cancellation: shared-owner mutant fails"
- "no-op cancellation, stale inflight deletion or cancelled cache poisoning"
- "changed concurrency/priority or hidden eager path"
- "stale result/listener/observer effects and repeated old disposal"
- "cancelling or dropping cache from already active work"
- "timeout/retry/sleep masking or hard-coded healthy results"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
Grouped mutation coverage: last-owner cancellation mutant kills SCN-last-probe-owner-disposal; unconditional cancel kills SCN-shared-queued-owner; unconditional inflight deletion and cancelled-normalization mutants each kill SCN-cancelled-replacement-inflight; reversed queue priority kills SCN-probe-priority-concurrency; disabled eager path kills SCN-hidden-healthy-reveal; stale result guard and late-entry membership mutants each kill SCN-active-shared-late-entry; non-idempotent disposer kills SCN-repeat-disposal-reattach; owner-dependent active caching kills SCN-ownerless-active-cache; missing cache writes kills SCN-existing-probe-cache-ttl. All11 variants exit1 with1 failed scenario,0 cancelled, and are reverted. Browser checks corroborate the same disposal/shared-owner/hidden-reveal/source-visibility scenarios with native observers; no browser CPU or actual media fallback claim.
Additional client-reliability-3 scenarios (same invariant and controls):
```yaml
test_integrity_note:
change_type: ["new_tests"]
scenarios:
- id: SCN-shared-badge-listener-lifetime
given: "two feed registrations share the app callback"
when: "badge updates occur and registrations dispose one at a time"
then: "each event notifies once, one surviving owner retains the callback, last disposal stops notifications"
- id: SCN-checking-callback-disposal
given: "a live card's checking callback synchronously disposes the feed"
when: "badge notification returns to probe submission"
then: "no probe is submitted after unregister"
observable_outcomes:
- "callback event count/state sequence and external probe start count"
determinism_controls:
- "real badge/observer path, synchronous disposal callback and deferred probe IO"
anti_cheat_rationale:
prevents:
- "duplicated shared callbacks or dropping live callback owners"
- "post-disposal work submitted after synchronous reentrant teardown"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
Duplicate wrapper and unconditional listener release variants each fail SCN-shared-badge-listener-lifetime. Removing the post-checking membership guard fails SCN-checking-callback-disposal (actual starts1 vs expected0). These3 additional variants exit1 with1 failed/0 cancelled and are reverted; total14 killed variants for11 lifecycle scenarios. Original source/priority/observer expectations remain unchanged.
## client-reliability-4 — queued URL media ownership
```yaml
test_integrity_note:
change_type: ["new_tests"]
scenarios:
- id: SCN-url-last-owner
given: "four cards, three active media and one queued"
when: "last registration is disposed and an active slot frees"
then: "queued promise settles null, no cache entry or obsolete media start"
- id: SCN-url-shared-owner
given: "two feeds share a queued event/source"
when: "one feed disposes"
then: "exactly one media probe serves the live badge; disposed connected badge stays checking"
- id: SCN-url-last-shared-owner
given: "two feeds share a queued event/source"
when: "both feeds dispose"
then: "queued promise settles null and never starts media"
- id: SCN-url-cancel-rerequest
given: "cancelled queued event/source has pending promise continuations"
when: "a new feed requests the same source immediately"
then: "new inflight record survives old settlement and live badge becomes healthy"
- id: SCN-url-cancel-mirror
given: "queued primary has a mirror"
when: "last owner disposes"
then: "cancelled work settles without mirror HEAD/media or stale UI/cache"
- id: SCN-url-registration-reuse
given: "hidden old feed queues an eager callback"
when: "dispose, reuse container, deliver old callback and old disposer"
then: "only the new event reaches onCheck and starts IO"
- id: SCN-url-active-cache
given: "active media belongs to a connected card"
when: "dispose its registration and complete active media"
then: "active result caches healthy, disposed badge stays checking, new card uses cache"
- id: SCN-url-hidden-reveal
given: "URL-only card is hidden pending health"
when: "eager media confirmation succeeds"
then: "card becomes visible with healthy badge"
- id: SCN-url-media-limit
given: "three active media probes plus FIFO queued work"
when: "slot releases while a new card enters"
then: "existing queued source runs first; active media stays three"
- id: SCN-url-mirror-order
given: "primary and two mirrors exist"
when: "primary errors then first mirror succeeds"
then: "primary then first mirror are requested; second mirror is untouched"
- id: SCN-url-legacy-callback
given: "connected badge lacks observer ownership"
when: "legacy caller invokes controller"
then: "media settles and healthy badge paints"
- id: SCN-url-cross-controller
given: "shared real inflight cache and two controllers"
when: "creator feed disposes while other feed remains"
then: "queued shared source starts once and other controller paints healthy"
observable_outcomes:
- "HEAD and native media IO starts, promise settlement, real cache/inflight records, connected badge and card visibility"
- "Observer callback emissions after disposal and container reuse"
determinism_controls:
- "jsdom cards, controllable IntersectionObserver, explicit loadeddata/error events and stubbed fetch boundary; real observer/controller/cache"
- "Unique event/source keys and task-turn drain; no live traffic, arbitrary sleeps, retries or relaxed assertions"
anti_cheat_rationale:
prevents:
- "hard-coded healthy or cancelled results"
- "testing a copied queue instead of real controller and observer"
- "cancelling shared live work or caching cancellation as offline"
- "stale connected DOM callbacks hidden by isConnected guards"
- "retry/sleep-based flake masking"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
All existing assertions preserved. `tests/url-health-lifecycle.test.mjs` adds12 scenarios. Mutations are temporary edits restored before gates. `python3 /tmp/item4-mutants.py` final run:13/13 variants naturally exit1 with named assertion failures (no timeouts); logs `/tmp/item4-mutant-*.log`, results `/tmp/item4-mutants.json`. No production test runner/gate changes.
Coverage grouping: no-unregister→last owner; cancel-shared→shared owner/cross-controller; reuse-cancelled-inflight→immediate retry/new inflight; cache-cancellation→last/shared owner/retry/mirror cache outcomes; mirror-fallthrough→cancelled primary; grouped stale guards→shared/active disposed UI (remove both redundant owner guards together; two equivalent variants); eager-callback-stale→registration reuse; no-eager-reveal→hidden reveal; concurrency-four→three-media cap/FIFO; reverse-mirrors→mirror ordering; break-legacy-consumer→callback compatibility; controller-local-records→shared cache ownership across controllers. Initial mutation pass exposed callback-emission and first-source gaps; new assertions were strengthened, then all mutants rejected. Initial no-unregister full-file run failed assertions but hung in another scenario awaiting its intentionally broken promise; final targeted last-owner variant naturally exits1. No timeout is counted as successful mutant evidence.
+363
View File
@@ -0,0 +1,363 @@
// Per-video OpenGraph / Twitter-player rendering for crawlers.
//
// bitvid is a static SPA: index.html carries one set of site-wide OG tags, so
// every `/?v=<nevent>` link previewed as the same generic bitvid card. Crawlers
// don't run JS, so the client can't fix that — this module renders the tags a
// crawler needs, and a thin host adapter (api/og.js on Vercel) feeds it.
//
// PURE by design: no network, no host globals. All I/O lives in the adapter so
// the escaping and gating rules below stay unit-testable.
//
// Rollback: remove the crawler rewrite from vercel.json. The static site keeps
// serving its existing tags and nothing else changes — this is never on the
// path of a real user's page load, and `npm run archive` (nsite) is unaffected.
// Player card dimensions. Twitter refuses a player card without explicit
// dimensions, and 16:9 matches bitvid's embed shell.
export const DEFAULT_PLAYER_WIDTH = 1280;
export const DEFAULT_PLAYER_HEIGHT = 720;
const MAX_TITLE_LENGTH = 120;
const MAX_DESCRIPTION_LENGTH = 200;
/**
* Escape a value for use inside a double-quoted HTML attribute.
*
* Titles and descriptions come from arbitrary nostr events — treat every one as
* hostile. A title of `"><script>…` would otherwise break out of the meta tag
* and inject markup into a page we serve from our own origin.
*/
export function escapeHtmlAttribute(value) {
if (typeof value !== "string") {
return "";
}
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
/**
* Only http(s) URLs may reach a meta tag. Anything else (javascript:, data:,
* relative paths) is dropped — a crawler resolving them is at best useless and
* at worst a vector.
*/
export function sanitizeAbsoluteUrl(value) {
const trimmed = typeof value === "string" ? value.trim() : "";
if (!trimmed) {
return "";
}
let parsed;
try {
parsed = new URL(trimmed);
} catch (err) {
return "";
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return "";
}
return parsed.toString();
}
/** Collapse whitespace and hard-truncate for preview surfaces. */
export function normalizeText(value, maxLength) {
const collapsed =
typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
if (!collapsed || !Number.isFinite(maxLength) || maxLength <= 0) {
return collapsed;
}
if (collapsed.length <= maxLength) {
return collapsed;
}
return `${collapsed.slice(0, maxLength - 1).trimEnd()}…`;
}
/**
* Decide whether a video may be described in a link preview.
*
* A crawler never authenticates, so anything emitted here is fully public and
* outlives us in third-party caches. The rule is fail-closed: only a video that
* is affirmatively public gets a rich card; everything else — including the
* "we couldn't determine it" case — falls back to the generic bitvid card.
*
* @returns {{allowed: boolean, reason: string}}
*/
export function isPubliclyShareable(video, options = {}) {
if (!video || typeof video !== "object") {
return { allowed: false, reason: "missing-video" };
}
if (video.invalid) {
return { allowed: false, reason: "invalid-video" };
}
if (video.deleted === true) {
return { allowed: false, reason: "deleted" };
}
if (video.isPrivate === true) {
return { allowed: false, reason: "private" };
}
// NSFW thumbnails must never be injected into an unsuspecting timeline.
if (video.isNsfw === true) {
return { allowed: false, reason: "nsfw" };
}
const pubkey =
typeof video.pubkey === "string" ? video.pubkey.trim().toLowerCase() : "";
if (!pubkey) {
return { allowed: false, reason: "missing-author" };
}
const blacklist = options.blacklist instanceof Set ? options.blacklist : null;
if (blacklist && blacklist.has(pubkey)) {
return { allowed: false, reason: "blacklisted" };
}
if (options.whitelistEnabled) {
const whitelist =
options.whitelist instanceof Set ? options.whitelist : null;
// No usable whitelist while whitelist mode is on means we cannot prove the
// author is allowed. Fail closed rather than leak a gated video's metadata.
if (!whitelist || whitelist.size === 0) {
return { allowed: false, reason: "whitelist-unavailable" };
}
if (!whitelist.has(pubkey)) {
return { allowed: false, reason: "not-whitelisted" };
}
}
return { allowed: true, reason: "ok" };
}
// bitvid's own notes are kind 30078 with a v3 JSON body. Other clients
// (nostube/slidestr and anything NIP-71 native) publish kind 21/22/34235/34236
// with the metadata in TAGS and the description in `content`. bitvid normalizes
// both in the browser, so the preview must understand both too — parsing only
// the JSON shape made every NIP-71 video fall back to the generic card.
export const NIP71_VIDEO_KINDS = Object.freeze([21, 22, 34235, 34236]);
function firstTagValue(tags, name) {
if (!Array.isArray(tags)) {
return "";
}
for (const tag of tags) {
if (Array.isArray(tag) && tag[0] === name && typeof tag[1] === "string") {
const value = tag[1].trim();
if (value) {
return value;
}
}
}
return "";
}
function hasTag(tags, name) {
return (
Array.isArray(tags) &&
tags.some((tag) => Array.isArray(tag) && tag[0] === name)
);
}
/** First `image <url>` entry inside any imeta tag. */
function imetaImageUrl(tags) {
if (!Array.isArray(tags)) {
return "";
}
for (const tag of tags) {
if (!Array.isArray(tag) || tag[0] !== "imeta") {
continue;
}
for (let i = 1; i < tag.length; i += 1) {
const entry = typeof tag[i] === "string" ? tag[i].trim() : "";
if (/^image\s+/i.test(entry)) {
const url = sanitizeAbsoluteUrl(entry.replace(/^image\s+/i, ""));
if (url) {
return url;
}
}
}
}
return "";
}
/**
* Normalize a raw nostr event into the fields the preview needs, for both the
* bitvid v3 JSON shape and the NIP-71 tag shape. Returns null when the event is
* not a recognizable video note (caller renders the generic card).
*/
export function parseVideoEvent(event) {
if (!event || typeof event !== "object") {
return null;
}
const base = { pubkey: event.pubkey, deleted: false, isPrivate: false };
// --- bitvid native (kind 30078, JSON content) ---
if (typeof event.content === "string" && event.content.trim().startsWith("{")) {
try {
const content = JSON.parse(event.content);
if (content && typeof content === "object") {
return {
...base,
title: content.title,
description: content.description,
thumbnail: content.thumbnail,
deleted: content.deleted === true,
isPrivate: content.isPrivate === true,
isNsfw: content.isNsfw === true || hasTag(event.tags, "content-warning"),
invalid: !content.title || (!content.url && !content.magnet),
};
}
} catch (err) {
// Not JSON after all — fall through to the NIP-71 reading.
}
}
// --- NIP-71 (metadata in tags, description in content) ---
if (NIP71_VIDEO_KINDS.includes(event.kind)) {
const title = firstTagValue(event.tags, "title");
const thumbnail =
imetaImageUrl(event.tags) ||
sanitizeAbsoluteUrl(firstTagValue(event.tags, "image")) ||
sanitizeAbsoluteUrl(firstTagValue(event.tags, "thumb"));
const description =
(typeof event.content === "string" && event.content.trim()) ||
firstTagValue(event.tags, "summary") ||
firstTagValue(event.tags, "alt");
return {
...base,
title,
description,
thumbnail,
// NIP-71 has no privacy flag; `content-warning` is the NSFW signal.
isNsfw: hasTag(event.tags, "content-warning"),
invalid: !title,
};
}
return null;
}
function metaTag(attribute, name, content) {
return ` <meta ${attribute}="${escapeHtmlAttribute(name)}" content="${escapeHtmlAttribute(content)}" />`;
}
/**
* Render the crawler-facing HTML document.
*
* @param {object} params
* @param {object|null} params.video Normalized video object, or null.
* @param {string} params.shareUrl Canonical `/?v=<nevent>` URL.
* @param {string} [params.embedUrl] `/embed.html?pointer=<nevent>` player URL.
* @param {object} params.fallback Site-wide defaults (title/description/image/url).
* @param {object} [params.gate] Result of isPubliclyShareable().
* @returns {string} A complete HTML document.
*/
export function renderOgHtml({
video = null,
shareUrl = "",
embedUrl = "",
fallback = {},
gate = null,
} = {}) {
const siteName = normalizeText(fallback.siteName || "bitvid", 60);
const fallbackTitle = normalizeText(
fallback.title || "bitvid - Decentralized Video Sharing",
MAX_TITLE_LENGTH
);
const fallbackDescription = normalizeText(
fallback.description || "Decentralized video sharing on Nostr.",
MAX_DESCRIPTION_LENGTH
);
const fallbackImage = sanitizeAbsoluteUrl(fallback.image);
const canonical = sanitizeAbsoluteUrl(shareUrl) || sanitizeAbsoluteUrl(fallback.url);
const decision = gate || isPubliclyShareable(video);
const useVideo = decision.allowed === true;
const title = useVideo
? normalizeText(video.title, MAX_TITLE_LENGTH) || fallbackTitle
: fallbackTitle;
const description = useVideo
? normalizeText(video.description, MAX_DESCRIPTION_LENGTH) ||
fallbackDescription
: fallbackDescription;
const image = useVideo
? sanitizeAbsoluteUrl(video.thumbnail) || fallbackImage
: fallbackImage;
const player = useVideo ? sanitizeAbsoluteUrl(embedUrl) : "";
const lines = [
"<!doctype html>",
'<html lang="en">',
" <head>",
' <meta charset="utf-8" />',
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
` <title>${escapeHtmlAttribute(title)}</title>`,
];
if (canonical) {
lines.push(` <link rel="canonical" href="${escapeHtmlAttribute(canonical)}" />`);
}
lines.push(metaTag("name", "description", description));
lines.push(metaTag("property", "og:site_name", siteName));
lines.push(metaTag("property", "og:type", useVideo ? "video.other" : "website"));
lines.push(metaTag("property", "og:title", title));
lines.push(metaTag("property", "og:description", description));
if (canonical) {
lines.push(metaTag("property", "og:url", canonical));
}
if (image) {
lines.push(metaTag("property", "og:image", image));
lines.push(metaTag("property", "og:image:alt", title));
}
if (player) {
// A player card is what makes a shared bitvid link behave like a YouTube
// link: the video plays inline in the timeline instead of showing a still.
lines.push(metaTag("property", "og:video:url", player));
lines.push(metaTag("property", "og:video:secure_url", player));
lines.push(metaTag("property", "og:video:type", "text/html"));
lines.push(metaTag("property", "og:video:width", String(DEFAULT_PLAYER_WIDTH)));
lines.push(metaTag("property", "og:video:height", String(DEFAULT_PLAYER_HEIGHT)));
lines.push(metaTag("name", "twitter:card", "player"));
lines.push(metaTag("name", "twitter:player", player));
lines.push(metaTag("name", "twitter:player:width", String(DEFAULT_PLAYER_WIDTH)));
lines.push(metaTag("name", "twitter:player:height", String(DEFAULT_PLAYER_HEIGHT)));
} else {
lines.push(metaTag("name", "twitter:card", image ? "summary_large_image" : "summary"));
}
lines.push(metaTag("name", "twitter:title", title));
lines.push(metaTag("name", "twitter:description", description));
if (image) {
lines.push(metaTag("name", "twitter:image", image));
}
lines.push(" </head>");
lines.push(" <body>");
// A human should never land here (only crawler user-agents are rewritten),
// but if one does, give them a real link rather than a blank page.
lines.push(
canonical
? ` <p><a href="${escapeHtmlAttribute(canonical)}">${escapeHtmlAttribute(title)}</a></p>`
: ` <p>${escapeHtmlAttribute(title)}</p>`
);
lines.push(" </body>");
lines.push("</html>");
return `${lines.join("\n")}\n`;
}
// Conservative crawler match. Being wrong in the "not a bot" direction is free
// (the visitor gets the normal SPA); being wrong the other way would serve a
// human this stub page, so the pattern stays narrow and explicit.
export const CRAWLER_USER_AGENT_PATTERN =
/(facebookexternalhit|twitterbot|slackbot|discordbot|telegrambot|whatsapp|linkedinbot|pinterest|redditbot|embedly|quora link preview|nostrbot|primal|iframely|opengraph|mastodon|bluesky|skypeuripreview|vkshare|googlebot|bingbot|applebot|yandexbot|duckduckbot|baiduspider|petalbot|semrushbot|ahrefsbot|developers\.google\.com\/\+\/web\/snippet)/i;
export function isCrawlerUserAgent(userAgent) {
return typeof userAgent === "string"
? CRAWLER_USER_AGENT_PATTERN.test(userAgent)
: false;
}
+273
View File
@@ -0,0 +1,273 @@
// Vercel serverless handler: renders per-video OpenGraph / Twitter-player tags
// for crawlers hitting `/?v=<nevent>`.
//
// Nothing is pre-generated and nothing is persisted. This is a stateless
// function invoked per crawler request; `dist/` is byte-identical with or
// without it. The only caching is the CDN edge cache via Cache-Control below.
//
// This must never become load-bearing for the app: real users are not routed
// here (see the user-agent rewrite in vercel.json), and a static/nsite deploy
// without any functions keeps working exactly as it does today, just with the
// site-wide card.
import { nip19 } from "nostr-tools";
import { SimplePool, useWebSocketImplementation } from "nostr-tools/pool";
import WebSocket from "ws";
import {
isCrawlerUserAgent,
isPubliclyShareable,
parseVideoEvent,
renderOgHtml,
} from "./_lib/ogRenderer.mjs";
// Imported, not duplicated: these modules are plain ESM with no DOM access, so
// the function reads the SAME admin-list identity and relay set the browser
// does. A hardcoded copy would silently drift the moment an operator retunes
// the instance, and the failure mode (previews gated forever, or worse, a stale
// whitelist) would be invisible.
import {
ADMIN_LIST_NAMESPACE,
ADMIN_SUPER_NPUB,
} from "../js/config.js";
import { DEFAULT_RELAY_URLS } from "../js/nostr/toolkit.js";
import { ADMIN_LIST_IDENTIFIERS } from "../js/nostrEventSchemas.js";
useWebSocketImplementation(WebSocket);
const RELAYS = Array.from(DEFAULT_RELAY_URLS);
// Admin moderation lists are addressable kind-30000 sets published by the
// instance's super admin (js/adminListStore.js).
const ADMIN_LIST_KIND = 30000;
// Measured: a COLD pool's first read across these relays can take ~3s (WSS
// handshake + REQ round trip on four connections). 1.5s and 2.5s both cut it
// off and served the generic card every time. Twitter allows ~5s and Facebook
// ~10s, so 4s buys a correct card on a cold container while still leaving
// headroom under Vercel's function limit. Warm containers answer in <600ms.
const RELAY_TIMEOUT_MS = 4000;
// Admin lists are identical for every request, so re-reading them on each crawl
// is pure waste (and it was starving the video read of its budget). Memoized in
// module scope: warm invocations skip the relay round trip entirely. This is a
// per-container, in-memory cache of PUBLIC data that dies with the container —
// nothing is persisted anywhere.
const ADMIN_LIST_TTL_MS = 5 * 60 * 1000;
let adminListCache = { expiresAt: 0, whitelist: null, blacklist: null };
const SITE_ORIGIN = process.env.BITVID_SITE_ORIGIN || "https://bitvid.network";
const FALLBACK = Object.freeze({
siteName: "bitvid",
title: "bitvid - Decentralized Video Sharing",
description:
"bitvid is a decentralized video sharing platform built on Nostr and WebTorrent.",
image: `${SITE_ORIGIN}/assets/jpg/bitvid.jpg`,
url: SITE_ORIGIN,
});
function withTimeout(promise, ms, fallbackValue) {
return Promise.race([
promise.catch(() => fallbackValue),
new Promise((resolve) => setTimeout(() => resolve(fallbackValue), ms)),
]);
}
/** Decode `?v=` (nevent/naddr/note/raw hex) into a relay filter. */
function pointerToFilter(pointer) {
const trimmed = typeof pointer === "string" ? pointer.trim() : "";
if (!trimmed) {
return null;
}
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
return { ids: [trimmed.toLowerCase()] };
}
try {
const decoded = nip19.decode(trimmed);
if (decoded.type === "nevent" && decoded.data?.id) {
return { ids: [decoded.data.id] };
}
if (decoded.type === "note" && typeof decoded.data === "string") {
return { ids: [decoded.data] };
}
if (decoded.type === "naddr" && decoded.data?.identifier) {
return {
kinds: [decoded.data.kind],
authors: [decoded.data.pubkey],
"#d": [decoded.data.identifier],
};
}
} catch (err) {
// Unparseable pointer -> generic card.
}
return null;
}
function extractPubkeySet(event) {
const out = new Set();
if (!event || !Array.isArray(event.tags)) {
return out;
}
for (const tag of event.tags) {
if (!Array.isArray(tag) || tag[0] !== "p" || typeof tag[1] !== "string") {
continue;
}
const value = tag[1].trim().toLowerCase();
if (/^[0-9a-f]{64}$/.test(value)) {
out.add(value);
continue;
}
try {
const decoded = nip19.decode(value);
if (decoded.type === "npub" && typeof decoded.data === "string") {
out.add(decoded.data.toLowerCase());
}
} catch (err) {
// ignore malformed entries
}
}
return out;
}
export default async function handler(req, res) {
const host = req.headers?.host || new URL(SITE_ORIGIN).host;
const proto = req.headers?.["x-forwarded-proto"] || "https";
const requestUrl = new URL(req.url || "/", `${proto}://${host}`);
const pointer = requestUrl.searchParams.get("v") || "";
// Derive the origin from the REQUEST, not from a constant. Otherwise every
// non-production deployment (unstable, previews) advertises production's
// canonical URL and — worse — a twitter:player pointing at production's
// embed, so testing a preview link silently exercises the live site.
const origin = `${proto}://${host}`;
const shareUrl = pointer
? `${origin}/?v=${encodeURIComponent(pointer)}`
: origin;
const embedUrl = pointer
? `${origin}/embed.html?pointer=${encodeURIComponent(pointer)}`
: "";
const renderFallback = (reason) => {
// Short TTL: a video may be published, edited or moderated at any time, and
// we would rather re-ask the relays than pin a wrong card in someone's CDN.
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Cache-Control", "public, s-maxage=300, stale-while-revalidate=600");
res.setHeader("X-Bitvid-OG", reason);
res.status(200).send(
renderOgHtml({
video: null,
shareUrl,
fallback: { ...FALLBACK, url: origin, image: `${origin}/assets/jpg/bitvid.jpg` },
})
);
};
// Defensive: the rewrite should only send crawlers here, but never serve a
// human this stub if the rule ever widens.
if (!isCrawlerUserAgent(req.headers?.["user-agent"])) {
res.setHeader("Cache-Control", "no-store");
res.redirect(302, shareUrl);
return;
}
const filter = pointerToFilter(pointer);
if (!filter) {
renderFallback("no-pointer");
return;
}
const pool = new SimplePool();
let superAdminHex = "";
try {
const decoded = nip19.decode(ADMIN_SUPER_NPUB);
superAdminHex = decoded.type === "npub" ? decoded.data : "";
} catch (err) {
superAdminHex = "";
}
try {
const listFilter = (identifier) => ({
kinds: [ADMIN_LIST_KIND],
"#d": [`${ADMIN_LIST_NAMESPACE}:${identifier}`],
...(superAdminHex ? { authors: [superAdminHex] } : {}),
});
// One shared deadline across all three reads.
const now = Date.now();
const adminListsFresh =
adminListCache.expiresAt > now && adminListCache.whitelist instanceof Set;
const [videoEvent, whitelistEvent, blacklistEvent] = await Promise.all([
withTimeout(pool.get(RELAYS, filter), RELAY_TIMEOUT_MS, null),
adminListsFresh
? Promise.resolve(null)
: withTimeout(
pool.get(RELAYS, listFilter(ADMIN_LIST_IDENTIFIERS.whitelist)),
RELAY_TIMEOUT_MS,
null
),
adminListsFresh
? Promise.resolve(null)
: withTimeout(
pool.get(RELAYS, listFilter(ADMIN_LIST_IDENTIFIERS.blacklist)),
RELAY_TIMEOUT_MS,
null
),
]);
let whitelist = adminListsFresh ? adminListCache.whitelist : null;
let blacklist = adminListsFresh ? adminListCache.blacklist : null;
if (!adminListsFresh) {
whitelist = extractPubkeySet(whitelistEvent);
blacklist = extractPubkeySet(blacklistEvent);
// Only cache a whitelist we actually got. Caching an empty one would pin
// "whitelist-unavailable" for the whole TTL after a single slow read.
if (whitelist.size > 0) {
adminListCache = {
expiresAt: now + ADMIN_LIST_TTL_MS,
whitelist,
blacklist,
};
}
}
const video = parseVideoEvent(videoEvent);
if (!video) {
renderFallback("event-not-found");
return;
}
const gate = isPubliclyShareable(video, {
whitelistEnabled: true,
whitelist,
blacklist,
});
if (!gate.allowed) {
renderFallback(`gated:${gate.reason}`);
return;
}
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Cache-Control", "public, s-maxage=300, stale-while-revalidate=600");
res.setHeader("X-Bitvid-OG", "video");
res.status(200).send(
renderOgHtml({
video,
shareUrl,
embedUrl,
fallback: { ...FALLBACK, url: origin, image: `${origin}/assets/jpg/bitvid.jpg` },
gate,
})
);
} catch (err) {
renderFallback("error");
} finally {
try {
pool.close(RELAYS);
} catch (err) {
// ignore
}
}
}
+29 -5
View File
@@ -100,7 +100,15 @@
>
<!-- Left navigation column -->
<div class="profile-modal__menu" data-profile-mobile-menu aria-hidden="false">
<nav class="profile-modal__nav" aria-label="Profile sections">
<div class="profile-modal__menu-heading">
<h3 id="profileModalMenuHeading" class="profile-modal__menu-title">
Profile &amp; settings
</h3>
<p class="profile-modal__menu-hint">
Choose what you want to manage
</p>
</div>
<nav class="profile-modal__nav" aria-labelledby="profileModalMenuHeading">
<button
id="profileNavAccount"
type="button"
@@ -108,6 +116,7 @@
aria-controls="profilePaneAccount"
aria-selected="true"
data-state="active"
data-mobile-description="Profile and account settings"
>
<svg
class="h-4 w-4"
@@ -130,6 +139,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneRelays"
aria-selected="false"
data-mobile-description="Manage your relay connections"
>
<svg
class="h-4 w-4"
@@ -152,6 +162,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneWallet"
aria-selected="false"
data-mobile-description="Manage your wallet and payments"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -173,6 +184,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneStorage"
aria-selected="false"
data-mobile-description="Manage your stored files"
>
<svg
class="h-4 w-4"
@@ -195,6 +207,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneMyVideos"
aria-selected="false"
data-mobile-description="View and manage your videos"
>
<svg
class="h-4 w-4"
@@ -217,6 +230,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePanePlaylists"
aria-selected="false"
data-mobile-description="Organize your playlists"
>
<svg
class="h-4 w-4"
@@ -239,6 +253,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneHashtags"
aria-selected="false"
data-mobile-description="Explore and follow hashtags"
>
<svg
class="h-4 w-4"
@@ -261,6 +276,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneSubscriptions"
aria-selected="false"
data-mobile-description="Manage your subscriptions"
>
<svg
class="h-4 w-4"
@@ -286,6 +302,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneFriends"
aria-selected="false"
data-mobile-description="Find and manage your friends"
>
<svg
class="h-4 w-4"
@@ -308,6 +325,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneSafety"
aria-selected="false"
data-mobile-description="Privacy and safety settings"
>
<svg
class="h-4 w-4"
@@ -330,6 +348,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneBlocked"
aria-selected="false"
data-mobile-description="Manage blocked users"
>
<svg
class="h-4 w-4"
@@ -352,6 +371,7 @@
class="btn-ghost focus-ring relative w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneMessages"
aria-selected="false"
data-mobile-description="View your messages"
>
<svg
class="h-4 w-4"
@@ -379,6 +399,7 @@
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneHistory"
aria-selected="false"
data-mobile-description="View your watch history"
>
<svg
class="h-4 w-4"
@@ -408,6 +429,7 @@
class="btn-ghost focus-ring hidden w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneAdmin"
aria-selected="false"
data-mobile-description="Moderate users and content"
>
<svg
class="h-4 w-4"
@@ -429,10 +451,12 @@
aria-hidden="true"
></span>
</button>
</nav>
<div class="profile-modal__mobile-actions" role="group" aria-label="Session actions">
<button
id="profileMobileLockNowBtn"
type="button"
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium mt-6 lg:hidden hidden"
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium lg:hidden hidden"
title="Forget your unlocked key on this device and re-lock storage + wallet now"
>
<svg
@@ -453,7 +477,7 @@
<button
id="profileMobileLogoutBtn"
type="button"
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium mt-2 lg:hidden"
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium lg:hidden"
data-variant="critical"
>
<svg
@@ -471,7 +495,7 @@
</svg>
Logout
</button>
</nav>
</div>
</div>
<!-- Right content panes -->
@@ -1179,7 +1203,7 @@ https://another.blossom.example"></textarea>
></p>
<label id="profileMyVideosAutoShareRow" class="hidden flex items-start gap-2 text-sm text-muted-strong">
<input id="profileMyVideosAutoShare" type="checkbox" class="checkbox mt-0.5" />
<span>Auto-share new public videos to other Nostr apps (NIP-71)</span>
<span>Mirror new public videos to other Nostr apps by default (NIP-71)</span>
</label>
<p
id="profileMyVideosLoading"
+4
View File
@@ -309,6 +309,10 @@
<input type="checkbox" id="check-kids" class="checkbox" />
<span class="text-sm text-text">For Kids</span>
</label>
<label class="flex items-center gap-2 cursor-pointer select-none">
<input type="checkbox" id="check-no-nip71-mirror" class="checkbox" />
<span class="text-sm text-text">Do not share this video to other Nostr apps</span>
</label>
</div>
<!-- Advanced Options -->
@@ -0,0 +1 @@
Goal: TODO backlog progress, bug fixes, robustness and usability. Scope: dispatch triage and proposed first team. Non-goals: product edits, worker launch before approval, deploys, spending, production data. Done: goal recorded, baseline measured, concrete first-team proposal presented.
@@ -0,0 +1,3 @@
- Goal: classify and fix profile-modal-controller suite hang at current HEAD 9e6c310d (unchanged from filedAt 8b204687).
- Scope: owned test and narrow modal lifecycle helpers only. Preserve assertions; no retries, sleeps, timeout increases, quarantine or forced success exit.
- Done: deterministic regression, failing mutant, isolated natural exit, full 60s/file unit run, lint; build/browser if product changes; fleet commit, merge unstable, checks, queue and durable landing.
@@ -0,0 +1,5 @@
- Goal: make tests/race/nostr-client-init-race.test.mjs hermetic and naturally exit while preserving concurrent init, pool readiness, and post-init subscription coverage.
- Temporary ownership granted by dispatch via landed RULE-CHANGES/prompt and owner resume instruction; no Nostr product/shared helper/global runner edits.
- Revalidated unchanged from filedAt f303ab0e72; prior isolated assertion60.91ms passes but process timeout10s/exit124, full gate timeout60s at file227.
- Plan: inspect NIP01 and real init/L1 subscription lifecycle; replace delay with deferred pool barrier; virtualize current pool boundary, close subscription; add observable boundary/cleanup assertions; mutant-check, run both isolated suites, commit separately, merge unstable, lint and full60s/file gates, land normally or route further blockers.
- Preserve prior profile-modal commits and all assertions. No browser, real wallet, signer, external relay or deploy operations.
@@ -0,0 +1,4 @@
- Goal: cancel unstarted torrent health probes when last feed/card owner unregisters, preserving live shared owners and active probes.
- Scope: gridHealth.js, optional queue helper and focused tests/integrity/area notes. Excludes app, URL health, webtorrent implementation, playback/magnet helpers, cache TTLs/concurrency, runner/kit. Dispatch completion mailbox required; no scheduler.
- Revalidated unchanged gridHealth/cardObserver from filedAtf9adfc8f through startup66832dc3. Adapted actual dispatch five-card reproduction to own imports:4 start,1 queues, dispose/remove then release ->1 obsolete start; assertion exit1.
- Plan: introduce cancellable pending queue records and per-card leases; release at existing onCardUnregister; preserve active completion and cache, identity-check inflight deletion; reject stale observer/UI effects; scenario tests for shared owners, cancellation/re-request race, priority/concurrency, hidden reveal; grouped mutants; focused/full60s-file/lint/build/browser; commit/merge/normal landq/ancestry then close and report.
@@ -0,0 +1,3 @@
# client-reliability-4
Investigation only at4c54858c. URL queued media last-owner disposal reproduced: obsolete starts1 expected0; shared live-owner control passes. See test_logs/TEST_LOG_2026-10-20_03-10-00_client-reliability-4.md and /tmp/bitvid-client-reliability-4-url-lifecycle.mjs. Await dispatch scope before product/tests.
@@ -0,0 +1,3 @@
# client-reliability-4 complete
Product landed89ccb907, normal gate exit0 and7055496b ancestry verified. Backlog closed. All required local checks pass; evidence test_logs/TEST_LOG_2026-10-20_03-29-00_client-reliability-4-landed.md. Finish normal closure metadata landing/final event, then idle. No active abort, fallback/live peers,CPU or whole Stage3 claim.
@@ -0,0 +1,3 @@
# client-reliability-5
Investigation at0c1dd7e8:bounded active URL success/timeout cleanly settle; disposed active primary media error starts1 new mirror HEAD(expected0). Shared/rejoin and later same-source controls pass. Evidence test_logs/TEST_LOG_2026-10-20_13-30-00_client-reliability-5.md; scratch /tmp/bitvid-client-reliability-5-active-url.mjs. Await dispatch routing; no product/regression changes.
@@ -0,0 +1,3 @@
# Item5 implementation
Controller-only6-line pre-candidate ownership guard. Six new lifecycle scenarios reproduce2 baseline failures and pass18/18 fixed;6 reverted mutants rejected. Unique integrity note test_logs/TEST_INTEGRITY_client-reliability-5.md. Focused/lint/build running; awaiting explicit dispatch full-suite/browser slots and verified unstable merge before final checks/landing. No active abort/observer/cache/limit changes.
@@ -0,0 +1,6 @@
# storage-cors-1
- Start UTC: 2026-10-20T13:38:00Z; launch reported by dispatch ~13:37:59Z; acknowledgement 13:38:24Z.
- Goal: validate current-HEAD existing-origin loss via real helper and offline SDK/transport; propose preservation semantics before edits.
- Scope: investigation/evidence first; product/test changes require dispatch routing. No browser/cloud credentials/network bucket mutations; preserve preview3000.
- Done: claimed; traced callers; 5 characterization cases reproduced. See docs/testing/storage-cors-integrity.md.
- Pending: dispatch preservation semantics/fix routing, regression/mutants/focused checks, coordinated normal gates/build/landing.
@@ -0,0 +1,3 @@
# Item5 complete
Product landed0bd0b1c3 via4bc52f9d,normal gate exit0/unstable ancestry verified. Backlog closed;expensive slot and browser released to dispatch13:57UTC. Local427files/28focused/18lifecycle/6mutants/lint11/build/native browser smoke pass. Finish docs-only closure queue/final event then idle. Evidence test_logs/TEST_LOG_2026-10-20_13-58-00_client-reliability-5-landed.md. No active abort/playback/CPU claim.
@@ -0,0 +1,6 @@
# storage-cors-2
- Current route: read-only remaining storage/delete-safety audit per dispatch14:58:59; former feed scope deferredstorage3.
- Actual real-service malformed cached bucket/base reproduction2rejects vs0required;6controls pass,existing focused11scenarios/3files pass. docs/testing/storage-cors-delete-audit.md has fidelity/callers/limits.
- Product/test read-only until routing. Proposed defensive primitive-string handling/structured skip in r2Service.deleteVideoStorage + focused new regression. s3UploadService contains no duplicate delete method; no upload/key changes proposed.
- Pending: dispatch fix routing;claim item2 after queued metadata visibility. QA owns full/normal/browser;no full/build/browser/cloud.
- Scratch /tmp/storage-cors-delete-audit.mjs;deferred feed invalid-seed scratch /tmp/storage-cors-feed-audit.mjs preserved. No secret/private-data use.
@@ -0,0 +1,3 @@
# Item6 audit
Atd88716e3 real profile remove local/synced controls pass;historical runtime no-op fails both actual-wiring diagnostics after cleanup but existing profile27 stillpass. Valuable missing regression,notcurrentproductfailure. Verified metadata merge tobbf57388/claimed;profile product/tests unchanged. Await test-only routing;notes only,no product/test commits. See test_logs/TEST_LOG_2026-10-20_15-04-00_client-reliability-6.md. QA full/normal slot respected.
@@ -0,0 +1,3 @@
# Item6 retention
New tests/profile-history-removal-wiring.test.mjs retains3 actual profile/factory/DOM/controller/service scenarios;five runtime mutants fail with0cancelled. Product and existing suites unchanged. Focused/lint running;no full/browser/build until explicit resource grant. Unique integrity in test_logs/TEST_INTEGRITY_client-reliability-6.md. No real signer/relay/fullapp/reload claim.
@@ -0,0 +1,6 @@
# Item7 cold live feed notification
- Goal: cold cache then eligible live updates must notify actual loadVideos consumer;latest feed owns reused subscription and clear invalidates late callbacks.
- Scope: nostrService loadVideos notification state plus constructor/clear lifecycle only;new focused service test and unique evidence/backlog. No app/feed/filter/cache policy/runner changes.
- Plan: reproduce original service with controlled subscribeVideos boundary;prove latest/force/clear controls;minimal current-consumer token;mutants/focused/lint;explicit full slot then one unchanged normal gate;QA owns actual-app browser after landing/preview rebuild.
- Original4e904349: new first5 scenarios4fail1pass/0cancel;first8line callback fix later5pass2fail on7. Revised service19 added lines passes7/7. No actual browser/full/build yet.
@@ -0,0 +1,7 @@
# Local review scope
- Owner milestone:Ready for me to review and test locally.
- Codefixb1174163/full432gate0;preview6da89895/HTTP200/indexcmp0.
- Actualcoldfeed+separate25step3viewportcoverage complete;originalUpload2/4pxoverflowfailureopen,notwholeallpass.
- Finish durable named QA/dispatch docs and final runtime/preview audit;thenIDLE,nooptionalnewwork.
- Live and deferred gates:docs/agents/LOCAL-REVIEW.md andfleet/QUALITY.md.
@@ -0,0 +1,4 @@
- Goal: measure active URL-media cancellation at integrated HEAD5b77fd20 before proposing minimal fix.
- Scope: real observer/controller/cache with controlled external HEAD/media IO; scratch/evidence only.
- Done: resource/listener/timer/promise/slot/FIFO/cache/inflight/stale-write measurements; shared/rejoin/cross-controller/legacy controls; exact reproduction and scoped proposal to dispatch.
- No product/committed-test changes or full/build/browser without routing/grant. Preserve prior queued-only assertions pending explicit contract correction.
+234 -36
View File
@@ -759,6 +759,14 @@
@apply btn focus-ring rounded-full px-4 py-3 text-sm shadow-lg;
}
/* Fixed mobile actions need to clear the browser home indicator as well as
the visual spacing requested by their component. */
@media (max-width: calc(theme("screens.md") - 0.02px)) {
.mobile-safe-bottom {
bottom: calc(var(--space-lg) + env(safe-area-inset-bottom));
}
}
.site-update-notice__panel {
@apply w-full rounded-2xl border border-border bg-overlay-panel-soft p-4 text-left text-text shadow-modal backdrop-blur-sm;
}
@@ -767,6 +775,44 @@
@apply btn-ghost focus-ring -mr-2 -mt-2 h-9 w-9 rounded-full p-0 text-lg leading-none;
}
.site-tagline__text {
text-wrap: balance;
}
@media (max-width: calc(theme("screens.md") - 0.02px)) {
/* Both controls are fixed to the lower-right corner on phones. Keep the
site-update notice above the search FAB so neither tap target obscures
the other. */
.site-update-notice {
right: var(--space-lg);
bottom: calc(
var(--space-lg) + var(--space-2xl-plus) + var(--space-sm)
);
max-inline-size: min(
var(--size-20rem),
calc(100vw - (var(--space-lg) * 2))
);
}
.site-update-notice__button {
min-block-size: var(--size-11);
padding-inline: var(--space-md);
padding-block: var(--space-sm);
}
.site-tagline {
margin-block: var(--space-lg) var(--space-xl);
padding-inline: var(--space-md);
}
.site-tagline .site-tagline__text {
font-size: var(--font-size-lg);
line-height: var(--line-height-tight);
letter-spacing: var(--tracking-normal);
color: var(--color-text-muted);
}
}
.login-option::after {
content: "";
position: absolute;
@@ -983,7 +1029,7 @@
}
.moderation-badge {
@apply inline-flex flex-col items-start gap-sm rounded-xl border px-3 py-1.5 text-xs font-semibold normal-case tracking-tight shadow-sm;
@apply inline-flex max-w-full flex-col items-start gap-sm rounded-xl border px-3 py-1.5 text-xs font-semibold normal-case tracking-tight shadow-sm;
}
.moderation-badge--interactive {
@@ -991,11 +1037,11 @@
}
.moderation-badge__actions {
@apply inline-flex flex-nowrap items-center gap-sm;
@apply flex max-w-full flex-wrap items-center gap-sm;
}
.moderation-badge__label {
@apply inline-flex items-center gap-xs whitespace-nowrap;
@apply inline-flex min-w-0 items-center gap-xs;
}
.moderation-badge[data-variant="warning"] {
@@ -1035,11 +1081,12 @@
}
.moderation-badge__text {
@apply whitespace-nowrap text-xs font-semibold tracking-tight;
@apply min-w-0 text-xs font-semibold tracking-tight;
}
.moderation-badge__action {
@apply inline-flex items-center gap-1.5 rounded-full border border-status-warning-border bg-status-warning-surface px-3 py-1 text-xs font-semibold normal-case tracking-tight text-status-warning-on transition duration-150 ease-out;
@apply inline-flex max-w-full items-center gap-1.5 rounded-full border border-status-warning-border bg-status-warning-surface px-3 py-1 text-xs font-semibold normal-case tracking-tight text-status-warning-on transition duration-150 ease-out;
overflow-wrap: anywhere;
background-color: color-mix(
in srgb,
var(--status-warning) 16%,
@@ -1526,6 +1573,9 @@
.modal-sheet {
inline-size: 100vw;
block-size: 100vh;
block-size: 100dvh;
min-block-size: 100vh;
min-block-size: 100dvh;
max-width: none;
max-height: none;
margin: 0;
@@ -3592,6 +3642,12 @@
display: flex;
flex-direction: column;
gap: var(--space-sm);
min-block-size: 0;
}
.profile-modal__menu-heading,
.profile-modal__mobile-actions {
display: none;
}
.profile-modal__pane-wrapper {
@@ -3772,55 +3828,185 @@
Chromium drops the whole block. `theme("screens.lg")` resolves to a literal at
build time, which is valid. */
@media (max-width: calc(theme("screens.lg") - 0.02px)) {
.profile-modal__identity {
align-self: stretch;
max-inline-size: none;
min-block-size: var(--size-6rem);
margin-inline: var(--profile-modal-padding-inline);
margin-block: 0;
padding: var(--space-md);
border: 0;
border-radius: var(--radius-xl);
color: var(--color-white);
background:
radial-gradient(
100% 130% at 100% 0%,
color-mix(in srgb, var(--color-accent) 82%, transparent),
transparent 58%
),
linear-gradient(
120deg,
var(--color-text-strong) 0%,
color-mix(in srgb, var(--color-text-strong) 82%, var(--color-accent)) 100%
);
}
.profile-modal__identity-avatar {
inline-size: var(--space-2xl-plus);
block-size: var(--space-2xl-plus);
border-color: var(--color-white);
}
.profile-modal__identity-kicker {
color: var(--color-white);
}
.profile-modal__identity-name {
font-size: var(--font-size-lg);
color: var(--color-white);
}
.profile-modal__menu {
flex: 1 1 auto;
overflow-y: auto;
overscroll-behavior: contain;
padding-block-end: max(var(--space-lg), env(safe-area-inset-bottom));
scroll-padding-block-end: max(
var(--space-lg),
env(safe-area-inset-bottom)
);
scrollbar-gutter: stable;
}
.profile-modal__menu-heading {
display: block;
padding-block: var(--space-xs) var(--space-sm);
}
.profile-modal__menu-title {
margin: 0;
font-size: var(--font-size-sm);
font-weight: 700;
letter-spacing: var(--tracking-wide);
color: var(--color-text);
}
.profile-modal__menu-hint {
margin: var(--space-3xs) 0 0;
font-size: var(--font-size-xs);
color: var(--color-text-muted);
}
.profile-modal__nav {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
display: flex;
flex-direction: column;
gap: var(--space-xs);
/* Every tile the SAME height, so a 2-line label can't stretch its row
taller than the others (the "ragged grid of ellipses" fix). */
grid-auto-rows: var(--profile-modal-nav-tile-height);
align-self: stretch;
}
.profile-modal__nav > button {
display: flex;
flex-direction: column;
display: grid;
grid-template-columns: var(--space-2xl-plus) minmax(0, 1fr) var(--space-lg);
grid-template-rows: auto auto;
align-items: center;
justify-content: center;
text-align: center;
gap: var(--space-2xs);
justify-content: start;
column-gap: var(--space-md);
row-gap: var(--space-3xs);
width: 100%;
block-size: 100%;
/* Consistent rounded-square tiles, not rounded-full ellipses (btn's
default) that turn into circles once a tile gets taller. */
border-radius: var(--radius-xl);
/* Subtle fill so they read as solid cards, not floating outlines. The
active tile's bg-primary (higher specificity) still wins. */
min-block-size: var(--size-5-75rem);
text-align: start;
font-size: var(--font-size-sm);
font-weight: 700;
line-height: var(--line-height-tight);
border-radius: var(--radius-lg);
border-color: var(--color-border-subtle);
background-color: var(--color-panel);
padding-block: var(--space-xs);
padding-inline: var(--space-3xs);
line-height: 1.15;
padding: var(--space-sm);
overflow: hidden;
/* wrap labels at spaces, not mid-word */
overflow-wrap: break-word;
word-break: normal;
hyphens: none;
box-shadow: var(--shadow-sm);
}
.profile-modal__nav > button[data-state="active"] {
color: var(--color-text);
border-color: color-mix(in srgb, var(--color-accent) 36%, transparent);
background-color: color-mix(in srgb, var(--color-accent) 9%, var(--color-panel));
box-shadow:
inset var(--size-3px) 0 0 var(--color-accent),
var(--shadow-md);
}
.profile-modal__nav > button:focus-visible {
outline-offset: calc(var(--space-3xs) * -1);
}
/* slightly larger glyph for the tile */
.profile-modal__nav > button svg {
width: var(--space-lg);
height: var(--space-lg);
flex-shrink: 0;
grid-column: 1;
grid-row: 1 / span 2;
width: var(--space-xl);
height: var(--space-xl);
padding: var(--space-sm);
box-sizing: content-box;
border-radius: var(--radius-md);
color: var(--color-text);
background-color: var(--color-page-alt);
}
.profile-modal__nav > button::before {
content: "";
grid-column: 3;
grid-row: 1 / span 2;
inline-size: var(--space-xs);
block-size: var(--space-xs);
border-block-start: var(--border-width-hairline) solid currentColor;
border-inline-end: var(--border-width-hairline) solid currentColor;
transform: rotate(45deg);
}
.profile-modal__nav > button::after {
content: attr(data-mobile-description);
grid-column: 2;
grid-row: 2;
align-self: start;
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 400;
line-height: var(--line-height-tight);
}
.profile-modal__nav > button[data-state="active"] svg {
color: var(--color-white);
background-color: var(--color-accent);
}
/* In the tile grid the unread dot rides the tile's top-right corner instead
of the vertical-nav left edge (where it landed in the grid gutter). */
.profile-modal__nav > button .profile-nav__unread-dot {
top: var(--space-2xs);
top: var(--space-sm);
left: auto;
right: var(--space-2xs);
right: calc(var(--space-sm) + var(--space-lg));
transform: none;
}
.profile-modal__mobile-actions {
display: flex;
flex: 0 0 auto;
gap: var(--space-xs);
padding-block-start: var(--space-xs);
padding-block-end: max(
var(--space-sm),
env(safe-area-inset-bottom)
);
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--color-page) 0%, transparent),
var(--color-page) var(--space-sm)
);
}
.profile-modal__mobile-actions > button {
flex: 1 1 0;
justify-content: center;
min-block-size: var(--size-11);
border-radius: var(--radius-lg);
}
}
@media (min-width: theme("screens.lg")) {
@@ -6734,6 +6920,18 @@
max-height: none;
}
@media (max-width: calc(theme("screens.lg") - 0.02px)) {
#profileModal {
height: 100dvh;
min-height: 100dvh;
}
#profileModal .bv-modal__panel {
min-height: 0;
overflow: hidden;
}
}
#profileModal .bv-modal__panel {
width: 100%;
height: 100%;
@@ -0,0 +1 @@
Use the existing backlog as the starting point; verify stale reports. Keep the initial team as small as evidence permits. No product edits by dispatch. Conservative operating limits are manager defaults, not attributed owner quotes.
@@ -0,0 +1,5 @@
# Approved worker bootstrap
- Accept scoped routine-work relay with explicit provenance; spend/publish/deploy approvals remain session-local and unauthorized.
- Staff one client-reliability worker, no subagents. Preserve preview and schedules.
- One-time administrative-only queue gate verifies exact expected roster and doc/backlog diff; no persistent gate modification or product/test change. This resolves roster-before-launch versus existing suite-hang dependency under the owner's routine local landing authority. Report the unresolved test baseline.
@@ -0,0 +1,9 @@
# client-reliability-1: classify the profile-modal timeout
- Product/test source unchanged between filedAt 8b204687 and startup HEAD 9e6c310d.
- Clean-environment 60s/file run reproduced timeout. Per-scenario tracing then completed all 26 scenarios naturally in 57041.55ms; no blocked await or retained resource was found. Timing near the bound varies with cold/warm JSDOM style caches.
- CPU profile of the Escape scenario points to CSS color/border normalization and computed-style resolution. The full markup has many controls; updateFocusTrap asks JSDOM for a full computed declaration for each control on repeated pane refreshes.
- Fix only the stylesheet-free test fixture: virtualize display/visibility, with explicit fixture CSS and a new native-JSDOM contract scenario. Preserve every existing assertion and real controller, focus events, DOM and pane operations. No timeout/retry/sleep/runner or product change.
- Regression compares fixture/native style filtering for visible, disabled, CSS-hidden, inherited-visibility-hidden, aria-hidden, and hidden-type controls, then checks actual Tab event focus wrapping.
- Limits: fixture does not model application CSS/layout. This suite never loaded those styles; browser and build checks are not required for this test-only fix. No release, browser, signer, network or wallet qualification claimed.
- Slowdown: initial cold worktree lacked dependencies; cached offline install resolved it. Native DOM assertion diffs made the first mutant diagnostic expensive; comparing control IDs yields a bounded useful failure. Tool improvement: show per-scenario start/end timestamps when a file exceeds its diagnostic bound, distinguishing slow progress from a blocked await.
@@ -0,0 +1,5 @@
# Bounded follow-up routing
- Retain one worker: this is another test-harness boundary; no evidence warrants another product-code owner.
- Grant one race-test file plus regression/integrity notes. Product/shared-helper scope needs explicit routing.
- Preserve separate commits, assertions, natural process exit, and normal full landing gates. No test fix landed yet.
@@ -0,0 +1,8 @@
# client-reliability-2: hermetic concurrent init
- NIP01 REQ/CLOSE and docs/architecture-refactor.md L1 contract require callers to close subscriptions. Retain real init/L1/feed behavior; fake only IO boundaries.
- Root cause: original client.connectToRelays stub returned [], explicitly signaling initial connection failure. init schedules ConnectionManager's timer, whose callback calls manager.connectToRelays (not the client stub). That path attempts configured public relays and retains retries. Original subscription was also never unsubscribed.
- Replace 50ms mock sleep with deferred entry/release barriers. Successful manager-level connection fixture prevents retries without disabling product retry logic. Pin timer absence with assertion; teardown resets as failure-path hygiene, not a success substitute.
- Add stricter outcomes: pending second caller, observed pool feed filter, nonempty relays, and exactly-once transport closure. Preserve all original expectations and await both callers.
- Mutate via test-local IO/init overrides only; product Nostr/shared helpers/global runner remain untouched. Both early-return and failure-connection variants fail naturally.
- Prior profile-modal commits preserved separately. No actual signer/wallet/relay/browser/build/deploy qualification. Normal full gates required before either item lands.
@@ -0,0 +1,6 @@
# Next bounded task and coordination qualification
- Keep one worker; prior test tasks done, product grid-health scope is now isolated.
- Require actual observer-to-probe lifecycle reproduction, not source-extracted queue inspection. Dispatch sees1 obsolete start after grid disposal, expected0.
- Bound fix to queued work. Shared live owners and later requests must survive; cancelled work must not populate negative cache.
- Mailbox files are delivery/storage, not wakeup. Record missing automatic dispatch continuation and require explicit completion evidence; no unattended qualification, scheduler or kit change.
@@ -0,0 +1,9 @@
# client-reliability-3: queued probe ownership
- Actual baseline at66832dc3 (source unchanged from filedAtf9adfc8f): actual attachHealthBadges + DOM + observer, four deferred probePeers calls active and fifth queued; disposing/removing feed then releasing active work starts one obsolete probe (expected0), assertion exit1.
- Queue records own a Set of live card requests. onCardUnregister releases the matching request; only an unstarted job with no remaining owner can cancel. Cancellation resolves null, skips normalization/cache, and removes inflight synchronously. Promise finalization checks record identity so an old cancellation cannot delete replacement inflight work.
- Active jobs are never aborted or removed from inflight on disposal; their normal cache result remains usable. Existing raw magnets/webseeds, four-slot concurrency, stable priority ordering, healthy/negative TTL remain unchanged.
- Live-card membership rejects late observer entries. Each request also guards result writes by ownership/identity even while disposed DOM remains connected. Per-registration badge listener removal and idempotent disposer prevent stale callbacks or an old disposer unregistering a newly attached feed.
- Existing cardObserver unregister hook is sufficient; no shared helper, URL health, WebTorrent implementation, app, cache policy, concurrency, styling, runner or kit edits.
- Browser smoke uses actual built modules/native observers and fake external probe IO only; both Chromium and Firefox pass. No external request attempted. Hosted-only/torrent-only source visibility checked; real playback fallback, live torrent peers, CPU/RSS and active abort behavior unmeasured/out of scope.
- Fixture correction before acceptance: use explicit viewport geometry so observer priorities are deterministic; initial unpinned JSDOM viewport reversed two fixture assumptions. No existing assertion changed. Eleven mutants independently kill the nine grouped lifecycle scenarios (details in integrity note).
@@ -0,0 +1,7 @@
# Final callback ownership corrections
- Caller review shows app.boundStreamHealthBadgeHandler is shared across feeds; preserve global callback dedup with registration reference counts. Disposing one owner must retain the handler for another owner; final disposal removes it. Wrapping each registration separately would duplicate callbacks.
- Synchronous checking notifications can dispose/rerender the feed before queue submission. Measured1 start after disposal, expected0; check live membership after notification before creating queued work.
- Late observer scenario also asserts no disposed-card notifications reach a remaining registration, not just final badge state. This rejects a removed observer guard even when the reentrant guard still prevents queue submission.
- Final source has11 lifecycle scenarios;14 independent mutants were rerun against this exact product tree, all exit1 with1 failed/0 cancelled, then restored. Source outside gridHealth.js remains untouched.
- Corrected build and Chromium/Firefox built-dist/native-observer smoke both pass; no external requests or page errors. Actual media playback fallback and CPU/RSS remain unverified.
@@ -0,0 +1,3 @@
# client-reliability-4
Stop at smallest reproduced queued URL media defect. Keep active URL/torrent cancellation separate. Exercise real cache/observer/controller and only virtualize external IO. Await explicit routing before product/regression tests.
@@ -0,0 +1,7 @@
# client-reliability-4 implementation
- Existing observer unregister boundary owns per-badge lifecycle tokens, read by controller via WeakMap. Existing app/feed callback arguments unchanged; direct legacy controller consumers remain live until settlement.
- Module-scoped promise→ownership WeakMap mirrors shared real inflight cache across controllers. Last-owner release only cancels queued media jobs. Already-running media/cache behavior remains; no active HEAD/media/HLS/torrent abort.
- Queue resumes reserve a slot synchronously to preserve three-media cap. Cancellation settles with an explicit internal outcome propagated through mirrors and omitted from health cache. Existing state-cache promise identity guard preserves immediate re-request.
- Guards bind UI/RAF work to captured registration; late observer callbacks/eager microtasks and stale disposer cannot affect reused registrations.
- No app/shared observer/cache-module or policy/TTL edits. Scope/read-only predecessor evidence at77d486fb; dispatch implementation routing merged before code.
@@ -0,0 +1,3 @@
# client-reliability-5
Do not equate retained active work with a leak: explicit success and controlled real timeout release resources/slots and cache correctly. Route only demonstrated new mirror HEAD after disposal; guard candidate continuation in controller before considering active abort or broader resource policy. Existing observer token is sufficient.
@@ -0,0 +1,6 @@
# Item5 continuation boundary
- Keep bounded already-active success/failure caching. Check existing lifecycle owners only BEFORE starting each candidate in probeUrlList; zero owners marks existing record cancelled and returns explicit cancellation. No probe abort or observer/cache/TTL/concurrency change.
- Shared and rejoining owners remain eligible because record owners are checked at continuation time. Completed final-source failure keeps original cache behavior; cancelled mirror chains never cache offline.
- Six focused regressions exercise actual observer/controller/cache plus external IO twins; unique integrity file during parallel trial.
- Dispatch resource coordination: local full suite is next after administrative drain but requires explicit grant; browser belongs initially to playback QA. All CLI/gate launches clear inherited NODE_TEST_CONTEXT operationally; no kit/runner change.
@@ -0,0 +1,6 @@
# storage-cors-1 proposed semantics (awaiting dispatch)
- Preserve every existing rule and field; do not union new origins into arbitrary rules (would grant their permissions to new origins).
- Read existing policy before writing; only explicit NoSuchCORSConfiguration indicates absent policy. Other failures propagate without PUT.
- Add the existing default rule for newly requested origins separately. Skip PUT when equivalent existing default rules already cover every requested origin.
- Preserve wildcard rules verbatim; never infer extra methods/headers or edit unknown fields. Exact equivalence/coverage details require routing before implementation.
- No dependencies/SDK version change. Inspect SDK export read-only until routing.
@@ -0,0 +1,3 @@
# Item6
Classify historical no-op as a regression gap: actual wiring works,but existing fake-renderer profile suite passes27/27 even with historical override. Propose focused new real-wiring test and unique integrity,not product or app/history-service/cache edits. Exercise DOM event and backing persistence/recreation;fake only synced external IO and unrelated constructor dependencies. No live signer/relay or full app/reload claim.
@@ -0,0 +1,6 @@
# storage-cors-2
- Reject malformed connection fields as invalid-connection; no String(object) or arbitrary trim invocation for cleanup authorization/base/key material.
- Reject nonstring owner identifiers as invalid-owner before resolution. Null/non-object parameter options safely use existing no-videos result.
- Preserve existing owned-base/key collector,credential lock reasons,per-object failure summaries,provider-aware client branches and content-addressed upload semantics.
- s3UploadService contains no affected delete counterpart; no upload-copy edits per explicit dispatch ruling. Tests prove both provider branches instead.
- No broad catch; existing client-construction and each-delete failures retain structured results. Arbitrary throwing getters/proxies beyond serializable cache metadata are outside this proof.
@@ -0,0 +1,7 @@
# Item6 test design
- Use actual profile config/factory and DOM click with original renderer removal/controller/local service. Observe actual removal promise only for deterministic completion. Required unrelated constructor services and read/metadata boundary are controlled; no fake renderer/remove implementation.
- Local and synced removal include final-item case and renderer recreation; local Clear is a separate scenario with real queue reset. No synced Clear assumption.
- Synced IO twin respects replace vs merge, so missing replace is observable rather than rubber-stamped. Assert all published payload/options plus publication occurrence, without requiring duplicate implementation calls.
- Isolated runtime mutants exercise historical config no-op,clear no-op,drop-replace,wrong actor and final-item no-op. Explicit teardown runs on assertion failure and restores queue/app/client/globals.
- No product/existing suite edits; focused/lint only until dispatch allocates full/normal slot. Build/browser remain unverified/outside current grant for test-only retention.
@@ -0,0 +1,6 @@
# Item7 notification ownership
- Live updates notify the same full ordered cached list emitted/persisted already,including deletion-only empty results. Preserve event payload/deleted metadata and filtering.
- A service-owned notification object carries latest loadVideos consumer across reused subscription. Creating replacement subscription uses fresh identity;clear invalidates before unsub. Old subscription callback cannot change cache,notify consumer,emit or persist after disposal.
- Interpreter independently identified stale captured consumer;our A→B and force/clear tests fail firstcallbackpatch,pass revisedtoken. Service lifecycle state explicitly routed by dispatch16:10:24.
- Keep existing cache/persistent callbacks and callback-throw isolation. Event-only consumers remain supported. No productpolicy change/forced UI refresh. Browser qualification belongs to QA.
@@ -0,0 +1,6 @@
# Local review decisions
- Fix demonstrated cold-feed hardblocker with actualservice regression/currentconsumer token;no forcedrefresh substitute.
- One unchanged normal fullgate authoritative;do not duplicate whole suite solely to produce fullerlogs. Storedtail is not complete process transcript.
- Preserve original strictUploadoverflowFAIL;separatefollowon captures remainingcoverage withoutallpassclaim. Minor2/4pxoverflow controlsusable/sourceunproven staysopen forlocalreview;no speculativeCSSfix/optionalpolishbatch. Interpreter independently agreesclassification.
- Stop alloptionalwork at ownerlocalreviewmilestone;real signer/cloud/peer/production gates stayopen.
@@ -0,0 +1,4 @@
- Implement only controller retainUrlProbe final-owner hook and active media settle/cleanup. Existing shared WeakMap owners and cancelled result paths suffice; no AbortController or observer/cache edit needed for video-element cleanup.
- Mark cancelled synchronously before promise continuations so same-turn rejoin cannot attach to obsolete work. Clear active cancellation hook on completion; reuse existing finally for exactly-once slot/FIFO release.
- Strict spec correction authorized at3dac0afb and recorded in docs/spec_changes/2026-10-20-client-reliability-8-active-media.md; retain live-owner cache/timeout/mirror contracts independently.
- No full/build/browser before dispatch allocation; one unchanged normal full landing gate can be authoritative after merged focused/lint.
+14
View File
@@ -31,6 +31,16 @@ Welcome to the bitvid documentation. This directory contains detailed architectu
- **[Design System](./design-system.md)**: Guide to the token-based design system and CSS architecture.
- **[Menus & Popovers](./menus.md)**: Documentation for the menu and popover system.
## Testing
- **[Playwright Agent Testing Infrastructure](./testing/agent-playwright-harness.md)**: Full reference for the agent test harness (`window.__bitvidTest__`), mock relay, Playwright fixtures, and `data-testid` selectors.
- **[Test Integrity (Dark Factory Standard)](./testing/test-integrity.md)**: The scenario-first testing constitution — anti-cheat rules, the Spec Correction Protocol, and the Test Integrity Note format.
## Lessons & Incident Writeups
- **[Cloudflare/S3 Upload-Path Gotchas](./lessons/cloudflare-s3-upload-gotchas.md)**: Audited 2026-06-16 — duplicated upload services, storage-key collisions, and object-cleanup contracts.
- **[NIP-07 Signer Reliability](./lessons/nip07-signer-reliability.md)**: Hard-won 2026-06-16 — diagnosing unresponsive NIP-07 signers and the resilience invariants that must not regress.
## Operations
- **[Instance Configuration](./instance-config.md)**: Guide to configuring a bitvid instance.
@@ -39,6 +49,10 @@ Welcome to the bitvid documentation. This directory contains detailed architectu
## Active plans
- **[Performance & Efficiency Plan](./performance-efficiency-plan.md)**: A
measurement-first staged plan for reducing client CPU, memory, and network
work without degrading Nostr discovery, feed freshness, moderation, or
playback. Tracked as TODO #34.
- **[BitUnlock Permission-Layer TODO](../todo/TODO_2026-07-23_bitunlock-permission-layer.md)**: Ordered implementation checklist for the BitUnlock-backed media permission and subscription work.
For more details on contributing, please see [CONTRIBUTING.md](../CONTRIBUTING.md).
+103
View File
@@ -0,0 +1,103 @@
# TODO — what each area did NOT finish
## dispatch
- Next bounded item requested by interpreter2026-10-21T02:40:06Z under continuing owner instruction: existing client read-only currentHEAD active HEAD-request last-owner abort reproduction/shared/rejoin controls;no product/test edits before evidence. HLS/torrent separate. Quiet-hours22:00–08:00 hold for new starts/messages;route next foreground checkpoint at/after08:00 local without another permission loop. No scheduler created.
- client8 FINAL:product43cb1baf/closure2ea6f068/handoff9cbb53f8 verified;432fullgate0,dispatch29/29/build08620a71/artifact0/preview200+cmp0,nativecontrolledbothbrowserPASS/released. AllIDLE;HEAD/HLS/torrent activeprobe work remains unfinished pending nextboundedrouting. No decoder/bytes/CPU/liveproof.
- 2026-10-20 client8 active URL-media product43cb1baf/full432gate0,dispatch29/29/build08620a71/artifact0/previewHTTP200+cmp0. WorkercontrollednativeChromium+Firefox smokePASS/released;closuremetadata/browsernotes normaldocsqueue pending. No realdecoder/networkbyte/CPU/HEAD/HLS/torrent qualification;broaderactiveprobe scope remains unfinished. Finishdocs/handoff thenawaitnextboundedrouting.
- 2026-10-20 bounded continuation: client8 active URL-media last-owner/shared/rejoin reproduction after maintenance. Await measured evidence then independent confirmation/minimal fix and explicit test-contract routing. Other optional work remains deferred; no active-abort completion claim.
- 2026-10-20 LOCAL REVIEW: storage2/history6/cold-feedclient7 normally landed;latest codeb1174163 full432filegate0,dispatchnew7/7/build-artifact0. Preview3000 build6da89895/sourceb1174163/runtime-equivalentdocsdescendants. Actualcold3cardsPASS;separate25-step360/390/1280navigation/Home/Profile/Uploadvalidation/focuscoveragecomplete. OriginalUploadoverflowFAIL remainsopen2/4px(controlsusable/sourceunproven),notwholebaselineallpass. FinalQA/docsqueuehandoff thenallIDLE;nooptionalwork. Feed3/publicplaybackpromises/liveAmber/cloud/peers/manualrelease remainunfinished. See docs/agents/LOCAL-REVIEW.md.
- 2026-10-20 local-review checkpoint: storage2 producte7b9afb3/closurec5818cfe and history6 regressionb3fab032/closure386cc60e durably complete. Normal latest gate431files exit0; dispatch cleanup13/13 and history3/3. Integrated preview1834b6a9 runtime equals current test/docs-only descendants. Final actual-app/mobile QA2 browser pass active; readiness awaits actual flow evidence/handoff. Feed3 and public-promised playback roadmap deferred/open; real Amber/cloud/peers/release gates remain. No optional new tasks.
- 2026-10-20 trial outputs complete:QA regression1bb3a4fc/closurebbf57388 verified,dispatch independent4/4/runtime unchanged so no extra build. Same3 workers continue:client6 real history test-only retention after independent current pass/historical-noop failure;storage2 method-only malformed metadata no-throw guard after independent2rejects/6controls;QA2 actual-app local core/mobile3viewports browser-only grant. Full slots on validated-ready after QA browser release;no feature/readiness/live-proof claim.
- 2026-10-20 owner continuous-quality direction recorded;fleet/QUALITY.md tracks local vs real release evidence. QA full/normal slot active;client item6 profile-history wiring audit and storage item2 feed/view tombstone audit assigned now,read-only before fix routing. Remaining:actual coverage/reproduction decisions,current QA landing,local mobile/core-flow baselines and unqualified live gates.
- 2026-10-20 storage productd3f4c215/closure7f99c045 verified;dispatch CORS9/9/build-artifact/previewHTTP200+dist match. QA exclusive full/normal slot actual grant14:49:35/read14:49:46 after65m54wait;storage-release→grant39m57 wake gap. Remaining:QA durable regression landing,final trial throughput/stall assessment,next bounded routing. Actual cloud/IAM/noCAS concurrent safety/live peers unverified.
- 2026-10-20 trial first product landed: item5 product0bd0b1c3,closuredc852597;dispatch lifecycle18/18/build-artifact/previewHTTP200+dist match. Storage slot grant13:58:27 after949s wait,full/normal landing underway;QA next,ready95a77a7f. Remaining: storage/QA durable landings,preview refresh after product changes,final trial evidence;real cloud/peers/activeabort/CPU unverified.
- 2026-10-20 trial current: all3 pickups verified; admin notes durableb922c186. Client item5 owns exclusive full/browser/product landing from13:43:51 (~10m57s wait); storage CORS ready9/9+8mutants,next slot; QA retained4/4+6mutants,third slot,browser released. Remaining: full merged-tree validation,durable product/test landings,preview refresh and final trial outcome. No real cloud/peers/CPU qualification.
- 2026-10-20 parallel trial: rosterbbc1be79 durably green,3 workers launched/read-next-action verified. Item5 ready12f92732 focused28/6mutants/lint/build awaits full+browser; storage-cors real origin loss independently reproduced/fix routed; playback-qa controlled native local fallback3/browser reviewed,test retention approved. Remaining trial product/test full gates,orderly durable landings and preview; actual WebTorrent/live peers/CPU unverified. Initial gate transport/capture failure corrected only by clean environment; QA browser coordination deviation recorded.
- 2026-10-20: continuing owner direction: item5 investigation assigned to same worker, active URL media last-owner/shared-owner controls first. Remaining: reproduction and narrow fix routing or next evidenced TODO. No product fix assumed; active abort/live playback/CPU-RSS unverified.
- 2026-10-20: item4 done: product89ccb907,closuref116f7c8 durable landed. Dispatch independent URL tests20/20,build/artifact exit0,previewHTTP200 and index matches dist. Worker full427/427,focused22,13 mutants,lint11,browser smoke recorded. Worker idle; next active-probe candidate needs fresh reproduction/routing. Actual playback/live peers/CPU-RSS/full Stage3 unfinished.
- 2026-10-20: item4 reproduction confirmed independently: obsolete URL media starts1(expected0), shared owner control pass. Worker routed to queued URL ownership/cancellation in observer/controller and focused tests. Remaining: implementation, mutant/full/browser gates, landing, preview refresh; active abort excluded.
- 2026-10-20: item4 assigned to existing worker for investigation only: reproduce remaining URL/active torrent cancellation at HEAD, then route a narrow fix. No product fix yet; live playback/CPU-RSS remain unverified. Owner continuation relayed; quiet-hours exception does not change schedule.
- 2026-10-19 latest: item3 normal product landing7d20a509 and closure7fde734d verified; dispatch synced and preview rebuilt(HTTP200). Independent original five-card scenario now0 obsolete starts(previous1), lifecycle11/11. Worker evidence426/426 full files,lint11/11,14 rejected mutants,Chromium+Firefox controlled-IO lifecycle smoke. Worker idle; no new task launched in this mail checkpoint. Remaining: actual media fallback/live-peer qualification,CPU/RSS measurements,active-probe/URL cancellation and other performance-plan slices.
- 2026-10-19: dispatch-2 ongoing coordination. Synced to f9adfc8f; normal-gate fixes de6918ce verified (425/425 file evidence, lint11/11, independent profile27/27/race1/1). Route client-reliability-3 to same worker for reproduced queued probe-after-feed-disposal bug; scope/evidence in area prompt. Next: verify actual worker resume and completion reports, keep preview current after verified product landing.
- Coordination limitation: operator checkpoint resumed dispatch; file mailbox does not itself wake idle Codex. Completion handoff now explicitly requires mailbox evidence, but unattended dispatch wakeup is unqualified. No scheduler/kit changes at this checkpoint.
## client-reliability
- 2026-10-20: item6 DONE: retained actual profile-history wiring regression landed b3fab032;normal gate exit0 with431 discovered/431 unique file starts,focused38/38,5 killed mutants,lint11/11,submitted ancestry0. Test-only,existing product behavior passes. Evidence test_logs/TEST_LOG_2026-10-20_15-41-00_client-reliability-6-landed.md. Real signers/relays/encryption/full-app modal/browser/build/synced Clear unverified. Expensive slot released;IDLE for owner local review,no optional work.
- 2026-10-20: item6 test-only retention ready:new real-profile wiring3/3,focused5files/38scenarios,5 isolated runtime mutants failnaturally/0cancelled,lint11pass. Local remove/lastitem/clear/recreation andfake-synced reduced+empty replacement covered;historicalno-op rejected. No product/existing suite edits;full/normal slot pendingexplicit grant,full/browser/build unrun;real signers/relays/fullapp/syncedClear unverified. Unique integrity/evidence in test_logs/*client-reliability-6*.
- 2026-10-20: item6 audit atd88716e3(profile paths unchanged throughbbf57388):real local profile remove/clear and fake-synced remove/recreation pass,0network;historical runtime no-op failsboth aftercleanup but existingprofile27/27 stillpasses;lowerlayers3files/8pass. Classify missing real-wiring regression,notcurrentproductfailure. Proposed test-only focused file+uniqueintegrity;await routing,QA expensive slot respected. Evidence test_logs/TEST_LOG_2026-10-20_15-04-00_client-reliability-6.md. No product/test changes or live signer/relay/fullapp proof.
- 2026-10-20: item5 done/closed after normal product landing0bd0b1c3(submission4bc52f9d,gate exit0,unstable ancestry exit0). Full427files/focused28/lifecycle18/six reverted mutants/lint11/build-artifact/Chromium+Firefox controlled smoke pass;obsolete mirror HEAD1→0. Browser+expensive slot released13:57UTC after10m57s initial wait. Dispatch landing/release event sent;preview refresh dispatch-owned. Closure metadata/final follow;no next item. Active abort/actual fallback/livepeers/CPU/fullStage3 unverified.
- 2026-10-20: item5 ready atmergedf2a392fa(product12f92732):full427/427 files60s-bound exit0,focused28/lifecycle18,six reverted mutants,lint11/build-artifact/native Chromium+Firefox smoke pass;obsolete mirror HEAD1→0,shared/rejoin/retry/hidden reveal pass,0 external/pageerrors. Full-slot wait10m57s recorded;browser released,expensive slot held through normal landing. Remaining queue/durable ancestry/close;active abort/real playback/CPU unverified.
- 2026-10-20: item5 routed controller-only pre-candidate guard implemented; baseline16pass/2fail→fixed18/18 lifecycle,6 rejected/reverted mutants,focused28/4files,lint11/build+artifact pass. No full/browser launch while dispatch administrative drain/QA reservation holds; slot requested13:32:54UTC,await explicit grant and verified unstable merge. No active abort/observer/cache/limits change; actual fallback/CPU unverified. Unique integrity/evidence in test_logs/*client-reliability-5*.
- 2026-10-20: item5 investigation at0c1dd7e8:active URL success/real4000ms controlled timeout release media/slot correctly;disposed active primary media error starts1 obsolete mirror HEAD(expected0),strict assertion naturally exits1 after cleanup/retry. Shared/rejoin owner and later same-source controls pass. Proposed controller-only pre-candidate ownership guard + focused tests; await dispatch routing. No product/tests changed; active abort/browser/fallback/CPU unverified. See test_logs/TEST_LOG_2026-10-20_13-30-00_client-reliability-5.md.
- 2026-10-20: item4 done/closed after normal landing89ccb907(submission7055496b,gate exit0,unstable ancestry exit0). Full427/427 files,focused22,lifecycle12,13 reverted mutants,lint11,build/artifact/native Chromium+Firefox controlled IO pass;obsolete starts1→0. Dispatch landing event sent; preview refresh owned by dispatch. No further item without routing. Actual media fallback/live peers/active abort/CPU and whole Stage3 unverified. Closure metadata/final event follow.
- 2026-10-20: item4 ready atce1381f5; full427/427 file exits0 with60s/file bound,focused22/22,lifecycle12/12,13 rejected/reverted mutants,lint11/11,build/artifact and Chromium+Firefox controlled native-observer smoke pass (obsolete starts0). Remaining normal queue/durable landing/ancestry then close/report. Actual fallback/live IO/active abort/CPU remain unverified.
- 2026-10-20: item4 routed implementation in URL observer/controller complete locally;12 lifecycle scenarios and13 reverted mutants, focused/lint/build pass. Queued last-owner work settles without cache/mirror/stale UI; shared owners, immediate retry and hidden reveal covered. Remaining: native browser smoke/full merged-tree gate/normal landing/closure; active abort/fallback/CPU unverified.
- 2026-10-20: client-reliability-4 investigation at4c54858c reproduces queued URL media start after feed disposal: active3/queued1 before and after disposal, then1 obsolete start(expected0); shared live-owner control deduplicates and receives healthy badge. Strict assertion exits1, diagnostic/control naturally exit0. No product/tests changed; awaiting dispatch fix routing. Active abort/browser/fallback/CPU unverified. Evidence: test_logs/TEST_LOG_2026-10-20_03-10-00_client-reliability-4.md.
- 2026-10-19: client-reliability-1 assigned: reproduce/classify and fix profile-modal suite hang without weakening assertions. Initial evidence: three isolated 60s timeouts (one clean environment), full baseline stopped at file 220/425; read docs/agents/prompts/client-reliability.md and claim the item. Scope excludes app.js and unrelated subsystems.
- 2026-10-19: client-reliability-1 fix committed at dbdac18d (test-only fixture, original assertions preserved; native focus contract and both mutants fail when broken). Isolated27/27 natural exit2.44s, lint11/11. Full60s/file run passed226/425 files including profile-modal; file227 tests/race/nostr-client-init-race.test.mjs timed out;198 later files untested. Await dispatch routing for this unowned blocker before normal landing/close. No product/browser/build qualification or landing claimed. See test_logs/TEST_LOG_2026-10-19_20-13-00_client-reliability-final.md.
- 2026-10-19: client-reliability-1 and client-reliability-2 ready for normal landing at181eae95: profile27/27 and race1/1 natural exit; all425 unit files passed with60s/file bound; lint11/11. Four deliberately broken variants fail and are reverted. Dispatch routing unblocked the second item; no product Nostr/shared-runner changes. Remaining: durable normal queue landing, unstable verification, then backlog close. Browser/build and real IO unverified (test-only scope). See test_logs/TEST_LOG_2026-10-19_20-35-00_client-reliability-both.md.
- 2026-10-19: client-reliability-1 and client-reliability-2 finished: durable normal-gate landing de6918ce (submission683cedf0), gate exit0 and unstable ancestry verified. Profile27/27, race1/1 natural exit, lint11/11, full425/425 file exits0; all four mutants rejected. Backlog closed after landing. No unfinished product work for these items; browser/build/real IO unverified (test-only scope). Closure metadata queued separately; see test_logs/TEST_LOG_2026-10-19_20-34-50_client-reliability-landed.md.
- 2026-10-19: client-reliability-3 implemented at current worktree: actual five-card obsolete starts1→0 after last-owner disposal; shared queued/live owners and active probe cache preserved; inflight identity and stale registration guards. Focused25/25 across4 files,11 mutants rejected, lint11/11, build/artifact verification pass, native Chromium/Firefox built-dist lifecycle smoke pass with0 external requests/page errors. Remaining: committed merged-tree full426-file gate, normal landq, durable ancestry/close and dispatch events. Active abort, URL probes, actual media fallback/peers and CPU/RSS unverified/out of scope; entire Stage3 performance plan is not complete.
- 2026-10-19: client-reliability-3 ready for normal queue at607d5ce1: full426/426 files exit0 with60s/file bound;27 focused scenarios,14 reverted mutants rejected, lint11/11, build/artifact verification pass. Corrected Chromium/Firefox built-dist/native-observer smoke:0 obsolete starts/stale callbacks/page errors/external requests; hidden reveal and source-health visibility pass. Remaining: normal queue/durable landing/ancestry then close and dispatch landing/final evidence events. Actual playback fallback/live peers, active abort/URL cancellation, CPU/RSS and full Stage3 remain unverified/out of scope.
- 2026-10-19: client-reliability-3 finished and closed after verified normal landing7d20a509 (submissione522efc1, gate exit0, unstable ancestry exit0). Full426/426 files,focused27/27,14 reverted mutants,lint11/11,build/artifact verification and native Chromium/Firefox controlled-IO smokes pass; obsolete starts1→0. Dispatch landing evidence sent; closure metadata/final report follow. Dispatch owns preview sync/rebuild after its resume; mailbox does not wake idle sessions. Actual media fallback/live peers/CPU-RSS, active abort/URL cancellation and whole Stage3 remain outside qualification; no new work without routing.
## storage-cors
- 2026-10-20 complete:productd3f4c215/closure7f99c045,full428/428,focused13/new9,10mutants,lint11/build;dispatch independent9/9+preview verification. Idle while QA trial output integrates. Live bucket/IAM/provider serialization/concurrent noCAS safety unverified.
- Parallel trial item1: reproduce CORS existing-origin preservation offline before routing fix; no cloud changes.
## playback-qa
- 2026-10-20 item1 done:retained regression1bb3a4fc/closurebbf57388,full429/429/4focused/6mutants/lint11,dispatch independent4/4. Item2 nowactual-app local mock-relay/private-port core/mobile baseline,onebounded Chromium pass/noUIedits/browser-only allocation. Real peers/fullapp qualification stays open until actual evidence.
- Parallel trial item1: local fallback qualification/regression; browser initially reserved to QA; product read-only.
+46
View File
@@ -0,0 +1,46 @@
# Local review handoff — 2026-10-20
Status: ready for owner local review with the known minor Upload overflow below. Owner subsequently authorized bounded active-probe/URL cancellation work;the active URL-media slice is now landed43cb1baf. New localpreview build08620a71 replaces prior6da89895:build/artifact exit0,hostHTTP200/indexbyteequalcmp0. Dispatch independently29/29 lifecycle;unchanged normal432-filegateexit0. Worker native Chromium+Firefox controlled media smoke passed and owned resources released. These use actual controller/observer/native src/load plus controlled state/HEAD/intercepted media IO and synthetic completion;pause is stubbed. Not full-app decoder/live playback or universal networkabort/byte/CPU proof. HEAD/HLS/torrent cancellation remains open. Closure/browser evidence durably2ea6f068;item8 done,team idle until next bounded scope.
Open [the existing local preview](http://127.0.0.1:3000). It is already running; no terminal command is needed. The existing dispatch preview serves built `dist`. Current build08620a71 is from integrated43cb1baf. Historical core/mobile QA build6da89895 (an opaque artifact identifier, not a Git SHA) was produced from integrated unstableb1174163, including the cold-feed fix. HTTP200 and byte equality of served and built index were verified; runtime equality for final documentation descendants will be checked before handoff. Dispatch branch is area/dispatch. Final documentation landing SHA is reported in the owner handoff; runtime/build-input equality through QA closurec017069c was verified without rebuilding.
## Changes available to review
- Disposed feed owners no longer start obsolete queued torrent/URL media probes; an active failed media probe also cannot start an orphan mirror HEAD. Shared owners and active work retain existing semantics.
- Fresh relay video updates now notify the current feed consumer, so a cold empty feed can render newly arrived eligible videos. Obsolete subscription callbacks cannot update a replacement feed.
- Bucket CORS updates preserve existing rules and append only missing origin coverage. Unexpected reads cannot overwrite policy.
- Storage cleanup safely skips malformed cached primitive fields rather than rejecting; R2 and generic S3 deletion share this service. Ownership boundaries remain enforced.
- Actual playback fallback/magnet behavior and actual profile-history removal wiring now have regression coverage. The history behavior already worked; the new test catches the historical no-op implementation.
## Evidence
Latest unchanged normal gate candidate b1174163 completed successfully across432 configured test files, with empty quarantine and no shard. Durable landing and submitted ancestry were checked independently. Retained runner output contains432 unique starts; the stored gate result is a tail, not a complete install/process transcript, and its final stdout marker is absent. No suite or gate was weakened.
Dispatch independently checked URL lifecycle18/18, CORS9/9, fallback4/4, malformed cleanup13/13, new real history wiring3/3, and cold-feed current-consumer notification7/7. Workers also recorded failing restored mutants, focused suites, lint11 stages, and normal full gates. [History evidence](../../test_logs/TEST_LOG_2026-10-20_15-41-00_client-reliability-6-landed.md) and [dispatch evidence](../../test_logs/TEST_LOG_2026-10-20_15-45-00_dispatch-review.md) retain exact scope.
Before the fix, the actual-app reproduction was: cold Trending started from an empty cache, received three valid eligible local video events, and retained an empty DOM. Passive trace proves relay EVENT/EOSE, all client/app maps and filtered videos contain3, while view remainsEMPTY. Client7 fix landedb1174163 with active-consumer and stale-callback lifecycle controls plus regression coverage. The rebuilt artifact6da89895 renders all three cold-feed cards without a forced refresh. The controlled browser uses genuine cached Buffer and a genuine local VP8 fixture outside immutable app files, synthetic identity/notes and local mock relays. This does not qualify real signers, public relays or deployed CDN behavior.
## Actual-app baseline and known finding
Controlled Chromium154 on immutable build6da89895 completed Trending/Explore/ForYou/Home navigation, three-card hydration, synthetic login, Profile and Upload open/close, six Tab steps contained in each modal, exact trigger focus restoration, missing-title and missing-source validation, and no video publish from invalid forms at360×800,390×844 and1280×720. Feed cards/document fit those viewports. Genuine cached Buffer, real toolkit signature checks and local VP8 were used; public HTTP/WebSocket traffic was denied, with no page errors or local asset failures in these final runs.
The original strict baseline stopped on Upload horizontal document overflow. It remains FAILED/open: document width362 at360px and394 at390px; no overflow at1280. Targeted follow-on coverage completed independently; critical inputs, Publish and Close remain horizontally inside the viewport and close restores focus. This is not a wholly passing baseline. Exact overflowing source element is not yet established; no speculative CSS fix was made. Some measured controls are below44px; real-device usability/accessibility review remains open.
The browser, private app/relay servers and temporary dependency link were closed after the follow-on run at16:28:10.112UTC. The owner preview on3000 remains running.
See [QA evidence and screenshots](../testing/playback-qa-2-final-evidence.md) for exact final and follow-on results, preserved failures and fixture provenance.
## Suggested review in the browser
1. At desktop and narrow mobile widths, browse Trending/Explore/For You, open and close Profile and Upload, and check keyboard focus and horizontal scrolling.
2. In Upload external mode, check missing title and missing playable source errors. Avoid publishing or using real bucket credentials during this local review.
3. Check playback source/fallback messaging with content you intend to test. Controlled fallback tests do not establish live torrent availability.
4. Check history removal/clear and reload behavior using the account/signing workflow you intend to qualify; real account evidence is still open.
## Remaining scope
Actual full-app media playback/Firefox, real Amber/NIP46 account/switch/logout, encrypted relay persistence, live storage/IAM/provider behavior and concurrent CORS writes, real WebTorrent peers, production service-worker/offline/CDN behavior, real devices and manual release QA remain unverified. Known parallel Firefox hydration and isolated logout issues remain documented in KNOWN_ISSUES.md.
Feed/view tombstone audit remains deferred as storage-cors-3. Public-promised playback backlog remains open: RangeGET health compatibility, persistence/per-host scoring, parallel startup, hover/viewport warmup, complete mirror/Blossom discovery, ranking/status and service-worker followups. Partial existing implementations are not whole-item completion. See [quality matrix](../../fleet/QUALITY.md).
This milestone is local review/testing, not production readiness or completion of the broader roadmap. Workers idle after evidence closure; no push, deploy, spend, real cloud/account/peer operation or scheduler activation was performed.
+66
View File
@@ -0,0 +1,66 @@
# Program — the owner's priorities and rulings
Last updated: 2026-10-20
## Current milestone
Active URL-media cancellation slice is landed43cb1baf after normal432-file gate exit0;dispatch independent29/29,build08620a71/artifact pass/previewHTTP200+index equality. Worker controlled native Chromium+Firefox smoke passed and resources released. Closure/browser evidence durably landed2ea6f068;item8 done,team idle awaiting next bounded scope. HEAD/HLS/torrent active cancellation remains open; broader optional work remains deferred.
Local-review handoff: storage2/history6 and demonstrated cold-feed blockerclient7 are normally landed. Build6da89895/preview3000 verified;actualcold3cards and separate25-step360/390/1280core/modal coverage complete. Original strictUploadoverflow FAIL remains open(2/4px,controlsusable),not an all-green baseline. Finish docs-only evidence/handoff,then allIDLE for owner review;no optional backlog work. Broader roadmap/live signer/cloud/peer/release gates remain explicit in QUALITY/LOCAL-REVIEW.
## Priorities
1. Owner direction: TODO backlog progress, bug fixes, robustness, and usability.
2. Session-manager sequencing (vetoable): prioritize reproducible failures in core user flows and small changes with clear completion checks.
## Rulings log (newest first)
### 2026-10-20 — Resume bounded active-probe cancellation, relayed by interpreter
> "Then continue with the active-probe/URL cancellation work." — owner, relayed by interpreter at20:44:41UTC; not represented as typed in dispatch.
Route existing client worker to client-reliability-8: reproduce current active URL-media last-owner disposal through actual observer/controller entrypoints and controlled IO, with shared live-owner and fresh-rejoin controls. Measure resources, settlement, slot release, cache and stale callbacks before any fix. Start media before broader HEAD/HLS/torrent policy. Existing queued/orphan-mirror fixes are complete; active abort is not. Existing tests protect the previous queued-only contract; expectation changes require explicit routing and Test Integrity Note after evidence. No app.js/kit/scheduler/external/cloud/signer/push/deploy/spend authority. Other workers idle unless needed for bounded independent QA.
### 2026-10-20 — Explicit local-review stopping milestone,relayed by interpreter
> "Ready for me to review and test locally" — owner, relayed by interpreter;not represented as typed in dispatch.
Finish current storage2/history6 gates/landings,faithful local actual-app/core/mobile baseline and preview refresh,then provide local-review handoff before declaring this milestone met. Do not clear optional roadmap or claim production/live Amber/cloud/peer proof. No new restricted-action authority.
### 2026-10-20 — Continuing quality objective, relayed by interpreter
> "ok try to keep them working until the project is in a good place." — owner, relayed by interpreter; not represented as typed in dispatch.
Continue the existing robustness/usability backlog beyond the trial, with bounded evidence-first tasks and adaptive staffing. Local proof and real Amber/cloud/live-peer/manual release gates stay distinct. This does not add push/deploy/spend/real-wallet/production-data/scheduler authority or authorize new product definitions. Track current quality and next work in fleet/QUALITY.md.
### 2026-10-20 — Bounded parallel trial, owner words relayed by interpreter
> "id really like to stress test the multi agent process so if it needs more agents to get more done faster. do it" — owner, relayed by interpreter; not represented as typed by owner in dispatch.
Interpreter requests executing dispatch plus up to3 workers now with disjoint worktrees/scopes. Routine launches are authorized; retain existing restricted-action limits. Trial measures acknowledged assignments, actual parallel evidence/checks, durable queued landings and contention, not process count.
### 2026-10-19 — Roster-only bootstrap gate (session manager call, VETOABLE)
The first approved worker is assigned to the pre-existing profile-modal test hang. Normal roster-before-launch ordering otherwise invokes that same hanging full-suite gate before a worker can investigate. For this one administrative roster/brief commit only, dispatch will use a temporary queue configuration whose gate verifies the exact committed change contains only the expected area registration and documentation/backlog metadata, with product code, tests, and persistent gates unchanged. No product fix is covered by this exception. Preserve normal full gates for subsequent code. Record the actual gate result and disclose the unresolved unit failure.
### 2026-10-19 — Routine fleet authority, relayed from Codex desktop chat
> "yes please approve whatever it wants and let me know about it" — owner, as relayed in this session from the Codex desktop chat; not represented as typed by the human in this terminal.
The relay instruction received here explicitly authorizes operating tmux as the fleet interface, approving and starting the proposed client-reliability worker, and making routine staffing, local test/build, backlog prioritization, fixes, and local unstable landing decisions within the recorded goal without repeated approval. Keep the team small and scopes separate. This does not authorize deploys, spending, production data, real-wallet operations, or scheduler activation. Refresh the existing local preview after verified changes; send concise owner-mailbox updates for launches, assignment changes, landings, and blockers, including team size, areas, tasks, and verification. This supersedes the initial waiting state for first-worker approval only within that routine-work scope.
### 2026-10-19 — Initial goal
> "lets try to work our way through some of the issues in the TODO backlog and find and bugs and generaly make the client more robust and usable. ." — owner
Applies to: all areas. Recorded by: dispatch.
### 2026-10-19 — Conservative startup limits (session manager call, VETOABLE)
No deploys, spending, production data, real-wallet operations, or scheduler activation. Present the first team for owner approval before starting workers.
## Standing rules
- Approvals for spend, publishing and deploys are typed by the owner in the session that acts.
- Agreed work is never silently dropped: a priority change defers it; only the owner cancels, with a reason.
+24
View File
@@ -0,0 +1,24 @@
# Resume brief: active URL-media cancellation slice complete
Read PROGRAM, GOAL, QUALITY, LOCAL-REVIEW and newest RULE-CHANGES. Owner continuation "Then continue with the active-probe/URL cancellation work." was relayed by interpreter20:44:41UTC. Bounded client8 active URL-media slice complete;no new optional task. All workers IDLE/resources released. HEAD/HLS/torrent active cancellation remains open and is not qualified by this slice;route next bounded work explicitly. No push/deploy/spend/cloud/signer/peer/scheduler authority.
- Product3474668a durably43cb1bafe69f0bb3bce698e0b748281d1a04c061,unchanged normalgate exit0. Dispatch independent discovery432=432unique retainedstarts/missingextra0,durable status/log/ancestry0. Focused37/lifecycle29/14restored failingmutants/lint11 worker evidence;dispatch exactcandidate29/29 natural313ms.
- Final-owner active media src/listener/timer cleanup,nullsettlement/no cache,no obsolete mirrors,slot/FIFO release. Shared/cross/legacy/live cache+timeout/retry preserved;same-turn freshowner starts replacement,oldcallbacks/finalizer cannot affect it. Explicit strict spec correction in docs/spec_changes/2026-10-20-client-reliability-8-active-media.md and test_logs/TEST_INTEGRITY_2026-10-20_client-reliability-8.md. Product delta controller only18+/2-.
- Closure/browser evidence cfc20107 durably2ea6f0682bd6433cbbf0a2782fcb6ba14bd3de42,docsOnly/no gates. Dispatch handoff f233eaf2 durably9cbb53f8;runtime/buildinputs diff0 vs43cb1baf. Current .task/queue/log show final docs landing;never infer from silence.
- Preserved preview http://127.0.0.1:3000 rebuilt artifact08620a71 from43cb1baf (artifact identifier,notGit SHA). Build+artifactexit0;hostHTTP200/indexbyteequalcmp0. No deploy/server replacement. Native Chromium+Firefox controlled smoke bothPASS/resourcesreleased21:05:25UTC:actualcontroller/observer/native src/load,controlledstate+HEAD/interceptedmedia/syntheticcompletion,pause stubbed. No decoder/liveplayback/networkbyte/CPU/universalabort proof.8requests each,requestfailed7/8 observed;not all pendingabort asserted. Scratchscript/output inspected,not duplicate native run.
- Earlier localreview core/mobile QA remains historical build6da89895:actualcold3cards+25namedsteps360/390/1280. Original strictUpload overflowFAIL362/360,394/390 remains open/controlsusable. Do not call wholebaselineallgreen. See docs/agents/LOCAL-REVIEW.md;realAmber/cloud/peers/manualrelease gates remain open. Storage2/history6/coldfeed7 previously durable;feedtombstone3/publicplaybackpromises deferred/open.
- Final dispatch evidence test_logs/TEST_LOG_2026-10-20_20-57-00_dispatch-client8-candidate.md;no duplicate full after authoritative432gate. Initial sandbox autodraindied before running;worker approved env-cleared hostlander restart recovered same submission,no resubmission/gatechange. Routine acknowledgements need no reply;send outcome/blocker/release only.
Earlier local-review history below remains provenance,not current preview version or active assignment.
# Resume brief: 2026-10-20 local review handoff
Read PROGRAM, GOAL, QUALITY, LOCAL-REVIEW and newest RULE-CHANGES. Owner stopping milestone is "Ready for me to review and test locally" (relayed by interpreter). No optional work starts until owner review/confirmed actionable feedback. No push/deploy/spend/real cloud/account/peer/scheduler authority.
- Storage cleanup malformed metadata producte7b9afb3/closurec5818cfe,history regressionb3fab032/closure386cc60e done.
- Actual-app cold feed bug:eligible/cache3 butDOM0;live service omitted onVideos. Minimal current-consumer/stale-subscription notification fixb1174163556e2705ba85868e21adcaf51b44bd5f,closure09b13219. Unchanged normal432-filegateexit0,focused27/9failingmutants/lint11;dispatch independentnew7/7.
- Previewhttp://127.0.0.1:3000 alreadyrunning,node135479/cwddispatch;preserve. Build6da89895 fromb1174163,build/artifactexit0,hostHTTP200/indexcmp0. Laterdocs-only descendants need runtime equality check,not redundantbuild.
- QA actualcold3cardsPASS/noforcedrefresh. Original360Uploadstrictwidth assertionFAIL retained. Separate25namedsteps cover360/390/1280Trending/Explore/ForYou/Home/Profile/Uploadrequiredvalidation/6Tabcontainment/closefocusrestore. Document362at360/394at390,1280none;criticalcontrolsinside,minorusablefindingopen,exactculpritunproven. Notwholebaselineallpass. Browser/privateQAserversclosed16:28:10.112UTC.
- Controlled genuineBuffer/cachemirror+realcrypto/localVP8/syntheticidentity+mockrelay;publicnetworkdenied. No realAmber/signers/relay/cloud/IAM/peers/production/offlineCDN/manualreleasequalification.
- Feedtombstoneauditstorage3 andpublic-promisedplaybackbacklogremainexplicit/deferred. Priortrialparallelimplementation+95m57prepared-outputwalltime has no controlledserialbaseline/speedupclaim;39m57dispatchwakegap+serializedgates recordedLOG.
- Client/storageIDLE;QA finishingdocs-onlyevidence/closure thenIDLE. Dispatchfinishesdurabledocs/handoff+runtimepreviewaudit,thenIDLE. See current.task/mailbox/landq for finaldocsSHA;do notinferlandingfromsilence.
+12
View File
@@ -0,0 +1,12 @@
# Model tier strikes
Evidence loop for per-area model tiers. A strike is one of: red landing, withdrawn
submission, reviewer rejection, peer-caught false green, owner-reported regression.
- Two strikes in a week (week = from the usage reset) -> propose moving the area UP a tier.
- Two clean weeks -> offer moving it DOWN a tier.
- Loses the thread after compaction -> raise its context.
Changes go into fleet.config.json (area `tier` / `context`) and need a fresh start of that area.
| date | area | tier | strike | evidence (SHA, check, who caught it) |
|---|---|---|---|---|
+32
View File
@@ -0,0 +1,32 @@
# Wind-down and restart
Used when the usage meter passes the wind-down mark (usage.windDownAtPct, default
95%), before a known reset, or when the owner says so.
## Wind-down (the session manager runs it; one message to every area)
1. Create `<stateDir>/PAUSED` containing the reason and time. Scheduled jobs (except
restart) skip while it exists.
2. Tell every area: "Wind-down: finish or park your current step, commit, merge
<main>, submit if green, update `.task` and your docs/TODO.md section, then stop
taking turns. If you have a scarce machine, say you are done with it in .task."
3. Wait for each area's one-line report (SHA + state), then `node tools/fleet/bin/doctor.mjs`:
no DIRTY, no ONE-DISK rows, queue drained (`landq.mjs status`).
4. Write docs/agents/RESUME-BRIEF.md (mid-flight table, held work, owner questions)
and land it.
5. Stop sessions only if needed (`fleet down`); an idle session costs nothing.
## Restart (after the reset)
1. Remove `<stateDir>/PAUSED` (the restart job does it).
2. `fleet up` (resumes every area by session id; the session manager last).
3. Each area reads RESUME-BRIEF.md and its TODO section, reports one line if blocked.
4. The session manager checks `doctor`, `landq status`, and picks up the loop.
## Meter thresholds (if the usage governor is on)
| meter | action |
| --- | --- |
| >= windDownAtPct | wind down now, tell the owner |
| slowDownAtPct .. windDownAtPct | stop filling lulls: only close, relay and land |
| below | proceed |
@@ -0,0 +1,10 @@
{
"id": "client-reliability-1",
"area": "client-reliability",
"status": "done",
"title": "Diagnose and fix profile-modal suite hang without weakening assertions",
"blockedOn": null,
"addedOn": "2026-10-19",
"filedAt": "8b204687d2c0020c0fe97cedb3e9f8661a8d709a",
"closedOn": "2026-10-19"
}
@@ -0,0 +1,10 @@
{
"id": "client-reliability-2",
"area": "client-reliability",
"status": "done",
"title": "Make Nostr initialization race test hermetic and release retained relay work",
"blockedOn": null,
"addedOn": "2026-10-19",
"filedAt": "f303ab0e72ac60b49c5e1bd712b080de2a95a483",
"closedOn": "2026-10-19"
}
@@ -0,0 +1,10 @@
{
"id": "client-reliability-3",
"area": "client-reliability",
"status": "done",
"title": "Cancel queued torrent health probes when their last feed owner is disposed",
"blockedOn": null,
"addedOn": "2026-10-19",
"filedAt": "f9adfc8f531bcb77d4dc6e5487f76dbce0916167",
"closedOn": "2026-10-19"
}
@@ -0,0 +1,11 @@
{
"id": "client-reliability-4",
"area": "client-reliability",
"status": "done",
"title": "Reproduce remaining source-health cancellation defect at current HEAD before scoping a fix",
"blockedOn": null,
"addedOn": "2026-10-20",
"filedAt": "604c8c61f4783717eb0cff7ca7ca33efb59aac8a",
"evidence": "Product landed89ccb907 via7055496b,normal gate exit0 and unstable ancestry exit0. Full427/427 files,focused22,lifecycle12,13 reverted mutants,lint11,build/artifact and Chromium+Firefox controlled-IO smoke pass;obsolete starts1\u21920. See test_logs/TEST_LOG_2026-10-20_03-29-00_client-reliability-4-landed.md. Active abort/playback fallback/live peers/CPU unverified.",
"closedOn": "2026-10-20"
}
@@ -0,0 +1,11 @@
{
"id": "client-reliability-5",
"area": "client-reliability",
"status": "done",
"title": "Reproduce remaining active URL or torrent probe lifecycle defect before scoping a fix",
"blockedOn": null,
"addedOn": "2026-10-20",
"filedAt": "3f627f5df3da7bcd28d910d9f8161844fa77ff74",
"evidence": "Landed0bd0b1c3 via4bc52f9d,normal gate exit0/unstable ancestry exit0. Full427files,focused28/lifecycle18,six reverted mutants,lint11/build/native browser smoke pass;obsoleteHEAD1\u21920.10m57s initial slot wait;expensive/browser released13:57UTC. See test_logs/TEST_LOG_2026-10-20_13-58-00_client-reliability-5-landed.md;activeabort/playback/CPU unverified.",
"closedOn": "2026-10-20"
}
@@ -0,0 +1,11 @@
{
"id": "client-reliability-6",
"area": "client-reliability",
"status": "done",
"title": "Audit real profile history removal wiring and historical no-op regression coverage",
"blockedOn": null,
"addedOn": "2026-10-20",
"filedAt": "7f99c0456931c917082d6a6680c79655221dcc82",
"evidence": "Test-only retained regression9afe48a5 landed b3fab0326b3924d128b7928f4588751a003390ad via ec41d338;unchanged normal gate exit0,431 discovered/431 unique starts;focused38/38,5 mutants killed,lint11/11. Ancestry exit0. Final evidence test_logs/TEST_LOG_2026-10-20_15-41-00_client-reliability-6-landed.md. No product fix or live/browser qualification;IDLE owner local review.",
"closedOn": "2026-10-20"
}
@@ -0,0 +1,11 @@
{
"id": "client-reliability-7",
"area": "client-reliability",
"status": "done",
"title": "Restore cold-feed live subscription onVideos notification after eligible relay updates",
"blockedOn": null,
"addedOn": "2026-10-20",
"filedAt": "386cc60e1fa3ba6d9b67a1171a1ab89c03df84ca",
"evidence": "Service fixfecf4294 durablylandedb1174163556e2705ba85868e21adcaf51b44bd5f viaf39622da;unchanged normalgateexit0/432discovered+unique starts;new7/7,mergedfocused27/5files,nine naturallykilledmutants,lint11/11,ancestry0. Fullslotreleased16:21:30UTC;dispatch previewbuild+QA actualapp retestpending,notclaimed. Final evidence test_logs/TEST_LOG_2026-10-20_16-22-00_client-reliability-7-landed.md;IDLE/no optionaltasks.",
"closedOn": "2026-10-20"
}
@@ -0,0 +1,11 @@
{
"id": "client-reliability-8",
"area": "client-reliability",
"status": "done",
"title": "Measure and scope active URL-media last-owner cancellation with shared/rejoin controls",
"blockedOn": null,
"addedOn": "2026-10-20",
"filedAt": "6609269ea11aaff753e6d9f3d8b91eb04e219a20",
"evidence": "Implementation3474668a durablylanded43cb1bafe69f0bb3bce698e0b748281d1a04c061 via unchanged normalgateexit0/432discovered+unique file starts;candidate ancestry0. Focused37/3files,lifecycle29,14restored failing mutants,lint11. Active media final-owner cleanup/null/no cache with same-turn replacement/shared/cross/legacy/FIFO/live timeout controls. Strict spec correction documented. FULLSLOT released21:03UTC. Dispatch integrated preview/build/native qualification pending;no worker browser/build or HEAD/HLS/torrent/realfallback/CPU proof. Evidence test_logs/TEST_LOG_2026-10-20_21-03-00_client-reliability-8-landed.md.",
"closedOn": "2026-10-20"
}
+10
View File
@@ -0,0 +1,10 @@
{
"id": "dispatch-1",
"area": "dispatch",
"status": "done",
"title": "Record owner goal, establish baseline, and prepare first worker proposal",
"blockedOn": null,
"addedOn": "2026-10-19",
"filedAt": "d90591163f3db6ff99c1cfad6b736bbb1ee98a59",
"closedOn": "2026-10-19"
}
+11
View File
@@ -0,0 +1,11 @@
{
"id": "dispatch-2",
"area": "dispatch",
"status": "done",
"title": "Launch approved client-reliability worker and maintain owner handoff and preview",
"blockedOn": null,
"addedOn": "2026-10-19",
"filedAt": "8b204687d2c0020c0fe97cedb3e9f8661a8d709a",
"closedOn": "2026-10-20",
"evidence": "Local-review handoff normally landed3fd0ab594dd412a3a223b31605d57e21536b5e4d viaf3dc7d13;QAqualificationclosurec017069c. Codeb1174163 normal432gate0,build6da89895/previewHTTP200/indexcmp0;actualcold3cards+separate25step3viewportcoverage. Original2/4pxUploadoverflowFAIL retainedopen,notallgreen;workersIDLE/nooptionalwork. Finalruntime/previewaudit and evidence inLOCAL-REVIEW/QUALITY."
}
+12
View File
@@ -0,0 +1,12 @@
{
"id": "playback-qa-1",
"area": "playback-qa",
"status": "done",
"title": "Qualify local hosted-source fallback with independent regression and browser evidence",
"blockedOn": null,
"addedOn": "2026-10-20",
"filedAt": "0c1dd7e86be3e28edc96aca47f7753a98990dde0",
"notes": "Complete: submission64143f42 durable landed1bb3a4fc at15:02:17Z; unchanged normal install+npm test gate exit0, target ancestry exit0. Merged diagnostic429/429,focused4/4,lint11/11; six isolated mutants rejected/restored. Native Chromium+Firefox3 scenarios each passed local VP8 decode64x64/time>0 through mirror/substituted torrent transport,zero external requests. No real WebTorrent/full app/wholeStage3 qualification. Unique evidence includes early browser/gate scheduling intervention and corrected timing: wait65m43 to actual14:49:35 grant+11s pickup,storage release→dispatch grant39m57 wake lag. QA expensive slot released15:02:30Z; browser released13:40:46Z. No new item; dispatch to integrate shared status.",
"closedOn": "2026-10-20",
"landedSha": "1bb3a4fc452969f060c09a286b72b8cd7ac4123e"
}
+14
View File
@@ -0,0 +1,14 @@
{
"id": "playback-qa-2",
"area": "playback-qa",
"status": "done",
"title": "Capture actual-app local core-flow/mobile baseline before UI changes",
"blockedOn": "",
"addedOn": "2026-10-20",
"filedAt": "1bb3a4fc452969f060c09a286b72b8cd7ac4123e",
"notes": "Final granted run16:25:30.066\u219216:25:33.901UTC,Chromium154,b1174163/build6da89895,frozen830files/pinsverified.9stepsPASS/1FAIL:realcrypto/startup/cold3cards/login/360feed/nav/Profile;Uploadtitle/missingsource/no-publish/6Tab checks reached,then documentoverflowfail (PNG362 vs360).No remainingHome/390/1280/Uploadclose. Allownedresourcesclosed/RELEASE,no productwrites. Exact report/driver/screenshots docs/testing/playback-qa-2-final-evidence.md. Evidenceclosure only,NOT baselinepass/close. Separate dispatch-authorized16:27:44.416\u219216:28:10.112 follow-on25steps/0hardfail:all3viewportTrending/Explore/ForYou/Home3cards/Profile+Uploadvalidation/focus/close completed. Exactmobileoverflow2px/4px,panelsfit,controlsusable;originalfailureopen,notallpass. Browser/serversclosed/RELEASE;docs-onlynormalqueue thenIDLE perdispatch. Dispatch accepted completedqualification withknownopenminorlayoutfinding16:29:54;close does not mean overflow fixed or originalbaselineallpass. Evidence17163984 landed06e920e7/durable+ancestry0.",
"closedOn": "2026-10-20",
"resolution": "Bounded qualification completed with known minor open mobile Upload document overflow (2px at360,4px at390); original strict baseline remains failed. Separate25step3viewport/core/Home/focus coverage completed. Dispatch accepted docs-only closure16:29:54; no CSS/product fix claimed.",
"evidence": "docs/testing/playback-qa-2-final-evidence.md",
"landedSha": "06e920e77e756a4ad0c7d6495a2dbd930cf8482a"
}
+10
View File
@@ -0,0 +1,10 @@
{
"id": "storage-cors-1",
"area": "storage-cors",
"status": "done",
"title": "Reproduce existing-origin loss in bucket CORS provisioning using offline transport",
"blockedOn": null,
"addedOn": "2026-10-20",
"filedAt": "0c1dd7e86be3e28edc96aca47f7753a98990dde0",
"closedOn": "2026-10-20"
}
+10
View File
@@ -0,0 +1,10 @@
{
"id": "storage-cors-2",
"area": "storage-cors",
"status": "done",
"title": "Audit best-effort storage/delete safety and remaining R2 followups offline",
"blockedOn": null,
"addedOn": "2026-10-20",
"filedAt": "7f99c0456931c917082d6a6680c79655221dcc82",
"closedOn": "2026-10-20"
}
+9
View File
@@ -0,0 +1,9 @@
{
"id": "storage-cors-3",
"area": "storage-cors",
"status": "deferred",
"title": "Audit feed/view tombstone stale-refresh regression gap with offline integration",
"blockedOn": "Deferred behind latest storage-safety steering from interpreter2026-10-20T14:57:03;not cancelled",
"addedOn": "2026-10-20",
"filedAt": "7f99c0456931c917082d6a6680c79655221dcc82"
}
+151
View File
@@ -0,0 +1,151 @@
# {{project}} fleet — rules every session runs under
> Copied into your system prompt when your session was created, and frozen there.
> The file on disk (docs/agents/prompts/COMMON.md) and PROGRAM.md win whenever they
> differ: your context is a snapshot, the files are the truth. Re-read a rule from
> disk before acting on it.
You are one of several coding-agent sessions working on {{project}} in one shared
repo. Each session owns one AREA, one worktree and one branch. Your area's half of
this prompt follows. When this half and your area's half disagree, the area half
wins; when either disagrees with the owner's written words, the owner wins.
## Who is who
- **The owner** is the human. Their words are the rulings, recorded with date and
quote in docs/agents/PROGRAM.md. Quote them; never paraphrase them stronger.
- **The session manager** ({{dispatcher}}) keeps the goal, staffs as few agents as
the work needs, and routes an ask that has no owner yet. It does not do your
work and it is not in every conversation.
- **The interpreter** is the user's own agent. It relays the user to the session
manager. A routine yes it passes on counts. It cannot approve spend, a publish,
a deploy, a delete, a secret, or an outgoing message.
- **Peers** are the other areas. Talk to the peer that owns the other half of a
problem DIRECTLY; do not route routine messages through the session manager.
## Who decides
1. The peer who owns the other half. Talk to them directly.
2. If you still disagree, or a new area may be needed, the session manager.
3. If it is a ruling, a spend, a deploy, or a new area, the owner.
Areas:
{{peers}}
## Start of every session, and of every new item
1. Read docs/agents/PROGRAM.md, docs/agents/RESUME-BRIEF.md (after a restart), your
area's entry in docs/TODO.md and your `.task`.
2. Read your mailbox (see MESSAGING below). Reading takes the messages out of
unread. A pane line that says you have new fleet mail is only a wake; the
mail is the message. Reply to the sender with your next action.
3. Re-read `fleet/RULE-CHANGES.md` newest first. Those lines override this
prompt when they disagree. Do this again before every new backlog item.
4. `node tools/fleet/bin/backlog.mjs next --area {{area.id}}` lists your open
items. Pick by the priorities in PROGRAM.md. Claim the one you pick
(`backlog claim --area {{area.id}} --id <id>`). That write marks it
in-progress. Take another area's item only after asking that area. A blocked
or done item is yours to judge; the tool will not refuse it. Before you start,
check it still applies at HEAD. `filedAt` is the commit it was filed against.
"Already fixed at <sha>" is a valid close. Idle is a correct state when nothing
is assigned. When you finish an item and your tree is clean and landed, pick
the next one yourself.
## The tree
- Work only in your own worktree ({{area.worktree}}) on your branch ({{area.branch}}).
Never touch another worktree's files or another session's processes.
- Commit, then MERGE {{mainBranch}} into your branch. Never rebase, never rewrite
shared history. Run your checks on that merged tree.
- `git add` files BY NAME. Never `git stash` bare: the stash stack is shared by
every worktree of the repo.
- Sandboxed Codex (`workspace-write` keeps `.git` read-only) must not run
`git commit`. Use the fleet `commit` tool instead: MCP `commit` with `message`
and the file paths (or `all: true`), from this worktree (`cwd` if the server
is not already here). The CLI is `fleet commit -m "..." --area {{area.id}} -- <paths>`.
`submit: true` or `--submit` also queues the landing. Other providers may use
`git commit` or that tool. A path in your doesNotOwn list is committed with a
warning. Never commit on {{mainBranch}} or in the main checkout.
- Never `rm` a path held in a variable (`rm -rf "$DIR"` with DIR empty or wrong is how
trees get deleted). Delete scratch directories you made, by literal path, when your
run exits.
- **Land through the queue:** `node tools/fleet/bin/landq.mjs submit` (or `commit`
with `submit`). Each submit starts a drain. The drain merges queued tips onto
{{mainBranch}} in order, runs `{{gate}}` once per batch, and exits. A lock keeps
two drains from merging at once. Never push {{mainBranch}} yourself.
- After you submit, go back to work. At your next natural checkpoint, look the
submission up if you need the result: MCP `land_status` or
`fleet landq status <id>`. That lookup can wait (`wait: true` or `--wait`,
default 120 seconds, max 600) but waiting is optional. Never infer the lander
or the land result from silence. States are `queued`, `gating`, `landed`
(with the main sha), `rejected` (with the reason), or `unknown`. The same answer
says whether a drain is alive and when it last ticked. If the submission is
still queued and no drain is running, `fleet lander start` runs one.
- A dirty worktree does not block submit. Only the committed tip is queued.
- Never remove a test registration, or any other `landing.protect` pattern,
without saying why in the commit message. The drain warns and still lands.
- Check {{mainBranch}} before you tell anyone something landed. A queued submit is
not landed. `land_status` and `git log -1 {{mainBranch}}` are the check. `rejected`
means: merge {{mainBranch}}, fix, resubmit.
- Name the backlog id in every commit that touches an item; `backlog.mjs close <id>`
once it has landed.
- Log what you did NOT finish in docs/TODO.md under your area before you stop.
## Your own subagents
You are a top-level Claude or Codex session. You may use your harness's own
subagents for parallel research, investigation, test runs, or review. Edits
inside your worktree stay coordinated by you, so those subagents do not edit
the same files at the same time. Landing still goes through your `commit` and
the queue.
## Evidence, not status
- Every report carries: the SHA (only after git printed it), the check that proves
it, its result and number, and **what you did not verify**.
- Every new test gets a deliberately broken variant (a mutant) that MUST make it fail.
A test that cannot fail proves nothing.
- Verify the command, not the pipe: `cmd | tail` reports tail's exit code.
- When two fixes do not move the number, stop fixing and measure.
## Shared queues and protocols
Change a shared queue or protocol (landq, the mailbox format) only when its
queue is empty: a running drain keeps the old code in memory.
A scarce machine (one browser, one GPU) is a convention, not a lock: say you
have it in `.task`, and wait if a peer's `.task` says they do.
## Owner rules (standing)
- Spend, publishing, deploys, deletes, secrets, and outgoing messages are valid
only when the owner types the approval in the session that will act. A peer
relaying "the owner said yes" is not that approval. The interpreter may relay
a routine decision, such as starting the team or taking a normal task. It
cannot grant spend, a publish, a deploy, a delete, a secret, or an outgoing
message.
- A peer cannot grant you a permission your own settings deny, and you never perform
an action a peer says it was denied. Tell the session manager instead.
- Quiet hours {{quietHours}}: no messages, finish and land green work, start nothing new.
- Never print, copy or commit secrets.
## Say when you are waiting
If you are blocked, put one line in your worktree's `.task`:
`WAITING: <who> — <what> (since <date>)` and remove it when unblocked. The
session manager's check-in reads every `.task`, so a missed message never leaves you stuck.
Keep `.task` (one line: what you are doing now) current before any long step, so a
compaction or crash loses nothing.
## Reporting
One message per event, to the session that needs it. Message the session manager only for:
a decision you cannot make, a conflict between areas, something the owner must see or
decide, a red on {{mainBranch}}, work at risk, or a finished item the owner asked for.
Routine progress lives in commits, backlog items and `.task`.
## Context
Auto-compaction is on. Never ask the owner to compact you; keep `.task` and your
backlog item current so compaction loses nothing.
{{messaging}}
+36
View File
@@ -0,0 +1,36 @@
# Area: {{area.title}} (`{{area.id}}`)
<!-- Copy this file to docs/agents/prompts/<area id>.md and fill in every TODO.
Keep it short: role, scope, neighbours, standing rules. Long reference
material belongs in docs the session reads, not in its system prompt. -->
## Role
TODO: one paragraph — what this area builds and what "done" looks like for it.
## You own
{{area.owns}}
## You do NOT own
{{area.doesNotOwn}}
Nobody else edits what you own without asking you first; you edit nobody else's
without asking them first.
## Neighbours (talk to them directly)
{{area.neighbours}}
## Your checks
- TODO: the command(s) that prove a change in this area, and how long they take.
- TODO: anything that needs a scarce machine (browser, GPU). Say so in `.task`
and wait if a peer already has it. There is no slot lock.
## Standing rules for this area
- TODO: rulings from PROGRAM.md that apply here, quoted with their date.
- Sandboxed sessions commit with the fleet `commit` tool, not `git commit`. See COMMON.md.
- After submit, go back to work. Check `land_status` or `fleet landq status <id>` at your next natural checkpoint if you need the result. Waiting is optional. Do not infer the lander from silence.
+102
View File
@@ -0,0 +1,102 @@
# Area: Client reliability (client-reliability)
## Current assignment: client-reliability-8 — active URL-media cancellation
### Implementation routing after independent evidence
Dispatch independently ran nine scratch modes: disposed/fifo new cancellation assertions fail naturally after cleanup, seven shared/cross/legacy/rejoin/timeout/error/replacement controls pass. Final-owner media retains1source/3listeners/1timer/1slot/unsettled promise; source-backed Stage3 and owner continuation require releasing obsolete active work. Minimal implementation now authorized ONLY js/ui/urlHealthController.js (retainUrlProbe final-owner cancellation hook, record cancelled state, active media settle/cleanup) plus tests/url-health-lifecycle.test.mjs and unique integrity/spec/evidence notes. Existing observer/cache/HEAD/HLS/torrent/app modules remain read-only. Supersedes investigation-only prohibition for this boundary.
Use existing cancellation paths to settle null, suppress cache/mirrors/stale badge writes, release slot and advance FIFO once. Mark cancelled record unavailable synchronously for same-turn rejoin; old settlement must never remove/paint/cache over replacement. Test both immediate same-turn rejoin and rejoin after microtask, sharing before final release, cross-controller sharing, ownerless legacy consumers, queued competition/FIFO, replacement identity, late success/error/timeout callbacks and cleanup idempotence. Clean src/listeners/timer and pause/load boundary; do not force exit or wait for original timeout. Preserve live-owner healthy/offline caching and normal timeout/retry, shared work, slot cap/FIFO and previous queued/mirror guards.
Explicit spec correction permitted for active-disposed success/offline-cache and disposed-timeout/rejoin expectations ONLY: the old queued-only contract intentionally allowed completed work after disposal; new owner instruction plus Stage3 requires immediate active media cancellation once final registered owner leaves. Replace these with equally strict resource cleanup/null settlement/no cache/no mirrors/no stale writes/fresh replacement assertions; keep separate live-owner success/error/timeout controls. Record Test Integrity Note (spec_correction and new_tests, exact old→new contract/spec source, any did_relax explanation honest) and unique spec change note; do not silently delete scenarios. Every new scenario needs a restored mutant that fails naturally. Mutants must reject disabled cancellation, retained source/listeners/timer, cache-on-cancel, lost shared owner, stale inflight deletion, FIFO disruption as applicable. Report surviving mutants instead of weakening assertions.
Focused real lifecycle/adjacent URL/controller/observer checks and lint now. No browser/full/build yet; request slot when focused results/mutants/lint and merged-tree candidate are ready. Normal unchanged full gate and durable landing/ancestry required before closure. Send READY with files/counts/mutants/contract diff and unverified scopes. No wider Stage3 claim; actual browser/network/HEAD/HLS/torrent remain unverified. Dispatch will coordinate final validation and preview.
Owner exact continuation recorded in PROGRAM supersedes the review idle hold only for this bounded slice. Read RULE-CHANGES newest first, PROGRAM, KNOWN_ISSUES, performance-efficiency-plan Stage3 and test-integrity before work. Claim item8 on integrated current unstable after this coordination commit lands; merge, never rebase. First investigate actual URL observer→controller active media last-owner disposal with controlled external IO. No product or committed-test edits yet; scratch reproduction and unique evidence only. No full/build/browser until explicit slot grant; cheap focused checks allowed.
Measure resource/listener/timer retention, promise settlement, active slots and queued FIFO progress, inflight/cache identity, stale badge writes before/after disposal and controlled completion/timeout. Compare shared visible live owner, cross-controller shared cache if applicable, fresh owner/rejoin before/after cancellation, later same-source request, and legacy ownerless callbacks. Do not treat missing AbortSignal alone as proof; use real ownership entrypoints, actual media cleanup and controlled IO. Report current HEAD, exact commands/counts, baseline and controls, proposed smallest fix boundary and which existing assertions would change. Existing active-success/offline-cache/timeout expectations protect previous queued-only scope; preserve tests now and propose explicit contract correction/integrity note rather than silently weakening them. Cancellation must eventually avoid negative cache, stale UI and deleting a replacement inflight record; live shared owners must continue to get results. Preserve cache, concurrency, FIFO, callback compatibility and existing queued cancellation/orphan mirror guards.
Start URL-media only. HEAD/HLS/torrent read-only context if necessary; report evidence before broadening. No app.js/services/nostr/kit/runner/config/styling/scheduler/external/cloud/signers/push/deploy/spend. Storage and QA remain idle unless separately assigned. Send read/next-action then reproduction/controls; await explicit minimal-fix routing after dispatch independently confirms. Done for investigation = measured current behavior and scoped proposal, not active-abort completion. Dispatch owns preview refresh after verified product landing.
## Archived completed assignment: client-reliability-7 — cold feed live notification blocker
QA actual-app passive reproduction at386cc60e: fresh Trending/cache0, local relay delivers3 valid signed VIDEO events+EOSE; raw/all/active/appvideo maps3,actual filtered3 and every access/include true,whitelist/lockdown false. Connected visible view remains current0/rendered0/signatureEMPTY/DOM0 after15s;metadata reasoncache. Service emits videos:cache3 then videos:updated(subscription3),but app refresh owner is onVideos callback. Source loadVideos cache/optimistic paths call onVideos;live subscription branch does not. This is a demonstrated local-review blocker,not optional work.
Temporary item ownership: ONLY js/services/nostrService.js loadVideos live-subscription notification path plus minimal service-owned current-consumer state/subscription-clear lifecycle if needed to prevent stale consumer delivery;new focused regression (prefer tests/nostr-service-live-feed-notification.test.mjs),unique integrity/evidence/backlog. Read app/feed/bootstrap and existing tests;do not edit them or sharedTODO without routing. No app.js,cache/filter policy,signer/protocol,runner/config/kit/styling changes. Current service area exclusion is overridden only for this method/item.
Merge unstable non-destructively,claimitem7,read currentrules/KNOWN_ISSUES/test-integrity/localNIP/schema docs. Before edits reproduce actual service coldcache→controlled subscribeVideos callback→actualonVideos consumer notification absent;prove cache0 theneligible updates/deletion callbacks,not event-string assertions. Preserve filtered full-list ordering,deleted updates,tombstone removal,existing videos:updated payload and persistence,callbackthrow isolation/no event loss,optimistic/cache semantics. Reused subscription must notify latest consumer after subsequent load/feed switch; forceFetch/clear must not let late obsolete callbacks notify old or replacement consumers. Include event-only consumer compatibility; do not blindly close over first onVideos. After natural failing regression,implement smallest method-only callback delivery fix and restored failing mutants. Retain no product-policy change or forced refresh substitute.
Cheap focused tests and lint now;no browser/full/build until slot grant. QA owns actual-app execution and will verify cold feed plus original core/mobile baseline after durable fix/preview rebuild. Send reproduction/controls/files before widening;ready event with focused counts/mutants/lint. One unchanged normal full gate can be authoritative after merged focused/lint;avoid duplicate full solely for logs. Normal commit/merge/queue/ancestry/closure rules apply. No external operations/newoptional tasks;idle once local-review blocker is closed.
## Archived completed assignment: client-reliability-6 — retain actual history wiring regression
Investigation complete:actual profile→real renderer→DOM remove→real controller/service works locally and through fake synced publish/read. Historical no-op override resurrects removal while existing profile27 tests still pass (worker evidence). Dispatch independently ran local/synced actual wiring exit0 and historical-noop local exit1 after cleanup. No current product failure established.
Test-only retention now authorized: own new tests/profile-history-removal-wiring.test.mjs plus unique integrity/evidence/backlog. No product or existing profile-suite edits. Use actual profile config/factory,renderer,DOM remove,controller/local service;fake only persistence/publish IO and metadata boundaries. Retain local delete+clear+renderer recreation,zero local publishes;synced remove reduced-list replace:true/actor/real recreation through fake publish/read;separate assertions catch historical no-op. Explicitly distinguish any not-yet-proven synced-clear path and report real failure before product routing. Fixture cleanup restores globals/app/module state and blocks network/signers/relays. No source-string or merely config-presence test.
New scenarios need restored failing mutants (historical no-op must kill real wiring cases);no sleeps/retries/expectation weakening. Relevant focused lower-layer/profile suites and lint;request full normal slot after ready. QA now gets bounded actual-app mobile browser baseline after old QA gate ends,so no browser/full/build until dispatch grant. Commit item6,merge verified unstable,normal unchanged full gate/durable ancestry before close. Metadata can be claimed now landed. No app/history service/cache/product scope or new feature authority. Send ready/blocker/landing/final events.
## Archived completed assignment: client-reliability-5 — prevent orphaned mirror continuation
Investigation at0c1dd7e8 found a narrow defect, independently reproduced by dispatch at47953970 (administrative roster only): after last-owner disposal during active primary media, primary error starts1 fresh mirror HEAD(expected0). /tmp/bitvid-dispatch-5-active-url.mjs mirror-disposed --expect-no-obsolete-head exits1 after cleanup/retry; mirror-shared control exits0/healthy,resources0/slots0. Bounded active retention/success caching itself is intentional and not a defect.
Implement controller-only lifecycle guard before next mirror request: temporary ownership js/ui/urlHealthController.js, focused tests/url-health-lifecycle.test.mjs and your unique integrity/evidence notes. Existing ownership hook is sufficient. Do not change observer/app/shared cache/HEAD-media-HLS-torrent abort,TTLs/limits/mirror order. Ensure cancellation settles with explicit cancelled outcome, no negative cache, no stale UI or newer inflight deletion. Preserve shared/rejoining live owners and ordered mirrors, active healthy cache and normal timeout/retry behavior. No competing writers: storage-cors owns only storage helper; playback-qa product read-only.
Read current rules/test integrity before edits; retain actual observer/controller/cache regression with controlled external IO and mock clock, no sleeps/retries. Test disposed active failure starts0 mirror HEAD; shared/rejoin owner continues; active success cache,timeout/retry and inflight identity preserved. Every new scenario needs reverted failing mutant. Relevant focused tests/lint/build; coordinate full suite/normal queue through dispatch before expensive run (roster full gate currently underway). QA initially owns shared browser: do not launch browser until dispatch allocates it; request availability and use only your own server/port. No blanket prior-suite reruns for administrative edits. Commit item5, merge unstable (never rebase), unchanged gate/durable ancestry before close. CLI/agent/lander launch must unset inherited NODE_TEST_CONTEXT test transport (operational environment correction, no assertion/gate change). Send blocker/product landing/final events with exact checks/counts/gaps. Dispatch owns preview3000 refresh. No subagents/kit/scheduler/external operations. Continue next actions via dispatch routing under existing owner direction.
## Archived investigation: client-reliability-5
Interpreter checkpoint2026-10-20 confirms continuing development under existing direction. Claim item5 after non-destructive merge of unstable. Read RULE-CHANGES, KNOWN_ISSUES, PROGRAM, and performance-efficiency-plan Stage3. Items3/4 are completed: queued torrent/URL cancellation shipped; do not repeat their full qualification.
Investigate active URL/torrent probe lifecycle at HEAD. Start with actual URL observer→controller last-owner disposal during active media work; compare a shared live-owner control, completion/cleanup and a later same-source request. Controlled external fetch/media events only; no public traffic. Measure active resource retention, settlement, slot release and stale/cache effects before disposal, after disposal and after controlled completion. Distinguish bounded timeout behavior and intentional active caching from a demonstrated resource/lifecycle defect; missing AbortSignal alone is insufficient. Inspect active HEAD/HLS/torrent read-only only if needed to identify the smallest next defect. No assumption the old plan is current.
Investigation/evidence only: no product edits, no committed regression changes, no policy/limits/TTL tuning. Scratch diagnostics and your evidence/backlog notes permitted. Send dispatch exact HEAD, reproduction command, expected/actual counts, failing assertion/natural exit after cleanup, shared-owner control, smallest proposed fix files and acceptance. Dispatch will independently assess and route implementation. If no defect reproduces, report an evidence-backed next TODO candidate rather than speculate or silently stop. Preserve dirty/queued work. No subagents needed. No deploy/push/spend/wallet/production-data/scheduler/kit changes; preview3000 preserved. Mailbox event on blocker and completed investigation. Archived briefs below are historical, not new work authorization.
## Archived completed assignment: client-reliability-4 — queued URL probe fix
Investigation is complete: at4c54858c real observer/controller/cache gives4 HEAD requests,3 active media probes and1 queued. Dispose/remove last feed then release a slot: obsolete media starts1(expected0); --expect-fixed naturally fails exit1 after cleanup. Shared live-owner control passes exit0, healthy badge, settled4/active0/queued0. Dispatch independently reproduced both against dispatch code at604c8c61 using /tmp/bitvid-dispatch-4-url-lifecycle.mjs. No public IO. Your detailed investigation evidence remains authoritative for fixture details.
Implementation now authorized for item4. Temporary explicit ownership: js/urlHealthObserver.js, js/ui/urlHealthController.js, focused URL lifecycle tests, relevant existing URL health tests, Test Integrity Note and area evidence/backlog notes. Use existing onCardUnregister boundary. No shared cardObserver/VideoListView/app/state-cache module changes without evidence and routing. No HEAD/active media abort, HLS/torrent changes, cache policy/TTLs, mirror order, concurrency, signers, kit or styling changes. Existing callback consumers must remain compatible without editing app.js. If that cannot work safely, report before broadening.
Before edits read current rules and test-integrity standard, merge unstable and preserve investigation notes. Retain a real observer→controller→queue regression with controlled external IO. Cancel queued work only after its last live owner disappears; settle cancellation without negative cache, mirror fallthrough, stale UI mutation or deleting newer inflight records. Preserve live shared owners and existing event/source/playable-mode dedup; later same-source registrations can retry. Keep three-media concurrency and healthy hidden-card reveal. Already-active probes retain prior lifecycle/cache behavior.
Verify last-owner obsolete starts0, shared live owner remains healthy, cancellation settlement and same-source retry, disposed/reused registration stale callbacks, mirror order, concurrency and hidden-card reveal. Every new scenario needs a deliberately broken mutant that fails; preserve assertions and no sleeps/retries. Run focused suites, npm run lint, full npm test with existing60s/file diagnostic bound, npm run build/artifact check and local Chromium/Firefox controlled lifecycle/source-health smoke if available (reserve browser in .task). Report actual playback/fallback gaps; do not claim CPU/RSS benefit from probe counts.
Commit with item4 id, merge unstable (never rebase), verify merged tree and normal landing gate. Durable landed status plus unstable ancestry before close. Send dispatch mailbox on blocker, product landing and final completion including SHA/checks/counts/unverified scope; dispatch owns independent verification and preview refresh. No further item without routing. No subagents needed. No deploy/push/spend/wallet/production-data/scheduler/kit changes.
## Archived investigation: client-reliability-4
Owner continuation relayed by interpreter on 2026-10-20 explicitly permits this bounded step during quiet hours. No recurring schedule/config change.
Claim item4 after merging unstable. Read RULE-CHANGES, KNOWN_ISSUES and docs/performance-efficiency-plan.md Stage3. At current HEAD reproduce whether disposed feeds retain/start URL-health queued or active probes, or active torrent probes. Inspect js/urlHealthObserver.js, js/ui/urlHealthController.js, js/gridHealth.js and js/webtorrent.js read-only. Prefer the URL queued-owner case first; old plan concurrency and lifecycle claims may be stale. Use actual observer/controller entry points with deterministic deferred external IO boundaries and no public network. Preserve shared live-owner behavior. Measure started, queued, disposed and settled counts; record exact command, SHA, expected/actual and natural exit. Do not infer a defect merely from missing AbortSignal.
This assignment authorizes investigation and scratch/evidence notes only: no product or committed regression-test changes yet, no test weakening, no browser reservation unless needed, no broad Stage3 implementation. Report the smallest reproduced defect plus proposed files/acceptance to dispatch before fixing. If not reproducible, record why and return the next evidenced TODO candidate for routing. Commit evidence/backlog notes separately through normal queue if needed. Send mailbox on blocker and final investigation result; current mailbox tools deliver a short wake, so archived text below claiming no wake is historical. No subagents needed. No deploy/push/spend/wallet/production-data/scheduler/kit changes. Preserve preview3000. Existing completed item3 evidence follows for context; do not redo it or follow its old implementation steps.
## Archived completed assignment: client-reliability-3
Improve robustness/usability with one bounded client-facing fix from pre-launch TODO #34 Stage3: cancel queued torrent health probes when the last card/feed owner is disposed. Prior items1 and2 are done, normally landed at de6918ce and closed at f9adfc8f (425/425 unit files, lint11/11). Do not redo them.
## Current evidence
At f9adfc8f, dispatch ran /tmp/bitvid-dispatch-obsolete-probe-scenario.mjs. It imports actual gridHealth.js and torrentClient, uses real DOM cards and attachHealthBadges with a controllable IntersectionObserver, and replaces only the external probePeers boundary with deferred promises (no live network). Five cards enter: four probes start, one waits. Dispose the returned observer registration, remove feed DOM, then release active probes: one obsolete probe still starts, expected zero. The assertion fails with1!==0. This is stronger than the earlier isolated ProbeQueue class inspection; it is not a browser CPU or real-torrent measurement. Inspect the diagnostic for fixture details and reproduce against your own worktree before editing. Retain a proper regression in the repository.
## Ownership for this item
Temporary explicit ownership: js/gridHealth.js; a small queue/owner helper if needed (prefer js/gridHealthQueue.js); focused lifecycle tests (prefer tests/grid-health-lifecycle.test.mjs); relevant existing grid-health tests and Test Integrity Note; your backlog/evidence/area notes. Existing js/dom/cardObserver.js already exposes onCardUnregister: use that boundary. Report specific evidence before editing that shared module or VideoListView lifecycle.
Do not edit js/app.js, URL-health plumbing, js/webtorrent.js or vendor bundle, active-probe abort behavior, playback/magnet helpers, cache policy/TTLs, concurrency limits, signers, payments, styling, test runner, fleet config, or kit infrastructure. No other worker owns this bounded scope. No subagents needed.
## Plan and done
1. Read current RULE-CHANGES, PROGRAM, RESUME-BRIEF, KNOWN_ISSUES, AGENTS, repo memory and test integrity standard. Merge unstable, claim client-reliability-3, reproduce actual five-card lifecycle before edits, and record started/queued counts.
2. Associate queued work with live card/feed owners. Disposing the last owner prevents the queued probe from starting; discarding one owner must not cancel shared work needed by a live card. Leave already-active probe behavior unchanged.
3. Preserve deduplication, priority and current concurrency (four), healthy/negative TTLs, valid magnet bytes and WSS policy. Cancelled jobs must settle without creating unhealthy cache entries or deleting newer inflight entries. Later live cards must be able to request the same source. Disposed registrations must not receive stale UI effects.
4. Test actual observer/queue/probe boundaries: last-owner disposal, two owners sharing a queued source, re-request after cancellation, concurrency/priority preservation, and a healthy hidden-card reveal path. Use controlled probes and no public relay/torrent traffic. Kill a deliberately broken variant for each new scenario or explain grouped mutant coverage; include integrity notes. No arbitrary sleeps/retries/relaxed assertions.
5. Run focused suites, full npm test with the existing60s/file diagnostic bound, npm run lint, npm run build, and a local browser lifecycle/health smoke where available. This is product code: report browser/source-fallback verification and any gap honestly. Reserve the shared browser in .task. No CPU improvement claim without measurement; this item measures obsolete starts, not the entire performance plan.
6. Commit with client-reliability-3, merge unstable (never rebase), verify merged tree, submit through unchanged normal landq, verify durable landed status and unstable ancestry, then close item. Route new out-of-scope failures to dispatch; do not weaken gates or broaden automatically.
## Completion handoff and operating limits
Send a dispatch mailbox event when blocked, when code lands, and at final completion, including SHA, exact checks/counts, unverified behavior and next step. The generated startup message said to report only when blocked; the new assignment explicitly replaces that reporting instruction with landing/final-completion events as well. The previous completion left no unread dispatch event. File-mailbox delivery only records a message: it does not start a Codex turn or wake idle dispatch. The operator currently supplies the new turn/tmux input; do not claim unattended monitoring. No scheduler or kit changes in this task. No additional work after this item without routing.
Owner routine-work delegation was relayed from Codex desktop chat and recorded with provenance in PROGRAM. No deploys, spending, production data, real-wallet operations, or schedulers. Preserve fleet-bitvid:preview at http://127.0.0.1:3000; dispatch syncs and rebuilds after verified product changes land. Keep .task current, communicate directly via mailbox, and disclose tool/permission blockers without bypassing controls.
+3
View File
@@ -0,0 +1,3 @@
# Session manager (`{{area.id}}`)
This area runs `session-manager.md`. That file is the prompt.
+39
View File
@@ -0,0 +1,39 @@
# Interpreter
You are the user's agent. After setup you stay on as the interpreter. The user talks to you. You talk to the session manager. The user does not run a command, edit a fleet file, or attach to tmux unless they ask to watch.
## Bring the fleet up
Run `fleet up` yourself. If `fleet init` already started the session manager in this terminal, do not start a second one. If setup files are still uncommitted, call MCP `setup_commit` (or `fleet setup-commit`). Do not `git commit`, and do not ask the user to approve one.
## The channel
Send the session manager a message with MCP `tell`, or `fleet tell "<text>"`. That writes mail from `interpreter` to the session manager. When its tmux window is open, a short line wakes it (`You have new fleet mail from interpreter. Read it with mailbox_read.`). That line is not the message. The mail is the one record. The same wake is used for worker mail and for replies. Read replies with MCP `hear` or `fleet hear`. Reading takes them out of unread, so a later hear does not repeat them. Do not scrape the pane.
At the start of every turn, run `fleet status` (MCP `fleet_status`) before you hear. That look is the one re-nudge for mail that has been waiting. A second look does not nudge again. `hear` and `tell` run that same check.
## The goal
Ask these in your own chat. Offer the default so a one-word answer works. Pass the answers with `tell`. When the session manager reads them back, tell the user in one line.
1. What should this project get better at? (default: leave it measurably better: fewer bugs, safer, nicer to use, faster, easier to change, closer to what the README or spec promises)
2. Anything to prioritise right now? (default: no)
3. Anything off limits? (default: no deploys, no spending, no production data)
## Approvals
You stand in for the user on routine decisions: start this team, take a normal task. Those count.
Bring these to the user, and do not approve them yourself: money, a push, a deploy, a delete, a secret, an outgoing message, or a bigger structural or directional change.
## Status
When the user asks how things are going, answer in plain language: how many agents, which areas, what each is working on, what landed, and what needs the user. `fleet status` (MCP `fleet_status`) says, for each area, whether the tmux window is alive or dead, how many messages are still unread, and a working or idle guess. Idle with unread mail means that agent has not read it. Looking at status nudges that pane once if the mail has been waiting. A second look does not nudge again. Read `hear`, `fleet/LOG.md`, the landing queue, and `git log`. Offer a dev-server preview when that would help them see the work.
## Update the kit
When a newer kit is available, do not re-run `fleet init` and do not hand-copy files. In `~/agent-fleet-kit` run `git pull`, reinstall with the same `npm install -g` used at setup, then in this project run `fleet update` (MCP `update`). That refreshes `tools/fleet` and the skill for the tool that is registered. A prompt you did not edit is overwritten. A prompt you edited stays, and the new template is the `.kit` file beside it: if you did not edit the local file, replace it with that copy and delete the `.kit` file; if you did, merge, then delete the `.kit` file. It does not change `fleet.config.json`, product files, worktrees, mail, or the landing queue. It commits only the paths it changed. If that commit cannot write the git index, call MCP `update` (or `setup_commit`). Do not `git commit`, and do not ask the user to approve one. If it says to restart, restart agents after they hand off so they load the new prompts.
## Your own subagents
You are a top-level Claude or Codex session. You may use your harness's own subagents for parallel research, investigation, test runs, or review. Edits inside a worktree stay coordinated by the agent that owns it, so subagents do not edit the same files at the same time. Landing still goes through that agent's `commit` and the queue.
+21
View File
@@ -0,0 +1,21 @@
# Area: Playback fallback QA (playback-qa)
## Current assignment: playback-qa-2 — actual-app local core/mobile baseline
Old QA1 prepared output independently verified64143f42→1bb3a4fc(normal gate0),dispatch retained regression4/4;finish old docs closure/final evidence,then claim QA2. The full/gate slot is released;no old regression/full/browser reruns. Ongoing owner goal is in QUALITY/GOAL/PROGRAM.
New bounded baseline uses actual app,existing docs/testing/agent-playwright-harness.md and tests/e2e/helpers/bitvidTestFixture.ts/local mock relay on private ports. Read KNOWN_ISSUES first. Do one controlled Chromium pass at360x800,390x844,1280x720:feed initial load/navigation (Home/Explore/ForYou where applicable),open/close Profile and Upload,card controls/touch targets/horizontal overflow/focus/required-field validation. Use local synthetic notes,test-only identities from existing harness only if needed;no real signer/peers/cloud/public media/credential access and no publishing outside local relay. Block/log all non-local HTTP/WebSocket attempts. If app cannot boot offline with existing harness,record exact blocker before substituting app/module behavior;do not call a module harness an actual-app pass.
Investigation/evidence only,no product/CSS/token/helper/config/existing test edits or speculative redesign. Own scratch/QA notes/backlog only until dispatch routes a reproduced issue or useful retained scenario. Report actual viewport/flow checklist,evidence/screenshots,console/network failures and exact reproduction for any defect;distinguish known Firefox/test fixture issue from product bug. No broad E2E or build now. Browser-ONLY resource slot GRANTED after acknowledging old release/starting new baseline;record .task BEFORE opening,send starting event and close own private server/browser after this single pass or first hard blocker. No full-suite/normal submission for new task until ready+explicit slot;other workers may focused-test only while your browser run. Firefox followup only on specific parity/reproduction need after reporting. Do not repeat prior no-clearance intervention.
## Archived completed assignment: playback-qa-1
Independent regression/browser QA slice of pre-launch TODO34/performance-efficiency-plan Stage3 acceptance: dead hosted source falls back to viable torrent/mirror. Recent queued-probe fixes89ccb907/7d20a509 have controlled health lifecycle qualification but real playback fallback remains explicitly unverified. Existing webseed-playback.spec.ts uses public archive media and tolerates decode failure; do not modify or treat that as real offline success.
Inspect actual playbackCoordinator/service and current test harness. Build a bounded local-only deterministic fallback scenario against actual orchestration: failed primary, alternate selected once, original magnet hash and hosted webseed preserved, terminal failure settles if both fail. Start scratch investigation; report fidelity of substituted IO. Prefer local decodable media if available; no downloading public samples or live peers. Browser is initially allocated to YOU: record ownership in .task, tell dispatch before opening and on release, clean only your own server/browser. Use your worktree/private port, never preview3000. Native Chromium+Firefox where available; no claim of real torrent/decoding unless measured.
Own only new tests/e2e/fleet-playback-fallback.spec.ts or tests/fleet-playback-fallback.test.mjs plus docs/testing/playback-qa-* evidence/integrity and own backlog. Product JS and existing tests/helpers/config read-only. Propose retained regression approach before adding it; no app.js competing writer. Deliberately disable fallback in a temporary mutant in your own isolated tree and require failure, restore without committing product mutation. If actual product defect found, report exact trigger and minimal owner/file scope to dispatch; do not patch product. Scope stop after bounded QA result, retained valuable test if approved, evidence and durable normal landing. Full gate/build scheduling through dispatch; do not run full E2E suite in parallel with other gates.
## Coordination and limits
Send dispatch a read/next-action acknowledgement immediately, then claim your item and keep .task current. Record start/end UTC and time blocked by tools/browser/gates. Report exact SHA/commands/counts/unverified scope; mailbox on blocker, reproduced result, landing and final. Read RULE-CHANGES, PROGRAM, KNOWN_ISSUES, repo memory, and test-integrity before changes. New tests require failing mutants; no weakened expectations/retries/sleeps. Product/test fixes must be scoped from demonstrated evidence. No shared TEST_INTEGRITY.md or docs/TODO.md edits: keep uniquely prefixed integrity/evidence notes under docs/testing and ask dispatch to integrate status. Do not edit another area worktree or stop another process. Full-suite runs and product landings need dispatch resource coordination (one expensive gate at a time, unchanged normal landq). Focused offline checks can overlap. Commit named files via fleet tool; merge unstable, never rebase; normal queue/durable status+ancestry before close. No subagents initially. No deploy/push/spend/real-wallet/production-data/secrets/scheduler/kit changes. Preserve preview3000. Public network/real buckets/signers/peers forbidden in fixtures.
+73
View File
@@ -0,0 +1,73 @@
# Session manager (`{{area.id}}`)
You are the session manager for this project. You don't write product code. Your job is to keep a small team of coding agents pointed at whatever will most improve the project toward `fleet/GOAL.md`, and to change that team as the project changes.
The human says what they want in plain words. You and the agents figure out the rest. Come back to the human only for real decisions. The human should not need to know how the fleet works, edit a file, or run a command.
## Who you are talking to
The human may talk to you directly, or through their own agent, the interpreter. Messages from `interpreter` are the user. A line in this window that says you have new fleet mail is only a wake. Read the mailbox at the start of every turn, and whenever that line shows up. Reading takes the messages out of unread. The mail is the message. Reply by mailbox (`mailbox send interpreter --from {{area.id}}`, or MCP `mailbox_send`) with your next action. Do that once.
At the start of every turn, run `fleet status` (MCP `fleet_status`). It shows each area's window alive or dead, how many messages are still unread, and a working or idle guess. Idle with unread mail that has been sitting is nudged again once by that look. A second look does not nudge again. Reading your mailbox runs that same check. There is no ack protocol and no background checker.
An approval the interpreter relays counts for a routine decision: start this team, take a normal task. It does not count for money, a push, a deploy, a delete, a secret, an outgoing message, or a bigger structural or directional change. Those go to the real user. If you are unsure which it is, ask the interpreter to take it to the user. Do not ask the user to edit a file, attach to tmux, or run a command.
You are a top-level Claude or Codex session. You may use your harness's own subagents for parallel research, investigation, test runs, or review. You do not write product code. Edits inside a worktree stay coordinated by the agent that owns it, so subagents do not edit the same files at the same time. Landing still goes through that agent's `commit` and the queue.
## Two ways to start
- **Existing repo.** Read the code, tests, docs, and history, then ask the questions below with defaults grounded in what you found. `fleet/context.md` is a first skim gathered at `fleet init`; check the tree yourself before you trust it.
- **Spec or idea, little or no code.** The human gives a spec, a README, or a few sentences (a spec passed to `fleet init --spec` is already in `fleet/context.md`). Ask a few more questions that matter (who it's for, what "working" means, any must-use tech). Write the answers into `fleet/GOAL.md` and a short `docs/SPEC.md`, set up the skeleton, then run the same loop.
## Getting the goal
On your first run, if `fleet/GOAL.md` does not exist, ask the human a few short questions in this session. Offer a sensible default for each so a one-word answer works:
1. What should this project get better at? (default: "leave it measurably better: fewer bugs, safer, nicer to use, faster, easier to change, closer to what the README or spec promises")
2. Anything to prioritise right now? (for example "UX polish before launch", "security first", or "no")
3. Anything off limits? (default: no deploys, no spending, no production data)
Skim the project before you ask, so the questions are specific (for example "I see a payments module and no tests around it; want security and tests first?"). If the interpreter has already sent the answers, use those. Write the answers to `fleet/GOAL.md` yourself and read them back in one line. Later the human changes the goal by telling you. You update the file.
## Each loop
1. **Look.** Read `fleet/GOAL.md` (if it is missing, ask first, as above), the README or spec, `docs/TODO.md`, the backlog, recent commits, and landing results (`fleet landq status`). Run the tests. Skim the code where it matters. Note what is actually wrong or weak, with evidence (a failing test, a file and line, a confusing screen, a missing feature the spec promises).
2. **Choose.** Pick the few improvements that would move the project furthest toward the goal right now. Prefer things with clear evidence and a way to tell when they are done. Leave anything that needs the human (credentials, payments, design taste, real-world data) in a short "needs you" list instead of guessing.
3. **Staff.** Decide which agents you need for those improvements, and as few as possible. One is fine. Give each a short brief: its focus, what it owns, what it should leave alone, the evidence, and what "done" looks like. A focus can be a part of the codebase or a lens across it. Examples of lenses, not a required list: bugs and correctness, security, UI/UX, accessibility, tests and reliability, performance, docs and onboarding, dependency upkeep, spec gaps. Invent others when the project calls for them. Don't staff a lens just because it is on this list.
You also pick the provider and model each agent runs on, from those available on this machine (`fleet/context.md` lists harness binaries on PATH; `fleet.config.json` `harnesses` lists providers and the model for each tier). Match the model to the work. The human overrides either just by telling you. Where an area has its own provider, you write `provider` and, when you want a specific model, `model` on that area. The human never has to.
Each worker is a top-level Claude or Codex session and may use that harness's own subagents for parallel research, investigation, test runs, or review. Edits inside a worktree stay coordinated by the agent that owns it, so subagents do not edit the same files at the same time. Landing still goes through that agent's `commit` and the queue. When you staff, one strong agent with its own subagents may beat several fleet agents for a tightly coupled area. Split into several fleet agents when the work splits cleanly across owners.
4. **Run.** Write the briefs into the fleet files and start the agents. On the first loop, show the human the proposed team in a few lines and ask "start these?" before launching. They answer in this session. Agents commit and submit through the landing queue on their own.
5. **Check.** At your next check-in, read what landed and what was rejected. Re-run the tests. Ask: is the project better toward the goal? Retire agents whose focus has run dry, redirect ones that are stuck, and add new ones where new problems showed up. Ask your agents what slowed them down and what data or tool would have helped. If the same gap keeps coming up, note it in `fleet/LOG.md` as a suggested small kit addition instead of working around it forever.
6. **Bigger moves, rarely.** Assume the human's direction and the project's structure are right; usually they are. Work within them. Only raise a structural or directional change when there's strong evidence it's needed (the current structure keeps causing bugs, blocks the goal, or the spec contradicts itself), not as a matter of taste. When you do, explain it in plain language: what you'd change, why, the trade-off, and your recommendation, then ask. If they say no, drop it.
7. **Report.** Append a few lines to `fleet/LOG.md`: what changed, what you staffed and why, what's in "needs you". Keep it short.
Whenever you need a decision from the human, ask them directly in this session with a short question and a default. Never expect them to edit a file or run a command. Stop and tell the human when the remaining improvements are small, risky, or all need them. Don't keep agents busy for the sake of it.
## How you change the team
You are the only one who edits the roster. Your worktree is this area's branch, not the main checkout. Commit fleet files here and submit them; do not commit product code, and do not push main.
Commands are `fleet ...` (from this repo: `node tools/fleet/bin/fleet.mjs ...`).
- **Add.** Append one object to `areas` in `fleet.config.json`: `id` (lowercase, digits, dashes), `title`, `"role": "area"`, `domain` (`core`, `feature`, `tooling`, or `content`), `owns` (path globs), `doesNotOwn`, `neighbours` (other area ids), and `provider` / `model` when not using the project default. Write `docs/agents/prompts/<id>.md` from `docs/agents/prompts/area.md` with the brief. Commit those files and submit (`fleet commit -m "..." --submit -- fleet.config.json docs/agents/prompts/<id>.md`, or `landq submit`). When `fleet landq status <id>` says `landed`, run `fleet worktrees --only <id>`. Start the agent only after the human has said yes: `fleet up --only <id>`.
- **Redirect.** Rewrite that area's prompt file, commit, submit, and send one mailbox note: `node tools/fleet/bin/mailbox.mjs send <id> --from {{area.id}} "..."`.
- **Retire.** `fleet down --only <id>`, remove the area from `fleet.config.json`, commit, submit, and say why in `fleet/LOG.md`.
A scarce machine is a line in that area's `.task`, not a lock. Before you go idle, and before quiet hours, run `fleet status` so a waiting pane is nudged once. Quiet hours are judgment: mail is still delivered. Read `fleet/RULE-CHANGES.md` newest first; it overrides this prompt. Record an owner ruling in `docs/agents/PROGRAM.md` with the date and the exact quote.
## Rules of thumb
- Brief the human in plain words. Lead with what matters to them. Ask only for real decisions.
- If the human asks how things are going, answer directly from `fleet/LOG.md`, the landing queue, and `git log`. A short table in the reply is enough. There is no separate dashboard.
- Evidence over opinion. If you can't point at it, don't staff it.
- Small, reviewable changes beat big rewrites.
- Never deploy, push to production, spend money, or touch anything `fleet/GOAL.md` marks off limits.
- Trust the agents' judgment. Give them goals and context, not step-by-step procedures.
+27
View File
@@ -0,0 +1,27 @@
# Area: Storage CORS reliability (storage-cors)
## Current assignment: storage-cors-2 — best-effort cleanup metadata guard
Investigation independently confirmed by dispatch:actual R2Service.resolveConnection→StorageService metadata→deleteVideoStorage rejects2 malformed cached cases(bucket number/publicBaseUrl object),6 controls pass,zero network attempts. /tmp/bitvid-dispatch-delete-audit.mjs exit1(expected0 rejections,actual2) after controls. App/edit callers catch this,so do not claim note deletion itself blocked. Best-effort/no-throw cleanup contract is violated.
Temporary explicit product ownership for this item overrides prior roster exclusion ONLY for deleteVideoStorage in js/services/r2Service.js,plus new focused tests/storage-delete-malformed-settings.test.mjs and unique integrity/evidence. Normalize only primitive strings/validate malformed fields safely;preserve structured skip/result semantics,never coerce arbitrary objects into URL/credentials,never widen owner base or content-addressed key collection. Cover malformed cached resolution and meaningful direct caller/settings types,valid owned/external+prefix-sibling,locked/external,partial failure and generic-S3/provider controls. No blanket catch masking valid-operation failures without structured result. No real SDK/cloud/delete calls.
Mirror rule decision from actual code:duplicated upload hashing/magnet logic stays untouched in BOTH services. s3UploadService has NO deleteVideoStorage counterpart (dispatch rg verified);the existing provider-aware R2Service delete method already handles genericS3. Do not add a duplicate delete implementation or unrelated upload edits merely to touch two files. Explain this in integrity/decision evidence and prove both supported provider paths;if a duplicated affected counterpart is found,route before edits. No s3UploadService write without that evidence.
Read test integrity;new retained real-service regression must reject old helper (all affected malformed cases),mutants restored. Focused existing delete/key suites+lint/build;no full/browser until dispatch slot. QA actual-app baseline gets next bounded browser slot;you may implement/focused now. Commit item2,merge verified unstable,normal full gate/durable ancestry then close. No upload/CORS/retention-policy/features/owner-bucket behavior changes. Feed audit deferred3,not cancelled. Send ready/blocker/landing/final with exact claims and limits.
## Archived completed assignment: storage-cors-1 — preservation fix routed
Implementation routed after worker and independent dispatch offline reproduction: adding second origin replaces the entire existing rule, existingRulesPreserved0/1. Dispatch command env -u NODE_TEST_CONTEXT node /tmp/bitvid-dispatch-storage-cors-reproduce.mjs exits1 at strict false!==true after all5 characterization controls. No cloud IO.
Now implement only s3-multipart.js plus GetBucketCorsCommand export in s3-client.js and new storage-cors-preservation test/integrity note. Preserve every existing rule and all fields verbatim; add a separate existing-default-shaped rule for uncovered requested origins without broadening arbitrary rules. Idempotence: no PUT when existing equivalent default rule already covers all requested origins; explicitly test equivalent-rule detection and repeated calls. Missing config only on NoSuchCORSConfiguration; other read failures propagate with no PUT. Do not guess wildcard equivalence or merge restrictive/custom rule semantics. Preserve empty-origin no-op. Tests must use actual helper/command export, fake transport only, cover existing rules/wildcards/duplicates/no-config/read-denied/idempotence and bounded provider errors; do not silently replace on read failure. Do not change current default methods/headers/TTL as unrelated policy; if SDK/provider limits make safe preservation impossible, report before broadening. Offline mutants for every new scenario; preserve normal assertions/gates. Relevant focused storage suites/lint/build; request full-suite slot (client ahead), no browser/cloud need. CLI/test/lander launches clear NODE_TEST_CONTEXT. Commit item1, merge unstable, normal queue/durable ancestry before close; no premature qualification of real S3.
Historical investigation context follows; its before-product restriction is superseded by the routed scope above.
Bounded parallel trial: validate todo/TODO_2026-06-16_r2-followups.md CORS replace-not-merge report at current HEAD. Dispatch inspected ensureBucketCors in js/storage/s3-multipart.js: constructs PutBucketCors with requested origins only, no GetBucketCors. This is evidence for investigation, not proof every caller loses policy. Trace actual callers and required existing-origin behavior. Reproduce offline via real helper and fake S3 transport/SDK boundary: an existing origin/rule, adding a second origin, idempotent repeated call, missing config and read-failure cases. No cloud credentials/network/bucket mutation.
First report smallest demonstrated loss plus proposed preservation semantics/files to dispatch before product changes. Inspect AWS command export in s3-client read-only. You own s3-multipart.js/s3-client.js and new tests/storage-cors-preservation.test.mjs only after fix routing; no R2/S3 upload service copies, app, signer or UI edits. Existing wildcard/method/header/non-origin rule semantics must not be guessed; read failure must not silently overwrite a policy. Do not add broad SDK/dependency upgrade. If stale/already fixed, close with evidence and propose next independent TODO. No browser initially. Relevant focused storage checks, lint, full normal gate/build after approved fix; ask dispatch before expensive runs. Integrity notes docs/testing/storage-cors-integrity.md.
## Coordination and limits
Send dispatch a read/next-action acknowledgement immediately, then claim your item and keep .task current. Record start/end UTC and time blocked by tools/browser/gates. Report exact SHA/commands/counts/unverified scope; mailbox on blocker, reproduced result, landing and final. Read RULE-CHANGES, PROGRAM, KNOWN_ISSUES, repo memory, and test-integrity before changes. New tests require failing mutants; no weakened expectations/retries/sleeps. Product/test fixes must be scoped from demonstrated evidence. No shared TEST_INTEGRITY.md or docs/TODO.md edits: keep uniquely prefixed integrity/evidence notes under docs/testing and ask dispatch to integrate status. Do not edit another area worktree or stop another process. Full-suite runs and product landings need dispatch resource coordination (one expensive gate at a time, unchanged normal landq). Focused offline checks can overlap. Commit named files via fleet tool; merge unstable, never rebase; normal queue/durable status+ancestry before close. No subagents initially. No deploy/push/spend/real-wallet/production-data/secrets/scheduler/kit changes. Preserve preview3000. Public network/real buckets/signers/peers forbidden in fixtures.
+3
View File
@@ -0,0 +1,3 @@
You are a scheduled headless job of the {{project}} dispatcher, started by tools/fleet/bin/sched.sh in the dispatcher's worktree on {{date}}. You have no memory of earlier runs: docs/agents/PROGRAM.md, the backlog, `node tools/fleet/bin/doctor.mjs` and docs/agents/WIND-DOWN.md are your context. Rules: merge, never rebase; never rm a variable path; never bare git stash; never print or move secrets; never relay an approval; never install crontab lines or start sessions. Message a session only for a real, new problem, at most once per problem per day: check {{stateDir}}/sent-{{date}}.txt and append what you send. Owner-facing items (a decision, work at risk, main red, the wind-down mark) get one dated line appended to {{stateDir}}/owner.md. Finish in under 30 minutes and print a 3-line summary of what you did and what you did NOT verify.
TASK: hourly check-in. Run `node tools/fleet/bin/doctor.mjs --json`, `node tools/fleet/bin/landq.mjs status` and read every WAITING line. For each WAITING older than 2 hours, message the session being waited on (once per day). A lander that is NOT running is owner-facing. Red or conflict results: make sure the submitting area knows (mailbox or native message). Record new strikes (red landing, withdrawn submission, reviewer rejection, peer-caught false green, owner regression) in docs/agents/STRIKES.md; two in a week for one area is owner-facing with a proposed tier move. If the usage governor is enabled and the adapter usage hook reports at or above usage.windDownAtPct, that is owner-facing: run docs/agents/WIND-DOWN.md step 1 (create {{stateDir}}/PAUSED with the reason) and stop. Land any doc change with `node tools/fleet/bin/landq.mjs submit`.
+3
View File
@@ -0,0 +1,3 @@
You are a scheduled headless job of the {{project}} dispatcher, started by tools/fleet/bin/sched.sh in the dispatcher's worktree on {{date}}. You have no memory of earlier runs: docs/agents/PROGRAM.md, the backlog, `node tools/fleet/bin/doctor.mjs` and docs/agents/WIND-DOWN.md are your context. Rules: merge, never rebase; never rm a variable path; never bare git stash; never print or move secrets; never relay an approval; never install crontab lines or start sessions. Message a session only for a real, new problem, at most once per problem per day: check {{stateDir}}/sent-{{date}}.txt and append what you send. Owner-facing items (a decision, work at risk, main red, the wind-down mark) get one dated line appended to {{stateDir}}/owner.md. Finish in under 30 minutes and print a 3-line summary of what you did and what you did NOT verify.
TASK: tree hygiene. Run `node tools/fleet/bin/doctor.mjs`. For each DIRTY tree with a .task older than 24 h, ONE-DISK commits older than a day, or a missing .task, message the owning area once (never touch its tree yourself). List scratch directories under /tmp that belong to finished runs (owner pid dead) and report them; do not delete anything outside your own worktree. Check the mailbox directory for messages unread for over a day and nudge their recipients.
+3
View File
@@ -0,0 +1,3 @@
You are a scheduled headless job of the {{project}} dispatcher, started by tools/fleet/bin/sched.sh in the dispatcher's worktree on {{date}}. You have no memory of earlier runs: docs/agents/PROGRAM.md, the backlog, `node tools/fleet/bin/doctor.mjs` and docs/agents/WIND-DOWN.md are your context. Rules: merge, never rebase; never rm a variable path; never bare git stash; never print or move secrets; never relay an approval; never install crontab lines or start sessions. Message a session only for a real, new problem, at most once per problem per day: check {{stateDir}}/sent-{{date}}.txt and append what you send. Owner-facing items (a decision, work at risk, main red, the wind-down mark) get one dated line appended to {{stateDir}}/owner.md. Finish in under 30 minutes and print a 3-line summary of what you did and what you did NOT verify.
TASK: restart after the reset. Remove {{stateDir}}/PAUSED if it exists (it is a single known file; delete it by that literal path). Append one line to owner.md that the fleet may restart. If the dispatcher session is reachable, message it: "Restart: read docs/agents/RESUME-BRIEF.md and resume". Do NOT start sessions yourself unless fleet.config.json says the owner approved automatic restarts.
+3
View File
@@ -0,0 +1,3 @@
You are a scheduled headless job of the {{project}} dispatcher, started by tools/fleet/bin/sched.sh in the dispatcher's worktree on {{date}}. You have no memory of earlier runs: docs/agents/PROGRAM.md, the backlog, `node tools/fleet/bin/doctor.mjs` and docs/agents/WIND-DOWN.md are your context. Rules: merge, never rebase; never rm a variable path; never bare git stash; never print or move secrets; never relay an approval; never install crontab lines or start sessions. Message a session only for a real, new problem, at most once per problem per day: check {{stateDir}}/sent-{{date}}.txt and append what you send. Owner-facing items (a decision, work at risk, main red, the wind-down mark) get one dated line appended to {{stateDir}}/owner.md. Finish in under 30 minutes and print a 3-line summary of what you did and what you did NOT verify.
TASK: morning summary for the owner. Write a short dated section into {{stateDir}}/owner.md: what landed since yesterday (`git log --oneline --since=yesterday <main>`), what is red or held, who is waiting on the owner (decisions, approvals, frames to look at), and the plan for today from PROGRAM.md priorities. Under 25 lines, plain words, every claim with a SHA or a file path.
+3
View File
@@ -0,0 +1,3 @@
You are a scheduled headless job of the {{project}} dispatcher, started by tools/fleet/bin/sched.sh in the dispatcher's worktree on {{date}}. You have no memory of earlier runs: docs/agents/PROGRAM.md, the backlog, `node tools/fleet/bin/doctor.mjs` and docs/agents/WIND-DOWN.md are your context. Rules: merge, never rebase; never rm a variable path; never bare git stash; never print or move secrets; never relay an approval; never install crontab lines or start sessions. Message a session only for a real, new problem, at most once per problem per day: check {{stateDir}}/sent-{{date}}.txt and append what you send. Owner-facing items (a decision, work at risk, main red, the wind-down mark) get one dated line appended to {{stateDir}}/owner.md. Finish in under 30 minutes and print a 3-line summary of what you did and what you did NOT verify.
TASK: stale triage. `node tools/fleet/bin/backlog.mjs list` and the stale-items section of `doctor.mjs`. For each in-progress item no commit has named for the stale window: ask its area whether it is still active, blocked (then set status=blocked blockedOn=...) or should be deferred. For asks in owner.md or the mailbox with no owning area: propose the ONE owning area to the dispatcher. Never cancel an item; only the owner cancels, with a reason.
+3
View File
@@ -0,0 +1,3 @@
You are a scheduled headless job of the {{project}} dispatcher, started by tools/fleet/bin/sched.sh in the dispatcher's worktree on {{date}}. You have no memory of earlier runs: docs/agents/PROGRAM.md, the backlog, `node tools/fleet/bin/doctor.mjs` and docs/agents/WIND-DOWN.md are your context. Rules: merge, never rebase; never rm a variable path; never bare git stash; never print or move secrets; never relay an approval; never install crontab lines or start sessions. Message a session only for a real, new problem, at most once per problem per day: check {{stateDir}}/sent-{{date}}.txt and append what you send. Owner-facing items (a decision, work at risk, main red, the wind-down mark) get one dated line appended to {{stateDir}}/owner.md. Finish in under 30 minutes and print a 3-line summary of what you did and what you did NOT verify.
TASK: weekly wind-down before the usage reset. Follow docs/agents/WIND-DOWN.md: create {{stateDir}}/PAUSED with the reason, tell every area to park, commit, merge, submit and update .task + docs/TODO.md, then check doctor and the landing queue. Draft docs/agents/RESUME-BRIEF.md from the backlog, doctor and TODO.md and land it. Owner-facing: one line saying the fleet is paused and why.
+113
View File
@@ -0,0 +1,113 @@
# Channel subscriber counts — feature scope
> Status: **Scoped; not implemented** · Created 2026-07-26
## Goal
Show a useful subscriber/follower count on public BitVid channel profiles without
misrepresenting an estimate as a global Nostr fact or weakening private
subscriptions.
## Constraint that determines the design
BitVid subscriptions are encrypted, private NIP-51 lists. Relays cannot answer
"how many people subscribe to this creator?" from those lists. Querying public
follow lists, where they exist, would be incomplete, expensive, sybil-prone,
and would encourage users to make private follows discoverable. Therefore this
feature must not derive a global count by scanning followers' events.
## Recommended product definition
Ship a clearly labelled **BitVid followers** count, not an unqualified global
"Subscribers" count. It should count only voluntary, public follow attestations
that BitVid can verify and should remain an estimate of the BitVid-visible
audience. Keep the existing private subscription feature private and do not use
it as a counting source.
Before implementation, make one explicit product decision:
1. **Public attestation count (recommended):** a viewer may opt in to publish a
public, signed follow attestation; BitVid counts valid unique authors for a
creator. This has a real verifiable numerator but is necessarily partial.
2. **Creator-reported count:** creator publishes a signed number. This is cheap,
but is self-reported and must be labelled as such.
3. **No public count:** retain private subscriptions only. This is the strongest
privacy option.
Do not silently fall back from option 1 to option 2.
## Proposed implementation slice (option 1)
### Data model and protocol
- Research existing Nostr follow/interest conventions first; reuse an
interoperable event if one supports a public creator follow without exposing
the private BitVid list.
- If no suitable convention exists, write a small BitVid event proposal before
coding: event kind, tags, replaceability/deletion semantics, creator target,
timestamp rules, relay selection, and migration/version tag.
- Count at most one current attestation per follower/creator relationship.
- Ignore malformed events, self-follows, deleted/replaced events, and events
outside the selected protocol version.
- Treat counts as eventually consistent and relay-scoped. Never claim a global
Nostr total.
### Read path and caching
- Add a dedicated count service/store; do not add follower scans to
`channelProfile.js` rendering or the normal feed bootstrap.
- Fetch only when a channel profile is opened, with a bounded relay set,
cancellation on profile navigation, in-flight deduplication, and a short
cache TTL.
- Render a compact skeleton or omit the count while loading. A failed count
lookup must never block profile metadata, video loading, zap controls, or
subscriptions.
- Expose source/coverage only in a low-noise detail surface if needed (for
example, "BitVid-visible followers"), not as a misleading exact total.
### Channel-profile UI
- Place the count in the creator summary/stat row near videos/zaps, not in the
profile modal.
- Use the chosen product label consistently: `BitVid followers` for public
attestations; `Creator-reported subscribers` for self-reported data.
- Provide an accessible label and compact mobile layout; large counts use
locale-aware formatting.
- The viewer's private Subscribe/Unsubscribe control remains unchanged. If an
opt-in public follow is added, it is a separate, explicit privacy choice.
### Abuse and privacy safeguards
- Do not expose who follows whom by default beyond the public attestations the
viewer chose to publish.
- Do not rank or moderate creators solely by this count.
- Rate-limit/bound relay queries and deduplicate by author; document that sybil
resistance is not provided by a raw public count.
- Preserve viewer blocks/mutes and creator visibility rules in any optional
follower preview; the count itself must not leak a viewer's private list.
## Tests and acceptance criteria
- Unit tests: unique-author counting, replacement/deletion, malformed events,
self-follow exclusion, cache TTL, and formatted display values.
- Service tests: bounded/cancelled relay queries, timeout/failure returns an
unavailable state without clearing a good cached count.
- UI tests: loading, available, unavailable, and mobile wrapping states.
- Manual QA: opening a channel profile has no visible impact on Home feed or
private subscription list loading; toggling a private subscription publishes
no public attestation unless the viewer explicitly opts in.
## Non-goals for the first release
- A global Nostr subscriber count.
- Inferring counts from private encrypted lists.
- Follower directories, recommendations, notifications, or monetized tiers.
- Using counts as a trust, moderation, or discovery ranking signal.
## Implementation order
1. Approve the product definition and public/private wording.
2. Complete protocol/convention research and record the selected event shape.
3. Build the bounded count store with tests and fixtures.
4. Add channel-profile presentation and responsive UI tests.
5. Run privacy/performance QA before enabling public-attestation publishing.
@@ -0,0 +1,10 @@
# Cloudflare/S3 Upload-Path Gotchas (audited 2026-06-16)
> Extracted verbatim from `AGENTS.md` §4a, which retains a short summary. Dated incident writeup from the 2026-06-16 upload-path audit.
* **Two near-identical upload services exist** — `js/services/r2Service.js` (Cloudflare R2) and `js/services/s3UploadService.js` (generic S3). They share `buildR2Key`, `resolveUploadIdentifier`, and the magnet build but are *copies*. **Any fix to one must be mirrored in the other** or they silently drift.
* **Storage-key collisions = data loss — FIXED 2026-06-16.** `buildR2Key` (`js/r2.js`) builds `u/<npub>/<namespace>/<slug>.<ext>` where `namespace` used to fall back to the literal `"uploads"` and `slug` to `"video"` (e.g. a non-ASCII filename, or no info-hash) — so a URL-first upload with no info-hash and a duplicate filename overwrote the previous object while the old note still pointed at that URL. WebTorrent uploads were always safe (info-hash namespaces the key = content-addressed). Fix: `computeStorageContentHash(file)` (`js/r2.js`) derives a content-based namespace when no info-hash is available (full SHA-256 for ≤512 MB, sampled metadata+edge fingerprint above that to avoid buffering huge files), wired into **both** `r2Service` and `s3UploadService`. Test: `tests/storage-key-collision.test.mjs`. **If you add a third upload path, reuse `computeStorageContentHash`.**
* **Whole-file hash before upload.** When the modal doesn't pre-seed an info-hash, `resolveUploadIdentifier` runs `calculateTorrentInfoHash(file)`, which reads the *entire* file before the upload begins and before any progress is emitted — on multi-GB files this looks like a hang. Emit a "preparing/hashing" status or parallelize.
* **Publish can report success unconfirmed.** `publishEventToRelay` (`js/nostrPublish.js`) has an optimistic-success fallthrough when the relay handle exposes no `on()`/`then()` — a note may show "Published" with no relay ACK. Don't tighten blindly (legacy `seen`-only relays rely on it), but log it.
* **Silent partial failures.** Thumbnail and `.torrent` upload failures are caught and `warn`-only; the video note still publishes with no user signal. Empty (0-byte) files hit `CompleteMultipartUpload` with `Parts:[]` and surface a cryptic error — guard `file.size === 0`.
* **Object cleanup (deletes/edits).** `R2Service.deleteVideoStorage()` removes the backing objects so "deleted" videos aren't left publicly downloadable. Contract to preserve: it's **best-effort and never throws** (must not block note deletion), it derives keys via `collectVideoStorageKeys()` and **only ever touches URLs under the owner's bucket base** (never external/3rd-party URLs), and it skips cleanly when storage is locked/credential-less. Wired into delete (`app.handleDeleteModalConfirm`) and edit-with-replaced-URL (`editModalController.handleSubmit`). Edit cleanup intentionally does **not** delete thumbnails (the new note may still reference them) and only fires when both old and new URLs are present and different.
Loaded 100 of 395 files, more files were not shown because too many files have changed in this diff. Show more