Compare commits

...
Author SHA1 Message Date
thePR0M3TH3AN ff107fa556 land: area/client-reliability bc5cb631e0 (client-reliability) 2026-10-29 10:59:05 -04:00
thePR0M3TH3AN bc5cb631e0 ci: pin browser jobs to supported Ubuntu 24.04 for release validation 2026-10-29 10:59:05 -04:00
thePR0M3TH3AN a2dbaccc6c land: area/playback-qa 732a781104 (playback-qa) 2026-10-26 21:56:08 -04:00
thePR0M3TH3AN 732a781104 Merge branch 'unstable' into area/playback-qa 2026-10-26 21:55:59 -04:00
thePR0M3TH3AN 0bc9c722a5 docs: preserve causal and candidate qualification evidence (playback-qa-4) 2026-10-26 21:55:50 -04:00
thePR0M3TH3AN f7d0a458cd land: area/client-reliability 1b1c7ff373 (client-reliability) 2026-10-26 21:54:55 -04:00
thePR0M3TH3AN 1b1c7ff373 Merge branch 'unstable' into area/client-reliability 2026-10-26 21:54:55 -04:00
thePR0M3TH3AN 5cf53824c0 docs: close client-reliability-10 after durable qualification evidence
Product5b0adf6b normal439gate, exact-candidate native acceptance, buildartifact86fc5236, required evidence af8e6540 and ancestry verified. Preserve separate overflow/live/native limits; no new runtime.
2026-10-26 21:54:53 -04:00
thePR0M3TH3AN af8e654010 land: area/client-reliability 23c69eba0f (client-reliability) 2026-10-26 21:54:16 -04:00
thePR0M3TH3AN 23c69eba0f docs: record client-reliability-10 build and artifact evidence
Build naturalexit0, existing artifact verifierPASS/version86fc5236/indexSHA50a89bd6/helpercmp0. Runtime equal to QA candidate; no artifact publication or repeated validation. Preserve original qualification limits.
2026-10-26 21:54:15 -04:00
thePR0M3TH3AN 36716dfe1e Merge branch 'unstable' into area/client-reliability 2026-10-26 21:52:30 -04:00
thePR0M3TH3AN 3104843276 docs: record client-reliability-10 normal landing evidence
Unchanged439-file gate exit0, durable5b0adf6b and ancestry verified. Preserve QA attribution and historical/live limits; remaining build/artifact stage awaits grant.
2026-10-26 21:52:28 -04:00
thePR0M3TH3AN 5b0adf6b3e land: area/client-reliability aff8841694 (client-reliability) 2026-10-26 21:46:11 -04:00
thePR0M3TH3AN aff8841694 fix: recover completed startup fade cleanup (client-reliability-10)
Extract existing delegated cleanup from index into one cohesive helper and reconcile finished interface CSS animations when initialization arrives after animationend. Preserve active/replacement effects, future end/cancel events, thumbnail eligibility and unrelated classes. No CSS or other bootstrap changes.

New17 controlled scenarios including real isolated index cleanup wiring;19distinct mutants killed/all17casescovered, full lint11pass. Existing assertions unchanged. Full gate, build and native point validation still require separate slots.
2026-10-26 21:39:42 -04:00
thePR0M3TH3AN f01ce01259 land: area/client-reliability 2f9ac1e553 (client-reliability) 2026-10-26 15:43:13 -04:00
thePR0M3TH3AN 2f9ac1e553 docs: record client-reliability-9 build and artifact qualification
Local build and existing artifact verifier exit0/version15868a91. Preserve sandbox swcompat history skip and unchanged-input host lint evidence; native/browser/live boundaries remain open.
2026-10-26 15:43:08 -04:00
thePR0M3TH3AN 7594c54deb Merge branch 'unstable' into area/client-reliability 2026-10-26 15:41:25 -04:00
thePR0M3TH3AN d5a69faaf0 docs: record client-reliability-9 torrent normal landing
Durable af61c271 after unchanged438-file gate exit0; ancestry verified. Preserve rejected275 partial separately. Next build/browser stage remains explicitly unverified and ungranted.
2026-10-26 15:41:17 -04:00
thePR0M3TH3AN af61c2716b land: area/client-reliability eafa364d39 (client-reliability) 2026-10-26 15:32:54 -04:00
thePR0M3TH3AN eafa364d39 Merge branch 'unstable' into area/client-reliability 2026-10-26 15:32:05 -04:00
thePR0M3TH3AN e35cb80ca1 land: area/dispatch 2754a8af1f (dispatch) 2026-10-26 15:24:00 -04:00
thePR0M3TH3AN 2754a8af1f [fleet] record hourly audit and thermal landing strike 2026-10-26 15:23:43 -04:00
thePR0M3TH3AN fd31797bd5 docs: preserve client-reliability-9 thermal gate interruption
Retain strict partial 275/438 starts, rejected queue result and full lint11 pass. No product changes, gate changes, retry or landing claim; renewed resource slot required.
2026-10-26 15:16:39 -04:00
thePR0M3TH3AN c20635bc85 docs: checkpoint client-reliability-9 merged candidate and resource hold 2026-10-26 15:09:00 -04:00
thePR0M3TH3AN 1bf50c1f7a Merge branch 'unstable' into area/client-reliability 2026-10-26 15:08:15 -04:00
thePR0M3TH3AN 1b2362d234 fix: cancel final-owner active torrent probes (client-reliability-9)
Preserve shared owners and standalone probe callers. Clear obsolete timers and defer temporary torrent destruction until event unwind, holding the queue slot through teardown. Guard cache and replacement inflight identity.

Spec correction: previous queued-only active reuse/cache expectations are replaced by equally strict final-owner cleanup and fresh-request assertions, with independent live/shared/cache controls retained. See unique spec and Test Integrity Note.

Saved focused checks: six files, 60 scenarios, zero failures/cancelled; 19 distinct mutants killed. Remaining full lint/full/build/browser and normal landing require a resource slot. No submission.
2026-10-26 15:08:05 -04:00
thePR0M3TH3AN 04c8c7cf61 fleet init 2026-10-26 15:00:32 -04:00
thePR0M3TH3AN e266b00d79 fleet update: resolve Bitvid template sidecars 2026-10-26 14:59:40 -04:00
thePR0M3TH3AN b908803232 fleet init 2026-10-26 14:59:04 -04:00
thePR0M3TH3AN 2d695e7acd fleet update 2026-10-26 14:58:19 -04:00
thePR0M3TH3AN ab1ea6dee1 docs: record client-reliability-9 active torrent cancellation baseline 2026-10-26 11:26:24 -04:00
thePR0M3TH3AN e9af76dfd2 docs: reconcile client-reliability-9 HLS controls and remaining lifecycle scope 2026-10-26 09:44:04 -04:00
thePR0M3TH3AN 5f8e58bec0 fleet: sync e7b0635 runtime while preserving HLS evidence 2026-10-24 15:44:39 -04:00
thePR0M3TH3AN 0ffd92a8d2 fleet init 2026-10-24 15:43:56 -04:00
thePR0M3TH3AN a94530403d land: area/dispatch 7c878d65f8 (dispatch) 2026-10-24 15:43:40 -04:00
thePR0M3TH3AN 49c5839115 fleet init 2026-10-24 15:43:40 -04:00
thePR0M3TH3AN 7c878d65f8 fleet: preserve Bitvid TODO through e7b0635 upgrade 2026-10-24 15:43:40 -04:00
thePR0M3TH3AN 75e00f2560 fleet update 2026-10-24 15:42:46 -04:00
thePR0M3TH3AN b149cebf31 land: area/dispatch 83a847e059 (dispatch) 2026-10-24 15:23:50 -04:00
thePR0M3TH3AN 83a847e059 docs: record hourly fleet check-in and recovered strike events 2026-10-24 15:23:34 -04:00
thePR0M3TH3AN 146076dd38 Merge branch 'unstable' into area/client-reliability 2026-10-23 22:57:33 -04:00
thePR0M3TH3AN 14cd7b7467 docs: retain client-reliability-9 controlled browser results and Firefox blocker 2026-10-23 22:57:28 -04:00
thePR0M3TH3AN d1d6e0fed1 land: area/dispatch 1222805b2d (dispatch) 2026-10-23 09:05:27 -04:00
thePR0M3TH3AN 1222805b2d Merge branch 'unstable' into area/dispatch 2026-10-23 09:05:24 -04:00
thePR0M3TH3AN 7f4113ad54 dispatch-2 client-reliability-9: retain HLS gate evidence, resource provenance and mobile QA blocker 2026-10-23 09:05:11 -04:00
thePR0M3TH3AN 7b667e797a docs: record client-reliability-9 HLS gate evidence 2026-10-23 09:01:36 -04:00
thePR0M3TH3AN a482d9d20f land: area/client-reliability 7b667e797a (client-reliability) 2026-10-23 09:01:36 -04:00
thePR0M3TH3AN 103eb04cd9 land: area/storage-cors 492557400c (storage-cors) 2026-10-23 08:56:58 -04:00
thePR0M3TH3AN 492557400c Merge branch 'unstable' into area/storage-cors 2026-10-23 08:54:48 -04:00
thePR0M3TH3AN 144ff557b1 storage-cors: record hosted Range probe audit 2026-10-23 08:54:43 -04:00
thePR0M3TH3AN 83fd2495b4 land: area/client-reliability 280f043a66 (client-reliability) 2026-10-23 08:51:52 -04:00
thePR0M3TH3AN 280f043a66 Merge branch 'unstable' into area/client-reliability 2026-10-23 08:41:12 -04:00
thePR0M3TH3AN dd092ed856 land: area/dispatch 2955645297 (dispatch) 2026-10-23 08:39:19 -04:00
thePR0M3TH3AN 3a89abd05b fleet init 2026-10-23 08:37:56 -04:00
thePR0M3TH3AN 2955645297 Merge branch 'unstable' into area/dispatch 2026-10-23 08:34:30 -04:00
thePR0M3TH3AN 6c64b97156 dispatch-2 client-reliability-9: record gate restart and relayed seven-schedule authorization 2026-10-23 08:34:21 -04:00
thePR0M3TH3AN d7184f07bd Merge branch 'unstable' into area/client-reliability 2026-10-23 08:32:59 -04:00
thePR0M3TH3AN 672ca95395 land: area/dispatch 98a20b991d (dispatch) 2026-10-22 17:05:00 -04:00
thePR0M3TH3AN 98a20b991d Merge branch 'unstable' into area/dispatch 2026-10-22 17:04:51 -04:00
thePR0M3TH3AN b38cefca36 dispatch-2 client-reliability-9: record durable HEAD and storage-cors-4 landings, route HLS lifecycle and mobile QA 2026-10-22 17:04:42 -04:00
thePR0M3TH3AN 79a9a1301c Merge branch 'unstable' into area/client-reliability 2026-10-22 17:02:58 -04:00
thePR0M3TH3AN cdc398b028 [client-reliability] client-reliability-9 cancel ownerless HLS manifest probes 2026-10-22 17:02:48 -04:00
thePR0M3TH3AN 8317da4d84 land: area/storage-cors 265ce4261f (storage-cors) 2026-10-22 16:59:01 -04:00
thePR0M3TH3AN 265ce4261f storage-cors-4: record durable gate, build and closure 2026-10-22 16:58:44 -04:00
thePR0M3TH3AN d9424fb7bd land: area/client-reliability fb0e671d4d (client-reliability) 2026-10-22 16:57:23 -04:00
thePR0M3TH3AN fb0e671d4d [client-reliability] client-reliability-9 record active HLS cancellation reproduction 2026-10-22 16:54:36 -04:00
thePR0M3TH3AN 7edc7ea86f land: area/storage-cors 888807baf6 (storage-cors) 2026-10-22 16:52:39 -04:00
thePR0M3TH3AN 888807baf6 Merge branch 'unstable' into area/storage-cors 2026-10-22 16:52:18 -04:00
thePR0M3TH3AN d04d501ee2 land: area/client-reliability 4f1d954872 (client-reliability) 2026-10-22 16:51:41 -04:00
thePR0M3TH3AN 4f1d954872 Merge branch 'unstable' into area/client-reliability 2026-10-22 16:51:35 -04:00
thePR0M3TH3AN 77f03cd6c6 [client-reliability] client-reliability-9 record native lifecycle evidence and remaining scope 2026-10-22 16:51:26 -04:00
thePR0M3TH3AN 0219bd31a4 [client-reliability] client-reliability-9 record normal landing and build evidence 2026-10-22 16:48:37 -04:00
thePR0M3TH3AN b21ce86c8e storage-cors-4: guard obsolete registered feed completions; close landed storage-cors-3 2026-10-22 16:46:33 -04:00
thePR0M3TH3AN f41ac656f6 land: area/client-reliability 09f52bdd50 (client-reliability) 2026-10-22 16:42:27 -04:00
thePR0M3TH3AN 09f52bdd50 Merge branch 'unstable' into area/client-reliability 2026-10-22 16:41:56 -04:00
thePR0M3TH3AN a79d49df42 fleet: retain Bitvid TODO after 923860e update 2026-10-22 16:39:49 -04:00
thePR0M3TH3AN 26e9a787cf land: area/dispatch a79d49df42 (dispatch) 2026-10-22 16:39:49 -04:00
thePR0M3TH3AN 64299297e1 fleet init 2026-10-22 16:39:31 -04:00
thePR0M3TH3AN d917053daa fleet update 2026-10-22 16:38:46 -04:00
thePR0M3TH3AN 75bc92f3a4 land: area/dispatch 69afc2f1a4 (dispatch) 2026-10-22 15:17:39 -04:00
thePR0M3TH3AN 69afc2f1a4 fleet: preserve Bitvid TODO and remove resolved kit sidecar 2026-10-22 15:17:39 -04:00
thePR0M3TH3AN a797d9a8f1 fleet init 2026-10-22 15:16:11 -04:00
thePR0M3TH3AN b8e87e655b fleet update 2026-10-22 15:15:22 -04:00
thePR0M3TH3AN 6495e0618c land: area/dispatch 30a13b2bbf (dispatch) 2026-10-22 10:36:23 -04:00
thePR0M3TH3AN 30a13b2bbf storage-cors-4 dispatch-3: route independently reproduced stale feed lifecycle fix 2026-10-22 10:32:14 -04:00
thePR0M3TH3AN 63e2b51961 land: area/storage-cors 984c219cd2 (storage-cors) 2026-10-22 10:31:28 -04:00
thePR0M3TH3AN 984c219cd2 Merge branch 'unstable' into area/storage-cors 2026-10-22 10:31:14 -04:00
thePR0M3TH3AN aee09d14e1 storage-cors-3: record merged readiness and separate registered-view race evidence 2026-10-22 10:31:12 -04:00
thePR0M3TH3AN 34edc6ce65 storage-cors-3 dispatch-3: retain independent candidate13 scenario verification 2026-10-22 10:27:18 -04:00
thePR0M3TH3AN 4bea541e67 [client-reliability] client-reliability-9 record merged candidate readiness 2026-10-22 10:27:10 -04:00
thePR0M3TH3AN e887ceafda Merge branch 'unstable' into area/client-reliability 2026-10-22 10:26:10 -04:00
thePR0M3TH3AN e9c960edcf [client-reliability] client-reliability-9 cancel ownerless active HEAD probes 2026-10-22 10:26:08 -04:00
thePR0M3TH3AN 4de357e4a7 land: area/playback-qa 48999ca22e (playback-qa) 2026-10-22 10:24:36 -04:00
thePR0M3TH3AN 48999ca22e Merge branch 'unstable' into area/playback-qa 2026-10-22 10:24:20 -04:00
thePR0M3TH3AN f4618d5d29 playback-qa-3: retain Firefox CI investigation and blocked evidence checkpoint 2026-10-22 10:24:17 -04:00
thePR0M3TH3AN ed3fff12d6 dispatch-3 dispatch-6 playback-qa-4: retain Firefox limits and route mobile investigation 2026-10-22 10:24:14 -04:00
thePR0M3TH3AN 5806fa69cb land: area/dispatch ed3fff12d6 (dispatch) 2026-10-22 10:24:14 -04:00
thePR0M3TH3AN 89f8923c6e Merge branch 'unstable' into area/storage-cors 2026-10-22 10:22:14 -04:00
thePR0M3TH3AN fb1997de42 storage-cors-3: prune tombstoned service snapshots and persisted feed cache 2026-10-22 10:22:12 -04:00
thePR0M3TH3AN ed5fb64991 storage-cors-3: preserve newer live versions on late deletion markers 2026-10-22 10:18:00 -04:00
thePR0M3TH3AN e21be27f34 Merge branch 'unstable' into area/client-reliability 2026-10-22 10:17:12 -04:00
thePR0M3TH3AN 145c09f7a6 [client-reliability] client-reliability-9 record HEAD retention reproduction 2026-10-22 10:17:10 -04:00
thePR0M3TH3AN 56417d11f0 client-reliability-9 dispatch-3: route reproduced active HEAD cleanup 2026-10-22 10:16:03 -04:00
thePR0M3TH3AN e04549f816 land: area/dispatch 56417d11f0 (dispatch) 2026-10-22 10:16:03 -04:00
thePR0M3TH3AN 89c5494aaa Merge branch 'unstable' into area/storage-cors 2026-10-22 10:15:03 -04:00
thePR0M3TH3AN 67140d891f storage-cors-3: record actual-path tombstone investigation and claim 2026-10-22 10:15:01 -04:00
thePR0M3TH3AN 169d894f72 storage-cors-3 dispatch-3: route reproduced buffer and service tombstone fixes 2026-10-22 10:13:56 -04:00
thePR0M3TH3AN 076ec80295 land: area/dispatch 169d894f72 (dispatch) 2026-10-22 10:13:56 -04:00
thePR0M3TH3AN 7369f1feeb dispatch-3: record three acknowledged campaign assignments 2026-10-22 10:09:38 -04:00
thePR0M3TH3AN f9bc7cb639 land: area/dispatch 7369f1feeb (dispatch) 2026-10-22 10:09:38 -04:00
thePR0M3TH3AN c5b678f7c9 land: area/dispatch c6022f21c6 (dispatch) 2026-10-22 10:06:58 -04:00
thePR0M3TH3AN c6022f21c6 dispatch-3: route six-priority campaign and stage acceptance gates 2026-10-22 10:06:58 -04:00
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
thePR0M3TH3AN 98c623c85a Promote beta to main: trust controls and hardened BitLogin 2026-07-25 22:48:54 -04:00
thePR0M3TH3AN 0513c7fcd8 test(moderation): align channel blur fixture with threshold 2026-07-25 22:48:46 -04:00
thePR0M3TH3AN 855022e911 Promote unstable to beta: trust controls and hardened BitLogin 2026-07-25 22:43:27 -04:00
thePR0M3TH3AN 589bda8cb0 feat(auth): enable BitLogin on unstable 2026-07-25 22:33:32 -04:00
thePR0M3TH3AN 18e08bf2cb feat: refine trust controls and harden BitLogin 2026-07-25 22:25:50 -04:00
thePR0M3TH3AN 126a613527 docs: link BitUnlock permission layer plan 2026-07-23 21:32:59 -04:00
thePR0M3TH3AN 4eaa78cf59 docs: add BitUnlock permission layer TODO 2026-07-23 21:32:49 -04:00
thePR0M3TH3AN f49080558b docs: add BitUnlock permission layer plan 2026-07-23 21:31:57 -04:00
thePR0M3TH3ANandClaude Sonnet 5 f25c2ac242 Re-vendor bitlogin widget: local session persistence
Pulls in bitlogin's session-persistence feature -- after a successful
sign-in, the everyday private key is cached in IndexedDB for this
origin, so a page reload restores the session instead of asking for
the login name + password again. No changes needed in
bitloginModalIntegration.js: the existing "bitlogin-login" listener
fires the same way whether the sign-in was just typed or silently
restored. See bitlogin's CHANGELOG.md (2026-07-22 entry).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-22 07:50:06 -04:00
thePR0M3TH3ANandClaude Sonnet 5 4d1116530b Surface BitLogin sign-in rejections instead of silently swallowing them
devLogger.warn is a no-op outside dev mode (IS_DEV_MODE = false on
this deployment), so when AuthService.requestLogin() rejected a
BitLogin sign-in -- e.g. the invite-only access-control check
("Access restricted to admins and moderators users only.") -- nothing
ever told the user why. The widget itself doesn't know bitvid rejected
it, so it kept showing its own "Signed in" screen, and the login modal
never closed, looking exactly like a hung/broken close button.

On rejection: route the error through app.handleLoginModalError (the
same visible-banner path every other sign-in method already uses) and
reset the widget via widget.logout() so it doesn't keep claiming a
session bitvid never actually granted.

Also added a WeakSet guard so wireBitloginLogin() can never attach two
"bitlogin-login" listeners to the same element, regardless of how
attachWidget() ends up getting invoked -- cheap insurance, no observed
bug currently triggers it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-22 00:19:43 -04:00
thePR0M3TH3ANandClaude Sonnet 5 f2d390ff18 Wire up NIP-04 in the BitLogin adapter, flip capabilities.nip04 to true
bitlogin's widget now implements NIP-04 (nip04Encrypt/nip04Decrypt),
closing the one gap left relative to a real NIP-07 extension. bitvid's
own nip07Adapter/nsecAdapter/nip46Adapter all already wire this up and
report it via signerCapabilities.nip04 -- BitLogin's adapter was the
one exception, hardcoded to nip04: false because the widget genuinely
didn't support it before. js/nostr/client.js's DM encrypt/decrypt path
only offers NIP-04 to a caller when that capability flag is true, so a
BitLogin-signed-in user previously had no working legacy-DM path at
all. Re-vendored the widget bundle (bitlogin's CHANGELOG.md, 2026-07-21
entry, third one) and updated the one test that hardcoded the old
(now-incorrect) capabilities shape.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 23:32:27 -04:00
thePR0M3TH3ANandClaude Sonnet 5 ca7b711b8a Widen pinned BitLogin relay set for redundancy against a single dead relay
relay.nostr.band (shared by both relay roles pinned here) is currently
unreachable, and BitLogin's publishAndVerify() enforces a fixed
ack/readback floor of 2 regardless of relay count -- so pinning to
only 3 vault relays left zero margin the moment one went down,
surfacing as a registration/login quorum failure.

Widen the <bitlogin-auth> vault-relays/discovery-relays attributes in
components/login-modal.html to match bitlogin's own updated built-in
defaults, and re-vendor the widget bundle (see bitlogin's CHANGELOG.md
2026-07-21 entry). No CSP change needed -- this branch's connect-src
already allows wss: broadly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 21:44:40 -04:00
thePR0M3TH3AN 88c906fd34 Add BitLogin as the primary sign-in method (gated, unstable)
The login modal now shows <bitlogin-auth> -- a portable Nostr identity
behind a login name and password -- as the default sign-in path,
styled with bitvid's own brand tokens (accent-strong, surfaces, radii).
The existing NIP-07 extension, nsec, and NIP-46 remote-signer options
still work exactly as before, just collapsed under a new "Other login
options" fold instead of a flat "Available methods" grid; nip07 no
longer carries the stale "Recommended" eyebrow now that BitLogin has
that role.

BitLogin isn't a single button-click-then-await-a-promise flow like
the other providers -- it's a whole multi-screen sign-in/create/recover
UI -- so it gets a permanent, statically-declared widget in
components/login-modal.html (js/ui/bitloginModalIntegration.js relocates
it, never destroys and recreates it, since removal tears down its crypto
Web Worker) instead of a grid button. It still goes through the exact
same session-establishment path as every other signer: a new "bitlogin"
provider (js/services/authProviders/bitlogin.js) lets AuthService
resolve and activate it via the normal requestLogin -> handleAuthLogin
path, while providersForModal excludes it from the grid so it isn't
rendered twice.

Two things this needed beyond the wiring itself:
- js/app.js is one line from its lint/file-size ceiling, so the only
  change there is renaming an import binding (providers -> providersForModal),
  a net-zero line change. The actual widget/event wiring lives in a new,
  unconstrained module instead.
- The login modal's HTML (including the permanent widget element) loads
  asynchronously after bootstrap runs, so wireBitloginLogin() waits via
  MutationObserver rather than assuming a specific call order.

Vendored via scripts/build-bitlogin-widget.mjs (mirrors the existing
Bitcoin Connect/Blossom vendor steps): BitLogin isn't published to npm,
so the widget build is copied verbatim from the sibling ../bitlogin
checkout into committed vendor/bitlogin/, then lazy-imported (like
Bitcoin Connect) so its ~350KB never weighs on visitors who never open
the login modal.

Gated behind FEATURE_BITLOGIN (config/instance-config.js), default on
for this unstable-branch experiment per AGENTS.md §1 -- reset to false
before this reaches main until verified end-to-end against production
relays.

Verified: npm run lint (clean), the full auth/login test suite (43/43,
including new coverage for the provider + adapter), and a manual
Chrome pass against the built dist/ -- widget renders themed correctly,
"Other login options" still shows the original four methods unchanged.
2026-07-21 20:56:01 -04:00
thePR0M3TH3AN 61f7d3f15e Promote beta → main: upload-artifact→v7, download-artifact→v8 (Node 24, clears remaining deprecation) 2026-07-14 10:11:48 -04:00
thePR0M3TH3AN 1d14a1f392 Promote unstable → beta: upload-artifact→v7, download-artifact→v8 (Node 24, clears remaining deprecation) 2026-07-14 10:11:46 -04:00
thePR0M3TH3ANandClaude Opus 4.8 676844edfc ci: bump upload-artifact→v7 and download-artifact→v8 for Node 24
The v5 line of these two actions still targets Node.js 20 (unlike
checkout/setup-node@v5, which are already on Node 24), so the deprecation
warning persisted. upload-artifact reaches Node 24 at v6+ and
download-artifact at v7+; move to the current latest (v7 / v8), which
share the same v4-generation artifact backend and pair correctly.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DwbE3uTiFaKRNjx5vP2muV
2026-07-14 10:11:44 -04:00
590 changed files with 73812 additions and 4646 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 the MCP tools `tell` and `hear`. Do not send those with the shell `fleet tell`: that is a sandboxed command, and the MCP approval mode does not apply to it. 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
+7 -7
View File
@@ -13,7 +13,7 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
matrix:
@@ -83,7 +83,7 @@ jobs:
- name: Upload dist hash artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/unstable'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: dist-hashes-current
path: artifacts/dist-hashes-current.json
@@ -96,7 +96,7 @@ jobs:
# deploy-cache-state CDN purge below. Real build/lint failures still gate the
# deploy via the build job.
visual-regression:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 30
strategy:
matrix:
@@ -144,7 +144,7 @@ jobs:
- name: Upload Playwright artifacts
if: steps.visual.outcome == 'failure'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: playwright-visual-diffs
path: |
@@ -171,7 +171,7 @@ jobs:
uses: actions/checkout@v5
- name: Download dist hash artifact
uses: actions/download-artifact@v5
uses: actions/download-artifact@v8
with:
name: dist-hashes-current
path: artifacts
@@ -401,7 +401,7 @@ jobs:
run: npm run test:dm:unit
e2e-tests:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 35
strategy:
fail-fast: false
@@ -489,7 +489,7 @@ jobs:
run: npm run test:dm:integration
e2e-headless:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
matrix:
@@ -1,8 +1,6 @@
name: Daily Design System Audit
on:
schedule:
- cron: '0 9 * * *' # Run daily at 09:00 UTC
workflow_dispatch:
permissions:
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
# `path: dist` stores the site files at the artifact root (index.html at top),
# so the downloaded .zip extracts straight to the nsite site directory.
- name: Upload site archive
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: bitvid-site-${{ github.ref_name }}-${{ steps.vars.outputs.sha }}
path: dist
@@ -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
}
}
}
+35 -5
View File
@@ -47,18 +47,48 @@
>
<div class="space-y-2 text-left">
<p class="text-sm text-text-muted">
Choose how you'd like to sign in. Connect a browser extension, link a
remote signer, or work directly with your keys when you need to.
Use BitLogin for a password-style sign-in, or connect the Nostr
identity you already use. Both keep you in control of your keys.
</p>
</div>
<!--
Permanent, statically-declared mount for <bitlogin-auth> -- never
written by an innerHTML re-render (this markup never changes after
the page loads), so the element is only ever hidden/shown, never
destroyed and recreated. Removing it from the DOM tears down its
crypto Web Worker (see BitLogin's README).
-->
<div
id="bitloginMount"
class="rounded-3xl border border-border bg-overlay-panel-soft/90 p-4 shadow-lg backdrop-blur-sm sm:p-5"
>
<div class="mb-3 space-y-1">
<h3 class="text-base font-semibold text-text">BitLogin</h3>
<p class="text-sm text-text-muted">
A portable login name and password for BitLogin-enabled sites.
</p>
</div>
<bitlogin-auth
id="bitloginWidget"
vault-relays="wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band,wss://nostr.wine,wss://relay.snort.social"
discovery-relays="wss://purplepag.es,wss://relay.nostr.band,wss://nostr-pub.wellorder.net"
></bitlogin-auth>
</div>
<section
class="space-y-5 rounded-3xl border border-border bg-overlay-panel-soft/90 p-4 shadow-lg backdrop-blur-sm sm:p-5"
class="space-y-5 rounded-3xl border border-accent/40 bg-overlay-panel-soft/90 p-4 shadow-lg backdrop-blur-sm sm:p-5"
aria-labelledby="nostrLoginOptionsTitle"
>
<div class="space-y-1">
<h3 class="text-base font-semibold text-text">Available methods</h3>
<p class="text-xs font-semibold uppercase tracking-wide text-accent">
Nostr sign-in
</p>
<h3 id="nostrLoginOptionsTitle" class="text-base font-semibold text-text">
Use your existing Nostr identity
</h3>
<p class="text-sm text-text-muted">
Pick the option that matches how you manage your Nostr identity.
Browser extension (NIP-07), remote signer (NIP-46), or your own key.
</p>
</div>
+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 -->
+20 -6
View File
@@ -276,6 +276,20 @@ export const FEATURE_AUDIO_INGEST = false;
*/
export const FEATURE_BITCOIN_CONNECT = true;
/**
* Whether the login modal offers BitLogin (a portable Nostr identity behind a
* login name/password, https://github.com/PR0M3TH3AN/bitlogin) as the primary
* sign-in method, with the NIP-07 extension, nsec, and NIP-46 options
* collapsed under "Other login options" (js/ui/bitloginModalIntegration.js).
* The vendored widget bundle (vendor/bitlogin/, scripts/build-bitlogin-widget.mjs)
* only lazy-loads once the login modal's HTML is present in the DOM.
*
* Enabled on unstable for the final production-relay smoke test. Keep this
* gated from beta/main until that manual login, reload, logout, and recovery
* pass is confirmed.
*/
export const FEATURE_BITLOGIN = true;
/**
* Whether this deployment offers Blossom (nostr-native blob storage) as a
* first-class upload provider alongside Cloudflare R2 / generic S3 / Backblaze B2
@@ -359,9 +373,9 @@ export const DEFAULT_TRUST_SEED_NPUBS = [
* When a video receives at least this many trusted reports for the active
* category (for example, `nudity`), thumbnails render with the blurred preview
* by default. Align this value with your moderation policy — the upstream
* deployment targets 1 trusted report before blurring.
* deployment requires 3 trusted reports before blurring.
*/
export const DEFAULT_BLUR_THRESHOLD = 1;
export const DEFAULT_BLUR_THRESHOLD = 3;
/**
* Baseline trusted-report count that blocks autoplay during browsing.
@@ -369,9 +383,9 @@ export const DEFAULT_BLUR_THRESHOLD = 1;
* Videos that meet or exceed this count will not autoplay in feeds unless the
* viewer explicitly opts in. Increase the number for a more permissive stance
* or decrease it if you want autoplay to stop sooner. The upstream deployment
* targets 1 trusted report before blocking autoplay.
* requires 3 trusted reports before blocking autoplay.
*/
export const DEFAULT_AUTOPLAY_BLOCK_THRESHOLD = 1;
export const DEFAULT_AUTOPLAY_BLOCK_THRESHOLD = 3;
/**
* Trusted mute threshold that hides creators globally.
@@ -388,10 +402,10 @@ export const DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD = 20;
*
* When a video's trusted report count reaches this number, bitvid treats it as
* spam or policy violating content and removes it from default feeds. Align
* the value with your moderation policy — 1 trusted report is the upstream
* the value with your moderation policy — 5 trusted reports is the current
* baseline.
*/
export const DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD = 1;
export const DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD = 5;
/**
* Optional theme accent overrides for light and dark mode.
@@ -56,7 +56,7 @@ To maintain a functional and ethical platform, the following types of content ar
bitvid enforces these guidelines through client-side visibility rules that match the current `moreMenuController` and `userBlocks` behavior:
- **Blurred previews:** When trusted NIP-56 `nudity` reports reach the configured blur threshold (`DEFAULT_BLUR_THRESHOLD` in [`config/instance-config.js`](../config/instance-config.js)), thumbnails render blurred and autoplay previews pause. Every blur includes a “show anyway” override. The upstream configuration currently uses one trusted report, but adjust the export to match your policy.
- **Blurred previews:** When trusted NIP-56 `nudity` reports reach the configured blur threshold (`DEFAULT_BLUR_THRESHOLD` in [`config/instance-config.js`](../config/instance-config.js)), thumbnails render blurred and autoplay previews pause. Every blur includes a “show anyway” override. The hosted default requires three trusted reports.
- **Downranked authors:** Muting a creator adds them to your NIP-51 mute list (kind `10000`). Videos from muted creators are deprioritized but still visible with clear badges so you can reverse the action.
- **Personal blocks:** Blocking a creator adds them to your private block list (`userBlocks`). Videos from blocked creators are hidden from feeds, carousels, and modals unless you explicitly unblock them.
- **Admin hard hides:** Moderator-maintained blacklists (NIP-51 kind `30000`) remove creators from default feeds for everyone using the hosted client. Moderators can still review the content through “show anyway” controls when auditing decisions.
@@ -88,7 +88,7 @@ You can manage moderation directly from the client using the existing menus rend
### **5.5 Safety & Moderation thresholds**
- Open your profile avatar → **Profile** → **Safety & Moderation** to adjust blur and autoplay thresholds. Leave a field blank to return to the defaults exported from [`config/instance-config.js`](../config/instance-config.js) (`DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD`). The upstream repo ships example values of 1 trusted report for blur, 1 for autoplay blocking, 20 trusted mutes to hide authors, and 1 trusted spam report to hide videos—tune the config file before deployment if your community needs different baselines.
- Open your profile avatar → **Profile** → **Safety & Moderation** to adjust blur and autoplay thresholds. Leave a field blank to return to the defaults exported from [`config/instance-config.js`](../config/instance-config.js) (`DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD`). The hosted defaults require 3 trusted reports for blur and autoplay blocking, 20 trusted mutes to hide authors, and 5 trusted spam reports to hide videos—tune the config file before deployment if your community needs different baselines.
- These controls update immediately and sync with the moderation overlays applied by `moreMenuController` and `VideoCard` components. Operators can edit the config file to change the baseline before deploying their own instance.
## **6. Appeals and Feedback**
@@ -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.
@@ -0,0 +1 @@
Six-priority campaign resumed via interpreter; see docs/agents/CAMPAIGN-2026-10-22.md. Scope administrative coordination and evidence-first routing only. No product edits. Definition of done covers all six and final integrated local review with explicit live/manual limits.
@@ -0,0 +1 @@
Client9 at c5b678f7; investigate HEAD owner cancellation only via actual observer/controller and controlled fetch. No product/retained tests/full/build/browser. HLS/torrent remain separate. Success: counts for disposal/shared/rejoin/cache/timers/media and scoped proposal to dispatch.
@@ -0,0 +1,4 @@
- storage-cors-3 resumed by dispatch/campaign; baseline c5b678f7. Read-only product/retained tests, offline synthetic data only.
- Goal: real deletion ingest followed by stale refresh/late older/view switch; preserve unrelated/newer content under existing semantics.
- Seven scratch scenarios: three controls pass, four strict failures. Evidence docs/testing/storage-cors-3-investigation-2026-10-22.md; script /tmp/bitvid-storage-cors-3-audit.mjs.
- Await independent reproduction/precise ownership routing. No browser/full/build/normal submission or live services authorized for this investigation.
@@ -0,0 +1 @@
Client9 HEAD-only candidate ready: controller cancel hook/abort+raced prompt settlement/timer identity cleanup;23new HEAD+37existing scenarios pass,16failing isolated mutants/cancelled0,lint11 pass. No existing test expectation edits. Explicit strict contract/integrity notes. Next commit candidate, merge current unstable, request full/browser slot; do not submit or run expensive jobs without grant. HLS/torrent separately open.
@@ -0,0 +1,5 @@
- storage-cors-4: prevent obsolete registered feed results repainting deleted/replaced DOM.
- Baseline26e9a787; storage3 durable63e2b519 normalgateexit0, ancestry verified; close3 after durable lookup.
- Own coordinator only plus unique tests/notes. No app/view/engine/service/buffer edits.
- Plan: retain actual registered pipeline/JSDOM baseline; guard refresh/load ownership; cover async runtime/engine success+error, disposal/remount, telemetry and current controls; kill restored mutants; focused/lint; request serialized normal gate; verify durable landing.
- Definition of done: strict regressions+mutants, existing focused/lint, merged normal gate, durable status+ancestry. No live/browser qualification inferred.
@@ -0,0 +1 @@
Client9 HEAD-only fix durably landed f41ac656 after normal435-filegate exit0, ancestry0; sequential build/artifact exit0. Heavy slot released. Await separate bounded native browser slot; no browser started. HLS/torrent/RangeGET and wholeStage3 remain open. No additional product changes.
@@ -0,0 +1 @@
HEAD slice complete locally: durablef41ac656 normal435unique/exit0/ancestry0; build/artifact0; controlled native Chromium+Firefox lifecycle pass with synthetic media completion/public-deny. Browser/heavy resources released. Pending docs-only evidence landing; HLS/torrent/RangeGET remain open, await bounded routing/HEAD closure disposition.
@@ -0,0 +1 @@
Item9 HEAD slice done/landedf41ac656. Read-only HLS diagnostic atd04d501e actualobserver/controller/helper reproduces5cancellationfailures/7controls pass/0cancelled/naturalexit1. Pending manifest GET/body retains timer and promise after final-owner disposal; rejoin original,late cache. Need dispatch explicit controller+HLShelper ownership/contract before edits. No browser/full/build/product/retainedtest changes.
@@ -0,0 +1 @@
HLS-only implementation perdispatch20:55:13Z/confirmed newestRULE. Controller passes optional lifecycle; manifesthelper cancellation covers fetch+body, synchronous invalidation/promptcancelled/abort/identitysafehook+timercleanup. No engine/player/session/HEAD/media/torrent/RangeGET changes. New35tests+adjacent94=129pass/6files;19isolatedmutantsfailnaturally/cancelled0. Final lint pending; record then commit+merge currentunstable,READY for serialized full/browser/build/normal queue. No expensive stage/submission without grant.
@@ -0,0 +1 @@
Client9 resumed at5f8e58be, existing item reclaimed. Runtime identical unstable and prior146076dd handoff;4 browser evidence docs retained. Current narrow goal: diagnose original Firefox HLS body-entry prerequisite using preserved unexecuted d0100521 instrumented scratch, only after fresh dispatch slot+cooling clearance. No product/test edits or broader cancellation work. Preserve all failures and strict5000ms bound; no repeat full/build needed for unchanged runtime. See matching test log; dispatch blocker sent.
@@ -0,0 +1,4 @@
Goal: reconcile existing HLS evidence and recommend remaining lifecycle boundary under interpreter13:41 routing.
Success: preserve prior failure/control results, verify current source ancestry/equality and raw evidence hash, commit existing dirty notes, send scoped recommendation.
Non-goals: no product/tests/browser/full/build/submission, no live or wholeStage3 claim; QA owns sole browser.
See matching reconciled test log. Next: await explicit offline active-torrent investigation routing.
@@ -0,0 +1,4 @@
Goal: interpreter-authorized current-HEAD offline active torrent lifecycle investigation, no implementation.
Baseline HEAD e9af76df, real observer/grid/queue/probePeers, fake WebTorrent IO+clock.
Result: corrected12cases7pass5fail/naturalexit1; cleanup0remaining resources; existing lifecycle11/11 naturalexit0. See matching TEST_LOG.
Next: dispatch independent reproduction and explicit gridHealth/probePeers-only ownership plus spec correction before fixes/tests. QA owns browser; no full/build/submission.
@@ -0,0 +1 @@
Approved dispatch routing read locally: actual baseline independently reproduced12=7pass5fail, no repeat needed. Scope gridHealth+optional probePeers lifecycle only. Goal final-owner null settlement/resource cleanup/fresh identity; preserve shared/standalone/live/cap4/FIFO/TTL/deferred destroy safety. Spec and integrity written first. Focused offline/mutants/targeted lint allowed; no full/build/browser/submission until resource grant.
@@ -0,0 +1,7 @@
# Client10 goal and scope
- Authority exact dispatch ROUTING-2026-10-27-client10-animation-cleanup.md read; sourcef01ce012 after safe ff, untracked client9 note preserved.
- Goal recover already-finished transient interface fade/delay classes during delayed cleanup init; preserve active/future interface animation and thumbnail end/cancel semantics. Existing listener-only cleanup cannot observe an event already dispatched.
- Done now: minimal index block extraction + helper, scenario/wiring regressions, killing mutants for all cases, lint under freshhealth. Fullgate/landing/build/nativebrowser are later explicit slots. No other bootstrap/CSS/app edits or oldoverflow closure.
- Plan: create/claim10; record contract/testintegrity; extract existing cleanup; reconcile finished own-target CSS interface animations with no stillactive counterpart; test real helper and extracted bootstrap wiring; isolated mutants; lint; namedpathcommit/merge and READY only, no submit.
- getAnimations/finished/fill reference https://www.w3.org/TR/web-animations-1/; finished forwards effects remain relevant and can retain stacking context. Avoid sleeps, timers, layout reads, arbitrary phase-duration guesses and broad class stripping.
+306 -36
View File
@@ -751,6 +751,68 @@
transition-property: border-color, box-shadow, transform;
}
.site-update-notice {
@apply fixed bottom-5 right-5 z-50 flex max-w-sm flex-col items-end gap-3;
}
.site-update-notice__button {
@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;
}
.site-update-notice__close {
@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;
@@ -880,6 +942,29 @@
position: relative;
}
/* Maps the <bitlogin-auth> widget's own theming hooks onto bitvid's design
tokens (components/login-modal.html), so it reads as part of this site
rather than a foreign embed, and follows the light/dark theme toggle
automatically since every token below is itself theme-aware. */
bitlogin-auth {
display: block;
--bl-accent: var(--color-accent-strong);
--bl-accent-hover: var(--color-accent);
--bl-accent-fg: rgb(255 255 255);
--bl-bg: var(--surface-panel);
--bl-fg: var(--text-primary);
--bl-muted: var(--text-muted);
--bl-border: var(--border-default);
--bl-input-bg: var(--surface-sunken);
--bl-danger: var(--status-danger);
--bl-danger-bg: var(--status-danger-surface);
--bl-warn: var(--status-warning);
--bl-warn-bg: var(--status-warning-surface);
--bl-radius: var(--radius-lg);
--bl-font-family: inherit;
--bl-max-width: none;
}
.btn-ghost[data-state="active"],
.btn-ghost[aria-pressed="true"] {
@apply border-transparent bg-primary text-white shadow-md;
@@ -944,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 {
@@ -952,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"] {
@@ -996,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%,
@@ -1487,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;
@@ -3553,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 {
@@ -3733,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")) {
@@ -4491,6 +4716,39 @@
-webkit-overflow-scrolling: touch;
}
/* The player owns a single scroll region. On mobile, `100vh` can extend
underneath the browser controls/home indicator, which previously made the
final composer action hard to reach. Prefer the dynamic viewport and keep
scrolling on the panel (where the sticky player navigation listens). */
@media (max-width: calc(theme("screens.lg") - 0.02px)) {
#playerModal {
height: 100vh;
height: 100dvh;
min-height: 100vh;
min-height: 100dvh;
overflow: hidden;
}
#playerModal > .bv-modal__panel {
block-size: 100%;
min-block-size: 0;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
#playerModal .player-modal__container {
block-size: auto;
min-block-size: 100%;
}
#playerModal .comment-composer {
padding-bottom: max(var(--space-lg), env(safe-area-inset-bottom));
scroll-margin-block-end: max(var(--space-lg), env(safe-area-inset-bottom));
}
}
#modalVideo {
width: 100%;
aspect-ratio: 16 / 9;
@@ -6662,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.
@@ -0,0 +1,5 @@
- Reuse3 existing workers; disjoint investigations are useful, no extra agent justified yet.
- Firefox gets browser/expensive slot first; cancellation and tombstone evidence use offline focused checks.
- All6 priorities have durable accountable items; staged means unfinished, not cancelled.
- Workers synchronize own clean branches; dispatch never alters peer worktrees.
- No tests rerun for administrative docs; historical gates remain historical.
@@ -0,0 +1,3 @@
- HEAD slice only. Current media cancellation hook does not cover fetch; reproduce real ownership flow before proposing hook reuse.
- Do not change product/tests without dispatch contract routing. Keep HLS/torrent and wide Stage3 completion open.
- No redundant full/lint/build/browser for scratch investigation; QA slot untouched.
@@ -0,0 +1,4 @@
- Treat kind30078 deleted markers using current timestamp semantics; do not infer new NIP09 or root/author ownership policy.
- Use actual subscribe/buffer/client/service with fake transport and synthetic verifier; retain real filtered localStorage writes. Stub unrelated network hydration and IndexedDB scheduling only.
- Capture actual coordinator render boundary with deferred engine decorator; explicitly leave full UI/DOM qualification open.
- Route discovered shared service/buffer/coordinator changes before edits. No upload/cloud/CORS code changes.
@@ -0,0 +1,4 @@
- Dispatch independent audit confirmed7=3pass/4fail and routed buffer/service only; merged routing076ec802 after evidence67140d89, merged HEAD89c5494a.
- Buffer defers deletion of active versions to existing client.recordTombstone; preserve guarded-event same-ID cleanup. No new deletion timestamp/key policy.
- Service rejects deleted/invalid/client-tombstone-covered objects before author/access bypass and direct caching; prune affected cached copies using authoritative guard plus existing explicit deletion IDs. Persist cleaned optimistic snapshots and empty active results.
- Separate buffer commit ed5fb649; service commit follows. No app/feed coordinator/client.js writes. Coordinator race remains open pending faithful registered-pipeline/view/disposal evidence and new routing.
@@ -0,0 +1,6 @@
- Per-app WeakMap request and loader tokens avoid exposing a public API. Capture actual view, mounted container and telemetry context; only eligible completion may publish metadata/map/render/telemetry/impressions/similar content.
- Capture mounted view.container (not mount adapter return); existing lightweight adapters need not expose a container. Real destroyed view loses its container and cannot be resurrected by completion.
- Preserve started engine caller result: stale success resolves original videos/metadata; stale engine error resolves existing fallback/error metadata. loadFeedVideos still resolves undefined.
- A request superseded while awaiting history preload never starts engine: resolve {videos: fallback, metadata: {reason, engine: "superseded"}}. This explicitly distinguishes a skipped run while preserving the resolved payload shape, and avoids new obsolete engine-stage subscriptions. No changes to current valid failure/unavailable fallback or content policies.
- Old service callbacks and recommendation activation must also match the original load/view/context, including a same-feed replacement. Loader continuations recheck after each await before state writes.
- No existing assertions changed. New failure injection wraps actual engine execution and rejects only after production stages, since moderation subscription rejection itself is deliberately caught by the production stage.
@@ -0,0 +1,4 @@
- Explicit dispatch20:55:13Z Stage3 HLS scope extension and helper/controller ownership; keep broaderitem9OPEN. Existing HEAD/media/engine/player/nativepreference behavior and standalone manifest callers preserved.
- Pass optional lifecycle as third helper argument; helper owns its own hook/controller/timer through fetch AND response.text, races logical cancellation independently of AbortController cooperation. Native media remains on existing path.
- Test actual observer/controller/cache/helper with controlled IO/clock boundaries, new unique suite only; existing tests/expectations unchanged. Baseline35newcases13pass/22fail before fix (isolated copies); original12mode5fail/7pass retained.
- Mutants isolate copies in/tmp,never mutate worktree/shared modules. Initial retainedtimer mutant hit engineclearTimeout outside manifesthelper;corrected exacthelper suffix and final19allfail naturally. Formatting outside manifesthelper restored to keep engine read-only.
@@ -0,0 +1,5 @@
- Preserve seven existing dirty notes verbatim alongside a new reconciliation, rather than rewriting old failed/incomplete results.
- Treat separate Firefox prefix3/3 as controlled qualification; original header-onlyFAIL remains separate. No speculative product fix or repeated browser.
- Use actual product SHA83fd2495 for ancestry; c95abe22 is a build identifier.
- Next recommended investigation is active torrent final-owner disposal under item9/priority2, with real production chain and controlled IO; source hints alone are not reproduced defect or implementation authority.
- Notes-only changes need source/hash/whitespace verification, no redundant full/lint/build. Commit evidence locally; no queue submission while source-only routing holds.
@@ -0,0 +1,5 @@
- Investigation uses actual production chain, fake only observer delivery/WebTorrent IO/timers/persistence; no implementation or committed tests.
- Preserve first12=5pass/7fail fixture: two control failures are JSDOM storage-event timers; separate memoryStorage IO fixture12=7pass/5fail retains identical assertions and original evidence.
- Treat five failures as planned Stage3 active-cancellation gap, not regression against existing intentional active-sharing/cache contracts. Need explicit routed spec correction before changing those tests.
- Smallest fix boundary gridHealth ownership/identity/cache plus probePeers lifecycle only. Preserve deferred temporary-torrent destruction after callback unwinds and release queue slot after teardown; retain shared probe client/other owners.
- No live browser/WebTorrent/network/CPU/RangeGET claim; no product edits until independent routing.
@@ -0,0 +1,6 @@
- Grid publishes owner/release/inflight promise before synchronous client IO so disposal or same-source registration inside constructor/add is safe; cancelled identity removed synchronously and cache guarded.
- Optional third probePeers lifecycle follows existing owner-record convention; standalone behavior unchanged. Cancel settlesnull after deferred temporary-torrent destroy call returns/throws; timers clear immediately, cancel hook/owned listeners removed at final teardown.
- Retain inert error handler through deferred teardown, even cancellation insideadd; repeated/late callbacks are settled guards. Shared probeClient remains alive; queue slot only releases after destroy, preserving event unwind/cap4/FIFO.
- Original tests367/417 contract corrected only after routed spec+integrity note; preserve separate shared-live/cache/TTL/timeout controls and strict noobsoletecache/freshrequest/resource assertions.
- No live WebTorrent transport/byte teardown qualification from fake IO. Async low-level network shutdown not measured; existing teardown call ordering preserved.
- Stop at completedfocused boundary for requested kit restart; no commit/merge/submission before restart, all snapshots saved.
@@ -0,0 +1,7 @@
# Client10 decisions
- Extract only existing animationcleanup block into59line ui/fadeInCleanup helper; index1456→1405lines. No other bootstrap/CSS/app files changed. Existing thumbnail eligibility retained on end/cancel; pseudo effects are excluded from element cleanup.
- Install one capture listenerpair then query Document.getAnimations once for already-finished own-target interface CSS animations. Skip running/paused/idle effects and finished effects with same-target live counterpart. No elapsedtime guesses/timers/MutationObserver/layout reads/promises to retain.
- Unsupported animation inspection retains event-based cleanup; reducedmotion animation:none retains original hooks without animation effects or new motion. No unsupported-browser late-recovery guarantee. Reference https://www.w3.org/TR/web-animations-1/ describes relevant finished/filling effects and stacking contexts.
- Per-document WeakMap makes repeated init idempotent and gives safe explicit disposal/reinstall. Stale disposer cannot erase replacement registration.
- Focused scenarios use real helper/JSDOM and controlled CSSAnimation metadata; wiring executes real isolated indexcleanup statements with actual imported helper, not fullapp/bootstrap dependencies. Native CSS animation/hit-testing requires later granted QA. Preserve originaloverflow362/360+394/390 and live/wholeStage3 boundaries.
+522
View File
@@ -0,0 +1,522 @@
# BitVid BitUnlock permission-layer plan
Status: proposed application architecture and implementation plan, 2026-07-23.
BitVid should evolve from a public Nostr video client into a creator-controlled media platform that
supports public media, purchases, rentals, creator memberships, platform subscriptions, bundles, and
pay-per-view access. BitUnlock is the reusable payment-verification and permission layer beneath those
features.
The architectural rule is:
> **BitVid owns media behavior. BitUnlock owns payment-backed permission.**
Related projects and documents:
- BitUnlock `docs/STREAMING_SUBSCRIPTION_ACCESS_PROFILE.md` — generic entitlement and streaming-access profile.
- BitUnlock `docs/GATED_ACCESS_PLATFORM_PLAN.md` — renewable/revocable entitlements and relying-party verification.
- BitUnlock `docs/ACCESS_GRANTS_PLAN_V2.md` — bring-your-own-storage capability patterns.
- BitUnlock `docs/SECURITY_AUDIT_AND_RESILIENCE_PLAN.md` — security, migration, backup, and recovery gates.
- BitVid [`nostr-event-schemas.md`](./nostr-event-schemas.md) — current NIP-71 event behavior.
- BitVid [`playback-fallback.md`](./playback-fallback.md) — current URL/WebTorrent playback behavior.
## 1. Product direction
BitVid remains a Nostr-native, creator-controlled video application, but its access model expands from
"public or hidden" into a general policy system.
BitVid should support:
- public videos;
- login-only videos;
- permanent individual purchases;
- time-limited rentals;
- creator memberships;
- platform-wide subscription catalogs;
- bundles, seasons, courses, and collection passes;
- pay-per-view premieres and livestreams;
- patron/supporter tiers;
- free previews and delayed public release;
- future prepaid or metered viewing where economically appropriate.
A single video may accept several routes. For example, a viewer may qualify through an individual
purchase, an active rental, a creator membership, or a platform subscription.
## 2. Responsibility boundary
### BitVid owns
- NIP-71 publishing and compatibility;
- channels, catalogs, playlists, seasons, bundles, and collections;
- search, discovery, feeds, recommendations, and moderation;
- profiles, watch history, favorites, and parental controls;
- access-policy authoring and resource-to-product mapping;
- checkout presentation inside the BitVid UI;
- playback-session creation and renewal;
- HLS/DASH manifests, MP4 range behavior, WebTorrent policy, storage, CDN, and origin integration;
- devices, concurrent streams, household rules, and application sessions;
- creator dashboards, pricing presentation, support, refunds, and catalog management.
### BitUnlock owns
- product registration and policy discovery;
- Lightning/NWC payment-plan creation;
- authoritative settlement verification;
- partial-payment recovery, retries, and idempotency;
- fulfillment and buyer-bound signed entitlement issuance;
- perpetual and term access semantics;
- renewal, expiration, revocation, and buyer recovery;
- generic relying-party verification;
- optional storage-capability issuance for static sellers;
- SDK, schemas, vectors, sandbox, and normalized errors.
### BitRoad's role
BitRoad may list and sell the same products, but BitRoad is not a runtime dependency for playback.
Purchases made through BitRoad and purchases initiated inside BitVid should produce compatible
BitUnlock entitlements that any compatible BitVid client can verify.
## 3. Access modes
### 3.1 Public
The video is playable by any NIP-71 client. No BitUnlock product is required.
### 3.2 Login-only
The video remains free, but BitVid requires a proven Nostr identity before playback. This can support
private communities or account features without payment.
### 3.3 Permanent purchase
The viewer buys a video or catalog item once and receives a perpetual BitUnlock entitlement. BitVid
may issue short-lived playback sessions each time, but the entitlement itself does not expire.
### 3.4 Rental
The viewer buys a term entitlement. The first MVP should use purchase-relative expiry because that is
already supported by BitUnlock. A future first-play-relative rental requires a separate activation
record and must not be inferred from payment alone.
### 3.5 Creator membership
A creator defines one or more membership products. BitVid maps each membership tier to a changing set
of videos and benefits.
Example tiers:
- Supporter — early access and badge;
- Member — premium catalog;
- Producer — premium catalog, downloads, and private livestreams.
BitUnlock verifies the tier entitlement; BitVid owns the current benefit mapping.
### 3.6 Platform subscription
BitVid defines platform-wide plans such as Basic, Premium, Family, Sports, or Annual. The active plan
entitlement grants access to application-defined catalog tiers.
BitUnlock does not need to know which videos are in each tier. BitVid resolves the catalog policy.
### 3.7 Bundle or catalog pass
One entitlement may unlock a season, trilogy, course, conference archive, creator bundle, or topical
collection.
### 3.8 Pay-per-view and livestream admission
A term entitlement may authorize a premiere, live event, conference, or limited screening, with an
optional replay window.
### 3.9 Delayed public release
A video may initially require an entitlement and become public at a specified time. BitVid evaluates
the release timestamp before invoking paid-access checks.
### 3.10 Metered viewing
Pay-per-minute or pay-per-stream is deferred until BitUnlock supports an appropriate prepaid rail and
batched fee accounting. BitVid should not create a Lightning invoice or minimum service fee for every
small playback action.
## 4. Resource access policy
BitVid should represent access as an application-owned OR-of-routes policy.
Conceptual shape:
```json
{
"version": "bitvid-access-policy-v1",
"mode": "any",
"routes": [
{
"kind": "public-after",
"at": 1798761600
},
{
"kind": "bitunlock-entitlement",
"servicePubkey": "<pinned-service-pubkey>",
"productCoordinate": "<individual-purchase>"
},
{
"kind": "bitunlock-entitlement",
"servicePubkey": "<pinned-service-pubkey>",
"productCoordinate": "<creator-membership>",
"requireCurrentStatus": true
},
{
"kind": "bitunlock-entitlement",
"servicePubkey": "<pinned-service-pubkey>",
"productCoordinate": "<platform-subscription>",
"requireCurrentStatus": true
}
]
}
```
The exact wire format must be reviewed before implementation. The policy must support:
- multiple acceptable entitlement products;
- public or login-only fallback;
- local release times;
- perpetual versus term expectations;
- mandatory online revocation checks where needed;
- future AND conditions for add-ons or region/catalog constraints;
- explicit service-pubkey pinning and versioning.
## 5. NIP-71 compatibility
BitVid must preserve ordinary NIP-71 interoperability.
Recommended approach:
1. Keep the standard NIP-71 video event valid and useful to other clients.
2. Add an optional reference to a separate replaceable BitVid access-policy event, or a minimal
extension tag pointing to that policy.
3. Do not publish permanent private storage URLs, decryption keys, playback tokens, buyer identities,
or subscription state in the public video event.
4. Clients that do not understand the access extension may show metadata and a locked state rather
than receiving a false public playback URL.
5. Public videos continue to carry normal public playback sources.
A separate access-policy event is preferable to embedding a large mutable catalog rule directly in
every video event. It allows membership/catalog mappings to evolve without republishing media
metadata.
## 6. Viewer flow
### 6.1 Discovery
BitVid loads the NIP-71 event and associated access policy. It can render the card, title, creator, and
preview metadata without revealing protected media capabilities.
### 6.2 Authentication
The viewer signs in through BitLogin, NIP-07, or another supported Nostr signer. BitVid treats the
proven pubkey as the account identity.
### 6.3 Entitlement recovery
BitVid loads buyer-held entitlements from:
- local encrypted storage;
- the buyer's relay/vault recovery path;
- BitUnlock's authenticated buyer-recovery endpoint.
The client should cache receipts but re-evaluate expiry and required revocation status before granting
new playback.
### 6.4 Policy evaluation
BitVid checks local routes first, then verifies candidate BitUnlock receipts against:
- pinned BitUnlock service pubkey;
- authenticated buyer pubkey;
- expected product coordinate;
- supported schema and policy versions;
- entitlement term and expiry;
- current revocation status where required.
### 6.5 Checkout
When no route succeeds, BitVid presents applicable purchase choices inside the video UI:
- buy permanently;
- rent;
- join creator membership;
- subscribe to BitVid plan;
- buy event pass.
The BitUnlock SDK manages quote creation, wallet interaction, partial-payment recovery, bounded
polling, unlock, and entitlement validation.
### 6.6 Playback authorization
After a route succeeds, a trusted BitVid backend or media-origin component issues a short-lived
resource-scoped playback capability. The video bytes flow from the storage/CDN/origin to the viewer,
not through BitUnlock.
## 7. Playback architecture
### 7.1 MVP private-origin path
Use a small BitVid authorization service or edge function:
1. Receive authenticated viewer proof and buyer-presented entitlement.
2. Verify the entitlement with the BitUnlock SDK.
3. Evaluate the BitVid resource policy.
4. Create a short-lived playback session.
5. Return a signed manifest URL, signed cookie, or presigned MP4/object URL.
6. Refresh the session during long playback without requiring a new payment.
### 7.2 Capability properties
The playback capability should bind:
- buyer or delegated device identity;
- video/resource ID;
- accepted policy route and entitlement ID;
- origin/audience;
- issue and expiry time;
- session revision or nonce;
- plan quality/download permissions where applicable.
The capability should normally last minutes, even when the subscription lasts a month.
### 7.3 HLS/DASH
The design must account for:
- manifest authorization;
- segment requests after the original URL expires;
- session renewal during long playback;
- seeking and bitrate switching;
- CDN caching without making protected manifests public;
- signed cookies versus per-segment query tokens;
- CORS and CSP among BitVid, the authorization service, and the media origin.
### 7.4 MP4
For MP4 playback, preserve byte-range support and ensure session renewal does not break seeking. A
short-lived signed cookie or origin session may work better than a one-time URL with an expiry shorter
than the video.
### 7.5 WebTorrent
Public WebTorrent distribution is naturally incompatible with revocable private subscription access
once plaintext media is shared to peers. BitVid should:
- keep WebTorrent for public media;
- allow encrypted torrent payloads for permanent purchases where the buyer receives a decryption key;
- not claim that private subscription streams remain revocable after distributing a reusable
decryption key or plaintext torrent.
## 8. Subscription MVP
The first subscription release should intentionally avoid automatic charging and household complexity.
MVP capabilities:
- one creator monthly membership product;
- one BitVid platform monthly plan;
- manual Lightning renewal;
- current plan and expiry display;
- renewal checkout against the same product;
- immutable new entitlement for each period;
- expiration and seller revocation;
- catalog-tier mapping;
- recovery on a new device;
- one active viewer session policy;
- short-lived playback session against a private test origin.
The MVP is complete when a buyer can subscribe, recover the entitlement elsewhere, play an included
video, fail after expiry/revocation, renew, and play again without mutating the old receipt.
## 9. Automatic renewal
Automatic renewal is a later phase requiring an explicit, constrained buyer NWC authorization.
Requirements include:
- merchant and product binding;
- maximum amount;
- minimum billing interval;
- period and cumulative spending limits;
- cancellation and revocation;
- duplicate-charge and replay protection;
- notifications and failed-renewal behavior;
- no seller ability to broaden permission silently.
BitVid must not market subscriptions as automatically recurring until this separate authorization flow
has been implemented and reviewed.
## 10. Devices, profiles, and household access
The root Nostr key should not be copied to every television.
Later phases should introduce:
- buyer-authorized device keys or application sessions;
- device list and remote sign-out;
- delegated scope limited to BitVid;
- maximum devices per plan;
- concurrent stream limits;
- profile records and parental controls;
- household-member authorization without sharing the root key;
- revocation and recovery.
Profiles and household rules remain BitVid application state. BitUnlock only verifies the underlying
subscription entitlement unless a future generic delegated-capability feature is intentionally added.
## 11. Creator and operator experience
Creators should be able to:
- register BitUnlock-backed purchase, rental, membership, and event products;
- connect those products to videos, playlists, or catalog groups;
- define accepted access routes;
- preview the locked viewer experience;
- revoke a term entitlement where policy permits;
- see aggregate commerce state without receiving buyer private keys or sensitive credentials;
- use BitRoad as an optional storefront while keeping BitVid-native checkout available.
Operators should be able to:
- configure the accepted BitUnlock service identity;
- configure a private media origin/CDN;
- control playback-session TTLs and concurrent-session policy;
- suspend new paid access safely during incidents;
- audit authorization failures without logging receipts or playback tokens.
## 12. Privacy and security
- Buyer receipts should remain private and buyer-presented.
- BitVid must not expose a public API allowing arbitrary enumeration of buyer purchases.
- Playback tokens, signed URLs, entitlements, NWC URIs, and storage credentials must not enter logs.
- Client-side verification may improve UX but cannot be the sole control for a private media origin.
- Copied receipts must fail for a different authenticated pubkey.
- Valid receipts for unaccepted products must fail.
- Expired and revoked subscriptions must not mint new sessions.
- Cross-video and cross-channel capability substitution must fail.
- BitUnlock's service identity and accepted schema versions must be pinned.
- BitUnlock must never proxy video bytes.
- Private storage/CDN credentials must remain application-side or narrowly scoped and encrypted.
## 13. Implementation phases
### Phase 0 — Architecture and protocol decisions
- [ ] Freeze the BitVid responsibility boundary with BitUnlock and BitRoad.
- [ ] Define `bitvid-access-policy-v1` and negative vectors.
- [ ] Decide the NIP-71 extension/reference format without breaking ordinary clients.
- [ ] Define resource IDs, catalog groups, product coordinates, and policy versioning.
- [ ] Define public, login-only, permanent, rental, membership, subscription, bundle, event, and
delayed-public routes.
- [ ] Threat-model buyer receipt privacy, copied receipts, cross-resource substitution, and playback
capability leakage.
### Phase 1 — BitUnlock SDK and identity integration
- [ ] Pin BitUnlock service policy and service pubkey.
- [ ] Add BitLogin/Nostr authenticated buyer context where not already available.
- [ ] Integrate entitlement recovery.
- [ ] Integrate `verifyEntitlement()` and revocation/status checks.
- [ ] Add typed access-decision results and normalized user-facing errors.
- [ ] Add unit tests for wrong buyer, wrong product, bad signature, expiry, and revocation.
### Phase 2 — Locked media UX and checkout
- [ ] Render locked cards and video pages without exposing protected playback URLs.
- [ ] Show accepted purchase/rental/subscription choices.
- [ ] Integrate BitUnlock checkout, partial-payment recovery, and completed-purchase recovery.
- [ ] Display current entitlement route, expiry, and renewal state.
- [ ] Accept entitlements produced by BitRoad purchases.
- [ ] Add sandbox fixtures for each access mode.
### Phase 3 — Playback authorization service
- [ ] Create a minimal trusted BitVid authorization service/edge function.
- [ ] Verify buyer authentication and presented entitlements server-side.
- [ ] Evaluate `bitvid-access-policy-v1`.
- [ ] Issue short-lived resource-scoped playback sessions.
- [ ] Integrate a private test origin with signed URL/cookie support.
- [ ] Add renewal during long playback and safe error recovery.
- [ ] Test cross-video, replay, expiry, and token-leak scenarios.
### Phase 4 — Subscription MVP
- [ ] Register one creator membership and one platform subscription product.
- [ ] Map subscription products to catalog groups.
- [ ] Add manual renewal UX.
- [ ] Demonstrate expiry, revocation, renewal, and new-device recovery.
- [ ] Add a subscription-management view.
- [ ] Enforce an initial concurrent-session policy.
- [ ] Complete a real mainnet end-to-end subscription and playback test after sandbox proof.
### Phase 5 — Purchases, rentals, bundles, and events
- [ ] Permanent individual video purchase.
- [ ] Purchase-relative rental.
- [ ] Bundle/season/course pass.
- [ ] Pay-per-view and replay window.
- [ ] Delayed public release.
- [ ] Creator tier benefits beyond playback.
### Phase 6 — Production streaming
- [ ] Choose HLS/DASH/CDN architecture.
- [ ] Implement manifest and segment authorization.
- [ ] Preserve MP4 range and seek behavior.
- [ ] Define CORS, CSP, cache, and origin rules.
- [ ] Add playback-session observability without leaking viewing details.
- [ ] Load-test authorization and renewal paths.
- [ ] Add incident suspension and recovery procedures.
### Phase 7 — Devices and households
- [ ] Delegated device keys or application sessions.
- [ ] Device list, revocation, and remote sign-out.
- [ ] Concurrent-stream limits by plan.
- [ ] Profiles and parental controls.
- [ ] Household-member authorization.
- [ ] Recovery without copying the root Nostr private key.
### Phase 8 — Advanced billing
- [ ] Design and audit restricted automatic NWC renewal.
- [ ] Trials, promotions, upgrades, downgrades, and plan transitions.
- [ ] Cashu/prepaid or batched metered viewing after BitUnlock fee accrual is ready.
- [ ] Creator revenue and platform-fee reporting without making BitVid custodial.
## 14. Acceptance criteria
The first complete reference implementation must demonstrate:
1. A standard public NIP-71 video still works in ordinary clients.
2. A protected video reveals no permanent private-media URL publicly.
3. A buyer purchases a monthly BitUnlock subscription inside BitVid.
4. An independently versioned BitVid authorization component verifies the entitlement.
5. The component issues a five-minute capability for the correct video.
6. The media origin serves bytes directly without BitUnlock proxying them.
7. A copied entitlement fails for another pubkey.
8. A valid but unrelated entitlement fails.
9. Expiry and revocation prevent new playback sessions.
10. Manual renewal creates a new entitlement period and restores access.
11. The buyer recovers access on a new device without repaying.
12. An individual permanent purchase is accepted as an alternate route to the same video.
13. A BitRoad-originated purchase works without BitRoad being present during playback.
14. Logs and analytics contain no entitlement plaintext, NWC secrets, storage credentials, or
playback bearer tokens.
## 15. Explicit non-goals for the first release
- Automatic recurring charges.
- Full Netflix-style household detection.
- Strong DRM guarantees against screen capture or authorized-user copying.
- Private plaintext WebTorrent subscription streaming.
- Pay-per-minute billing.
- Geographic licensing enforcement.
- Recommendation-system redesign.
- Building BitUnlock-specific payment or entitlement logic directly into BitVid instead of using the
public SDK and generic contracts.
+19
View File
@@ -4,6 +4,7 @@ Welcome to the bitvid documentation. This directory contains detailed architectu
## Core Systems
- **[BitUnlock Permission Layer](./BITUNLOCK_PERMISSION_LAYER_PLAN.md)**: Architecture and phased plan for purchases, rentals, creator memberships, platform subscriptions, bundles, events, and short-lived playback authorization using BitUnlock as BitVid's permission layer.
- **[Application Coordinators](./app-coordinators.md)**: Overview of the coordinator pattern used to manage app-wide state and services.
- **[Data-Flow Refactor — Dev Plan & Spec](./architecture-refactor.md)**: The incremental plan to put all subsystems on a shared, governed relay/data foundation (SubscriptionManager, stores, unidirectional flow).
- **[Known Bugs (deferred)](./KNOWN_BUGS.md)**: Confirmed defects/limitations found during the refactor, queued to fix later.
@@ -30,10 +31,28 @@ 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.
- **[Deployment Notes](./deployment-notes.md)**: Notes and checklists for deploying bitvid.
- **[Manual QA Checklist](./qa.md)**: The standard manual QA script for releases.
## 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).
+147
View File
@@ -0,0 +1,147 @@
# TODO — what each area did NOT finish
## dispatch
- 2026-10-23 buildc95abe22/artifactpass at103eb04c;SWcompatcheckSKIPPED Gitdelta unavailable,retainlimit. HLSevidencea482d9d2 independentlydurable. QA4 realappready360,Uploadclickblockedfirst-run disclaimer30s,no geometry/focus/touch;sourceexactinterceptclass maps components/disclaimer.html18,runneromitsaccept. QA genuineUIacceptpreponly/no productbugclaim; HLSnativebrowser solegrant13:04:31. FreshpowerACADonline1/BAT100%Discharging;no lowbatterythreshold,resourceadvisoryreviewed.
- 2026-10-23 HLS product83fd2495 durable independentlystatus/log/ancestry; current savedfulllog independently437starts/437unique,lastzap-totals;normalgateexit0. StorageRangeaudit103eb04c durable. Client solebuild/artifact granted; QA4 conditionalhostbrowser afterbuildrelease+freshhealth, HLSnativebrowser later. Priorpublish/thermalrejections+398partial+firstfull-logloss preserved. Torrent/Rangeimplementation/FirefoxCI/Amber/mobile/previewintegration remainopen.
- 2026-10-23 HLS replacement280f043a durablyREJECTED after resource-triggered own-gateSIGINT, worker sustained88.8→85.1C and398/437unique partial/lastvideo-modal-controllers interrupted. No fullgreen/productassertionfailure claim. Interpreter kitRELEASE12:46:56 reports84pass/9guards/upstreambeta-onlypush; exactstart/end requested to assess overlap before nextlonggate. No HLSretry/build/browser until cooling/no competingjob assessment; normalgate unchanged/required.
- 2026-10-23 resume: actual QA4 bootEPERM/release consumed; browser/runtimechecks0, prior mobile strict failures unchanged. HLS sole full/normal grant12:32:43Z at clean79a9a130 after safe merge latest672ca953; START/results pending. Health cooling77→71C/RAM68.3%/swap90% advisory; one heavy max. QA host escalation disposition/browser grant later; no app-fix/cron/autoapproval-effect claim. Overnight client wake left composer yet unread55642s, recovery now consumed.
- 2026-10-22 storage3+4DONE: storage4 product7edc7ea8/closure8317da4d independently durable status/log/ancestry; worker normalgateexit0/discovery436/build29cbe6f0 artifactPASS, no independent436start audit. QA4 now sole bounded mobile browser grant21:04:02; HLS candidatecdc398b0/merged79a9a130 next full after QA release. Dispatch exact candidate35/35 natural321ms; worker129focused/19mutants/lint11. HLS gates/preview/campaign remain unfinished.
- 2026-10-22 HLS follow-on: dispatch independent actual observer/controller/helper scratch atd04d501e12=7pass/5fail/cancelled0/naturalexit1/165.17ms. Final-owner GET/timer/promise retained, rejoin stale, late healthy cache, body-read disposal fail; no media-slot leak. Controller HLS handoff + optional manifest-helper cancellation only routed with strict spec/integrity/regressions. Focused/mutants/lint only while storage4 holds heavy slot; torrent/RangeGET remain separate, no HLS fix landed.
- 2026-10-22 HEAD slice productf41ac656/evidenced04d501e durable status/log/ancestry verified. Worker normal435unique/buildartifact/nativeChromium+Firefox controlledHEAD pass; loadeddata synthetic, no decoder/transport-byte proof. All client resources released20:51:23Z. Storage4 sole full/normal START ACK20:52:16; client9 HLS read-only measurement ACK, broader9 remainsOPEN; QA4 HOLD. Integrated preview and other campaign priorities remain unfinished.
- 2026-10-22 client9 product normally landedf41ac656: dispatch durable status/log/ancestry0, worker gateexit0/435unique starts. Sequential build active, separate bounded HEAD browser after build conditionally allocated; results pending. Storage4 candidateb21ce86c focused16/9mutants/lint11 ready, next full slot after client release; QA4 held. Integrated preview and broader campaign gates unfinished.
- 2026-10-22 restart20:41UTC: a79d49df verified durably landed26e9a787, queue0. Fleet health cooling74 to66C/load3.81 to3.30, >20GiB available RAM, swap52 to51% sole suggested-limit crossing. Client9 sole full/normal slot granted; START/results pending. Storage4/QA4 held. Own sync blocked read-only Git metadata; no new product/tests/build/browser qualification.
- 2026-10-22 storage4 registeredfeed/JSDOM independent4=2pass/2fail:obsolete result restores deletedDOM/overwrites replacementExplore;coordinator-only separate fix routed afterstorage3 submit. Storage3 fullgate/build grant active(client9 next full,QA4 mobile held/sourceprep). Client9 exactcandidate independent23pass. No product landing yet.
- 2026-10-22 QA3 native originalfixture3pass/no retries,CI2assertions unreproduced/open(logs403/no artifact),no fixjustified. QA4 now actual-app mobile360/390/1280 strictoverflow/source/focus/touch investigation,exclusiveboundedChromium/noCSSwrites. Remaining6priority campaign not complete.
- 2026-10-22 client9 HEAD independently11modes(3fail/8controls pass),finalowner fetch/timer retained(no media slot leak);controller HEAD-only fix+strict contract/identity/ignoringabort regressions routed. HLS/torrent/RangeGET remain open;QA offline genuineBuffer rebuild prep, mobile/Amber staged.
- 2026-10-22 storage3 independent7/3pass/4fail: buffer newer-version loss and service deleted snapshot/cache leakage routed separate small commits; coordinator stale render still open/read-only pending registered lifecycle evidence. QA3 app-less3pass not CI proof; genuine cached Buffer followup granted exclusive narrow browser.
- 2026-10-22 resumed six-priority campaign: Firefox QA3, active cancellation client9, tombstones storage3 now; playback dispatch4, Amber dispatch5, mobile dispatch6 staged with acceptance criteria. See docs/agents/CAMPAIGN-2026-10-22.md. All six remain unfinished; old idle/quiet-hours hold superseded for this foreground scope. Final integrated checks/build/preview and live/manual disclosure outstanding.
- 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-27 client10 DONE: product5b0adf6b normal439gate exit0/ancestry0, exactcandidateQA native4flowPASS peerattributed, build+artifact86fc5236 PASS/helpercmp0. Required build/landing evidence23c69eba durablyaf8e6540/docs-only/ancestry0; closed after durableevidence. AllslotsRELEASED, no artifactpublication/preview/deploy. Prioroverflow362/360+394/390, nativeactivecancel/visualAA/item9live/CPU/wholeStage3 remain separate/open. No new task/runtime self-selected.
- 2026-10-27 01:54UTC: client10 separate localbuild+artifactverifierPASS/naturalexit0/version86fc5236/indexSHA50a89bd6/helperdistbyteequalcmp0. No trackedartifactdelta/preview/deploy. SolebuildslotRELEASED; land build+normal439gate evidence docs-only then close10; historicaloverflow/nativeactivecancel/AA/item9live limits retained. test_logs/TEST_LOG_2026-10-27_01-54-00_client-reliability-10-build.md.
- 2026-10-27 01:52UTC: client10 productLANDED5b0adf6b via exactaff88416 normal439gate exit0/discovery439unique/missingextra0/ancestry0. QA exactcandidate nativepoint acceptance PASS attributeddispatch/QA; alloldoverflow/activecancelnative/AA/live limits retained. Completegateevidence state/evidence/client10-animation-cleanup-20261027; slotRELEASED. Separate remainingbuild/artifact/native routing pending, item10open. test_logs/TEST_LOG_2026-10-27_01-52-00_client-reliability-10-landed.md.
- 2026-10-27 client10 scoped cleanup READY: late-init recovery extracted from index animation block to59line helper;17newscenarios pass/19distinct killedmutants/all17covered/lint11pass. Existing tests/CSS/app/bootstrap untouched outside block/import. No full/build/browser/submission grant; native360point+390/1280/reducedmotion and normalgate/landing remain. Preserve prior362/360+394/390 overflow and item9live/native limits. Evidence test_logs/TEST_LOG_2026-10-27_01-39-00_client-reliability-10-ready.md.
- 2026-10-26 19:43UTC: separately granted local build/artifact passes exit0 at7594c54d (runtimeaf61c271); artifact15868a91/indexSHA9c48e0fe, clean tree, no previewserver/deploy. Sandbox build swcompat history probe skipped; prior unchanged-input hostlint11pass retained. Slot RELEASED; native torrent/browser+QA timing need distinct routing, item9 staysopen. test_logs/TEST_LOG_2026-10-26_19-43-00_client-reliability-9-build.md.
- 2026-10-26 19:41UTC: item9 active-torrent product LANDED af61c271 via normal eafa364d queue gate exit0; independent438discovered/438unique starts/missingextra0 and ancestry0. Complete log/result/discovery hashed under state/evidence/client9-torrent-validation-20261026, prior rejected275partial intact. lint11/savedfocused60/mutants19 retained. Normalgate slot RELEASED; separate build/native-browser routing remains, item9 open. No real peers/byte teardown/wholeStage3 claim. See test_logs/TEST_LOG_2026-10-26_19-41-00_client-reliability-9-torrent-landed.md.
- 2026-10-26 19:16UTC: item9 full lint11PASS; normal queue c20635bc interrupted for sustained90.1C→86C over38s. Durable rejected/red exit1, unstable04c8c7cf unchanged, not landed. Partial275/438starts/275unique,163notstarted; original log/result preserved in state/evidence/client9-torrent-validation-20261026. Slot RELEASED, fresh resource+new gate grant required; no auto-resubmit/build/browser. Evidence test_logs/TEST_LOG_2026-10-26_19-16-00_client-reliability-9-thermal-interruption.md.
- 2026-10-26 resumed item9 active-torrent candidate `1b2362d2`, merged unstable `04c8c7cf` at `1bf50c1f`; exact saved hashes/focused60/60/19distinct killed mutants preserved. Remaining full lint/full/build/browser/normal landing require dispatch slot plus fresh health; TCPU97.1C at19:07:29 exceeds85C. No new tests or submission. Hold recorded and blocker mailed; real peers/byte teardown/wholeStage3 unverified. Evidence: test_logs/TEST_LOG_2026-10-26_19-07-50_client-reliability-9-resumed-ready.md.
- 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.
+15
View File
@@ -0,0 +1,15 @@
# Bitvid scheduled-job policy
Owner direction, 2026-10-23: "I would like to not have extra processes like this running when the fleet is down or paused".
All seven installed Bitvid cron jobs enter through `tools/fleet/bin/bitvid-schedule-guard.mjs`, not directly through `sched.sh`.
- A missing/dead/stopped interactive fleet means no scheduled headless launch. At least one configured interactive area must have a running record and a live PID.
- `fleet/PAUSED` is represented by the existing state-directory `PAUSED` marker. It unconditionally blocks every job, including the weekly restart job; `ignorePause` does not bypass this outer guard.
- A running scheduled job is checked every second. On pause/down, the guard sends TERM only to the process group it created. Fleet's nested process runners forward termination to their own agent group. A bounded KILL fallback handles a group that does not exit.
- A cron tick while down or paused starts only the brief guard check, which exits without an AI session or persistent watcher. There is no always-on supervisor added.
- Unpausing and bringing the fleet up lets the next normal cron tick run; this policy does not automatically resume a paused fleet.
- Existing quiet hours, per-job non-overlap locks and time limits still apply inside `sched.sh`. The one-heavy-job convention still applies.
- This is a Bitvid-specific file, separate from the kit-managed scheduler. Kit upgrades should retain it. `fleet crontab` prints generic, unguarded commands: do not overwrite the installed guarded entries without preserving this policy.
Validation: `env -u NODE_TEST_CONTEXT node --test tools/fleet/test/bitvid-schedule-guard.test.mjs` passed 8 tests with natural exit; fixtures only, no real fleet shutdown/pause or headless model run. Test integrity: new behavioral coverage, no existing assertions/gates changed. Process stop tests use a disposable sleep child and verify the unrelated test parent remains alive. Nested production runner cleanup relies on the kit's existing TERM forwarding; this is not a real-model stop qualification.
+39
View File
@@ -0,0 +1,39 @@
# Six-priority campaign — 2026-10-22
Owner: "yeah lets get the fleet spun up and address all of these", relayed by interpreter at14:02:34Z. Routine YES14:03:29Z. Prior holds superseded only within this foreground scope. Baseline unstable fb54cea3. Team is dispatch plus three existing workers; windows alive. Coordination c6022f21 landed c5b678f7 (durable status/log/ancestry checked). All three assignments acknowledged at c5b678f7: storage14:08:34Z, QA14:08:41Z, client14:08:44Z; each reports safe fast-forward and claim. No browser/expensive process started at acknowledgement; QA retains initial narrow slot. No current tool blocker reported.
| Priority | Accountable owner / item | Immediate action | Acceptance / boundary |
|---|---|---|---|
| 1 Firefox deterministic CI | playback-qa / playback-qa-3 | Inspect run39536282396/job119013565277 and local workflow/fixture; identify actual failing step/assertion, reproduce narrowly | Preserve first failure, exact commands/counts and natural exit; diagnose environment vs product. Root cause before fix/test/helper/config ownership. Strict retained regression + failing mutant for changes; targeted Firefox then unchanged required gates. Empty remote logs do not establish a cause. |
| 2 Active cancellation | client-reliability / client-reliability-9 | Actual observer/controller HEAD last-owner disposal, shared owner, same-turn and later fresh rejoin with controlled fetch | Count abort/settlement/slots/cache/mirrors/stale writes; cancelled last-owner work releases promptly, shared live work continues, replacement identity safe, live success/error/timeout/cache/FIFO preserved. HLS and torrent separately measured before any completion claim. Investigation first; no product/test edits until routing and explicit contract note. |
| 3 Playback reliability | dispatch / dispatch-4 (staged; client and QA next) | Audit publicly promised Range GET, persisted health/host scoring, retained warmup, hosted/P2P startup against current source and tests after cancellation slice | Range probe has bounded first chunk/cancel and HEAD-incompatible host fixture; persistence survives reload with bounded expiry/malformed/quota controls and source/host isolation; warmup is reused by playback and cancelled on owner loss with measured resource caps; hosted/P2P startup preserves configured preference, one winning playback and losing cleanup. Establish exact policy/ownership before writes, route real ambiguities. All four subitems open. Other four Oct playback followups remain tracked in QUALITY/canonical TODO. |
| 4 Feed/view tombstones | storage-cors / storage-cors-3 | Actual delete/tombstone ingest then stale refresh using synthetic offline events/controlled relay boundary | Deleted video stays absent through cache refresh, late older event and view switching; unrelated live/newer valid content preserved under existing protocol semantics. Report real path/defect before edits; no new deletion-policy invention or real cloud/delete/signing. |
| 5 Amber sessions | dispatch / dispatch-5 (staged; client next) | Re-audit per-account restore/switch/logout using existing NIP46 lifecycle tests and fake transport, read docs/nips first | Restore selects expected identity; switching and logout preserve other accounts; transient/denied restore handled without stale unknown identity or lost saved sessions. Pin real UI/service wiring where possible. No credential/live signer operation; real phone Amber remains explicit manual gate, not simulated proof. |
| 6 Mobile | dispatch / dispatch-6 (staged; QA next) | Reproduce retained Upload overflow at360/390 plus1280 control; inspect actual app focus/touch and source | Strict document width <= viewport at360/390/1280; critical controls/touch/focus reachable, keyboard containment and restore retained, token/contrast rules followed. Locate culprit before CSS ownership/fix, preserve original failing baseline; real-device accessibility/manual check remains open. |
## Plan and sequencing
1. Inspect every worktree and preserve all dirty/unlanded work. Done: six clean, each HEAD-only count0. Dispatch ff-only current; workers sync own branches, never touch peers.
2. Land coordination docs/backlog; route three disjoint investigations and collect explicit read/next-action acknowledgements.
3. QA gets exclusive initial browser/expensive slot for narrow Firefox reproduction. Client/storage use cheap offline checks. No broad suite/build/normal product queue without dispatch grant; announce UTC start/release in .task and mailbox.
4. Independently reproduce demonstrated defects, record expected contract/ownership and test-integrity requirements, then authorize smallest fixes. No app.js writer currently assigned.
5. Stage remaining playback/Amber/mobile next as each owner frees up; do not close or omit them because another task completes. Add agents only for demonstrated disjoint work and permitted scope.
6. Verify required focused/lint/mutants and unchanged normal gates; land via queue, verify durable status + unstable ancestry before closing. Integrated build/artifact/local preview and final six-row status, honest live/manual blockers.
No deploy/push/spend/production data/real-wallet/credential/live signer/cron authority. No retries, sleeps, force exits, weaker assertions or speculative fixes. No background monitoring claim. Read KNOWN_ISSUES before debugging; existing Firefox flakes and logout failure are baselines, not presumed causes of this CI failure.
## 14:12UTC evidence/routing checkpoint
QA released first browser14:11:17Z: original3 scenarios pass but app bootstrap failed because Buffer CDN was denied, so not CI reproduction. Next exclusive narrow Firefox diagnostic authorized only with pinned genuine cached Buffer and actual readiness, preserving assertions and isolated control; no broad/full/build.
Storage3 dispatch-independent scratch on7369f1fe/runtime-equivalentc5b678f7:7scenarios/3pass/4fail/network0, naturalexit1/29.89ms. Service snapshot/persistent cache leaks original deleted version; old tombstone unconditionally removes newer live active version in buffer; overlapped coordinator older result renders deleted reference at renderVideoList boundary. Two smallest service/buffer fixes routed in separate commits with mutants/controls; coordinator remains open/read-only pending faithful registered lifecycle evidence. No DOM/production proof.
## 14:23UTC QA checkpoint / mobile routing
QA3 native Playwright runner + original instrumentedTest/global setup/3callbacks/assertions PASS3/0fail/0flaky/0skip/retries0, workers1/private genuine Buffer/public-deny/scratch static server. Worker naturalexit0,9955ms,release around14:22:11UTC. Original CI184pass/2fail/3flaky remains OPEN/unreproduced; no speculative fix. App-ready3 and isolated3 and original-timing3 are distinct prior controls, not repeated CI proof. Access to failed CI logs403/no E2E artifact limits diagnosis; offline mirror/serial/serviceWorker setting/server differ. Retain QA3 evidence and stage further reproduction only on new evidence.
Priority6 now ACTIVE via QA4: actual-app Chromium360/390/1280 Upload width+culprit/focus/touch measurement, strict overflow baseline preserved, no CSS/product edits before independent evidence+routing. QA exclusive bounded browser, no full/build. Priority1 remains open; priorities2/4 fixes ongoing,3/5 staged.
## 14:31UTC gate and coordinator checkpoint
Storage3 existing buffer/service candidate owns full/normalgate/build slot since14:26:33, QA4 confirms mobile browser unconsumed/HOLD. Client9 exactcandidatee887ceaf independently23/23 naturalexit0/282.24ms and next fullgate, browser separate. Storage4 now separately routed coordinator-only after actual registered-feed/JSDOM2pass/2fail independent repro at89f8923c; freeze/submit storage3 first, keep follow-on separate. No product landing/CI clearance/live proof claimed.
+25
View File
@@ -0,0 +1,25 @@
# Hourly check-in — 2026-10-24 19:22 UTC
- Read PROGRAM, WIND-DOWN, current RULE-CHANGES, repo memory, active backlog,
task lines, dispatch mailbox (empty), and landing results/log.
- `node tools/fleet/bin/doctor.mjs --json`: exit 0 at 19:22:16Z. Dispatch DIRTY4,
client ONE-DISK2, storage clean, QA DIRTY3 plus one WAITING line. Existing work
was preserved. Doctor also reports unowned paths; ownership was not changed.
- Every WAITING line read: QA waits on dispatch since 03:00:28Z, over 16 hours.
Sent dispatch one scheduler mailbox notice at 19:23:03Z asking acknowledgement
and resolution or explicit parking. Recorded delivery in today's sent ledger;
retained the heavy HOLD and granted no execution slot. Consumption unverified.
- `node tools/fleet/bin/landq.mjs status`: exit 0, queue 0, lander NOT running.
Appended dated owner line; an empty-queue drain normally exits by design.
- Sole failed result `d7184f07bd` is publication divergence, already known to
submitter and recovered per fleet/LOG.md. Append-only log also preserves two
red gates overwritten by later same-SHA success results. Added these to
STRIKES with environment/thermal context, one per area this week. No tier
move threshold reached. No outstanding conflict found in retained records.
- Usage governor: `usage.enabled=false`, `modules.usageGovernor=false`;
configured wind-down threshold 95%. Adapter usage hook was not called;
usage-based PAUSED step does not apply.
- No product edits, tests, builds, browser runs, push, deploy, approvals,
scheduler installation or interactive session starts. Main/product health,
usage percentage, live playback, owner acknowledgement and message consumption
were not verified. Documentation landing is checked separately after submit.
@@ -0,0 +1,12 @@
# Hourly check-in 2026-10-26 19:22 UTC
- Scope: fleet health, all WAITING text, landing results, daily notification deduplication, strikes and usage wind-down. No product changes or heavy validation.
- Plan completed: read PROGRAM/WIND-DOWN/backlog/STRIKES and daily ledger; run doctor/queue/fleet status; inspect every area .task including inline WAITING; review retained adverse landing log; record new strike and owner decision; submit only this audit and STRIKES.
- doctor --json, landq status, fleet status, landq status c20635bc, backlog list: exit0. Doctor: dispatch DIRTY21/WAITING, client ONE-DISK9, QA DIRTY10, storage behind22. Existing work retained.
- All .task files read. Dispatch thermal WAITING since19:16Z and inline client WAITING since19:15:42Z are under2h. No overdue reminder. Storage .task still mentions its historical queued landing; durable result492557400c is landed, not a fresh blockage.
- Fleet reports lander DOWN, queue0; workers unavailable/dead, storage stopped with1 unread, interpreter3 unread. No status says stalled; a dead/unknown window proves neither progress nor a live stall. Lander absence recorded in owner.md.
- Retained log adds one new red c20635bc at19:15:32.782Z; durable result rejected. Existing client MCP notice19:16:19Z is in the daily ledger, so no duplicate send or ledger append. No additional conflict/withdrawal found in retained landing log. Session-only incident completeness unverified.
- STRIKES records thermal interruption without claiming assertion failure. Two client reds within7days straddle configured Monday03:00 usage reset: current configured week1. Owner proposal conditional on rolling-week interpretation; no tier/config edit.
- usage.enabled=false and modules.usageGovernor=false; no usage-hook query or PAUSED creation. Latest40 wake records contain no five-hour-nearly-gone warning; panes not scraped.
- No lint/unit/build/browser rerun for Markdown-only audit during thermal hold. No product/main/live qualification, real session liveness, worker acknowledgement or hold-compliance verification.
- Verification: git diff --check limited to audit files before commit; docs-only landing result and target ancestry checked separately after submission.
+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.
+92
View File
@@ -0,0 +1,92 @@
# Program — the owner's priorities and rulings
Last updated: 2026-10-23
## Current milestone
Six-priority campaign resumed 2026-10-22; prior completion statements below are historical evidence. See CAMPAIGN-2026-10-22.md.
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-23 — Campaign continuation after resource interruption
> "ok lets get the fleet working some more" — owner, relayed by interpreter at2026-10-23T12:48:12Z; not typed in dispatch.
Continue six existing priorities, with client HLS exact-candidate prep/normal gate when safe, QA bind-permission diagnosis/host-launch prep, and disjoint storage read-only playback audit. Preserve all rejected/partial evidence. Interpreter freezes direct/setup target writes during product gates and requires actual dispatch exclusive-slot acknowledgement before heavy validation; queue snapshots do not reserve a slot. No new restricted-action authority.
## 2026-10-23 — Scheduled processes must respect down and pause
> "I would like to not have extra processes like this running when the fleet is down or paused" — owner, relayed by interpreter at2026-10-23T12:35:35Z; not typed in dispatch.
Interpreter is implementing a local Bitvid schedule guard and focused tests in unique files. Intended scope: all seven existing schedules require a live interactive fleet run; PAUSED blocks every job including weekly restart, and a supervised scheduled child is terminated only within its owned process group when fleet pauses/goes down. At12:37:56Z interpreter reports guard installed for all seven entries and8focused fixture tests passing naturally. Dispatch inspected guard/policy source at printed unstable3a89abd0; installed crontab execution and real-model shutdown remain unverified. No always-on watcher; inactive cron ticks run only the brief guard. Existing HLS gate/landings remain untouched; no unrelated process stops, global kit change or restricted authority added.
## 2026-10-23 — Existing seven schedules authorized in interpreter session
> "All seven jobs shown above" — owner, relayed by interpreter at2026-10-23T12:32:43Z; not typed in dispatch.
Interpreter reports installing the existing checkin, hygiene, triage, summary, winddown, restart and resources jobs in user crontab, preserving prior entries and verifying readback. Scoped PATH/FLEET_CONFIG; resources every15minutes, only while laptop awake. Installation/readback are interpreter evidence, not independently verified here. This records scope and provenance; dispatch did not install or alter cron. One-heavy-job/resource guidance and all restricted-action limits remain. It adds no push/deploy/spend/delete/credential authority. Interpreter separately reports owner-approved MCP-only autoapproval setting saved; effective state in existing sessions is unverified and does not override a denied action.
## 2026-10-22 — Six-priority campaign resumed via interpreter
> "yeah lets get the fleet spun up and address all of these" — owner, relayed by interpreter at 2026-10-22T14:02:34Z; not typed in dispatch.
The relay identifies six priorities: Firefox deterministic CI failure; remaining active HEAD/HLS/torrent cancellation; playback reliability promises; feed/view tombstone stale-refresh; Amber sessions; mobile usability. This supersedes the prior idle/local-review and quiet-hours hold for this foreground campaign. Routine investigation, evidence-routed fixes, tests/builds and normal local unstable landing are approved. No push/deploy/spend/production data/real wallet/credential/live signer/scheduler authority. Track all six in docs/agents/CAMPAIGN-2026-10-22.md. Preserve worktrees, use three existing workers with disjoint scope, stage before fixes, serialize browser/full gates, keep app.js single-writer. Final integrated local build/preview and honest live/manual limits required.
### 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.
Loaded 100 of 590 files, more files were not shown because too many files have changed in this diff. Show more