@rubytech/create-maxy-code 0.1.478 → 0.1.479

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +1244 -0
  3. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +462 -0
  4. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +321 -0
  5. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +2177 -0
  6. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +201 -0
  7. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +217 -0
  8. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +80 -0
  9. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +163 -0
  10. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  11. package/payload/platform/lib/graph-style/dist/index.js +22 -0
  12. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  13. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +6 -0
  14. package/payload/platform/lib/graph-style/src/index.ts +20 -0
  15. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +13 -1
  16. package/payload/platform/plugins/cloudflare/PLUGIN.md +26 -1
  17. package/payload/platform/plugins/cloudflare/bin/d1-http.mjs +80 -0
  18. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +17 -2
  19. package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +357 -0
  20. package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +103 -0
  21. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +2 -2
  22. package/payload/platform/plugins/cloudflare/mcp/__tests__/d1-http.test.ts +96 -0
  23. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +120 -0
  24. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +59 -4
  25. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +176 -0
  26. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-state-contract.test.ts +75 -0
  27. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +131 -0
  28. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-sign.test.ts +68 -0
  29. package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +88 -0
  30. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +39 -0
  31. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +68 -1
  32. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +30 -1
  33. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/portal-sign.mjs +87 -0
  34. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +8 -4
  35. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +10 -0
  36. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +107 -1
  37. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +82 -2
  38. package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +9 -0
  39. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +56 -0
  40. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +98 -2
  41. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +12 -0
  42. package/payload/platform/plugins/docs/references/cloudflare.md +12 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts +2 -0
  44. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts.map +1 -0
  45. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js +66 -0
  46. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js.map +1 -0
  47. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts +41 -0
  48. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts.map +1 -0
  49. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js +75 -0
  50. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js.map +1 -0
  51. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +122 -0
  52. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  53. package/payload/server/public/activity.html +8 -6
  54. package/payload/server/public/assets/{AdminLoginScreens-DR5eLF-a.js → AdminLoginScreens-DkxQZnvB.js} +1 -1
  55. package/payload/server/public/assets/AdminShell-Cv_nIFGe.js +2 -0
  56. package/payload/server/public/assets/Checkbox-CsTWK8mm.js +1 -0
  57. package/payload/server/public/assets/activity-BbnTiVBT.js +1 -0
  58. package/payload/server/public/assets/{admin-D2KLZnQW.js → admin-BZY3i_Ff.js} +1 -1
  59. package/payload/server/public/assets/{browser-CDWqEiCD.js → browser-BbIiSFKr.js} +1 -1
  60. package/payload/server/public/assets/{calendar-BG7fOpbB.js → calendar-BvxeTHvQ.js} +1 -1
  61. package/payload/server/public/assets/chat-DUKxufRk.js +1 -0
  62. package/payload/server/public/assets/chevron-left-BtfJD6LL.js +1 -0
  63. package/payload/server/public/assets/data-hl1wa-Ax.js +1 -0
  64. package/payload/server/public/assets/{graph-DRloNic4.js → graph-CDSGRoeb.js} +1 -1
  65. package/payload/server/public/assets/graph-labels-C5qLwnWA.js +1 -0
  66. package/payload/server/public/assets/{maximize-2-CzDaBugs.js → maximize-2-CAzH8dED.js} +1 -1
  67. package/payload/server/public/assets/{operator-BZeeyait.js → operator-BK7r8DAB.js} +1 -1
  68. package/payload/server/public/assets/{page-ef-6rRPL.js → page-B5YngOrK.js} +1 -1
  69. package/payload/server/public/assets/{page-B0obcg3b.js → page-BsCS0Dmc.js} +1 -1
  70. package/payload/server/public/assets/{public-9D3MMOkC.js → public-C1Pz-IBs.js} +1 -1
  71. package/payload/server/public/assets/{rotate-ccw-DXEKUhXP.js → rotate-ccw-UDln4Tou.js} +1 -1
  72. package/payload/server/public/assets/{tasks-BB0L7e5a.js → tasks-B-cLsn3p.js} +1 -1
  73. package/payload/server/public/assets/{time-entry-format-CsYAjplS.js → time-entry-format-DPOBIE18.js} +1 -1
  74. package/payload/server/public/assets/{triangle-alert-0lSBIUg-.js → triangle-alert-Dz-CZKo5.js} +1 -1
  75. package/payload/server/public/assets/{useCopyFeedback-jTBQ2h0V.js → useCopyFeedback-wtUHGohk.js} +1 -1
  76. package/payload/server/public/assets/{useSelectionMode-BooArySZ.js → useSelectionMode-sph7AQT5.js} +1 -1
  77. package/payload/server/public/assets/useSubAccountSwitcher-BRIluIKf.css +1 -0
  78. package/payload/server/public/assets/{useVoiceRecorder-PUT8q_sm.js → useVoiceRecorder-DN4jYQ0A.js} +1 -1
  79. package/payload/server/public/brand-defaults.css +1 -0
  80. package/payload/server/public/browser.html +5 -5
  81. package/payload/server/public/calendar.html +6 -6
  82. package/payload/server/public/chat.html +12 -12
  83. package/payload/server/public/data.html +11 -11
  84. package/payload/server/public/graph.html +11 -11
  85. package/payload/server/public/index.html +14 -14
  86. package/payload/server/public/operator.html +14 -14
  87. package/payload/server/public/privacy.html +1 -0
  88. package/payload/server/public/public.html +12 -12
  89. package/payload/server/public/tasks.html +6 -5
  90. package/payload/server/public/vnc-popout.html +1 -0
  91. package/payload/server/server.js +741 -366
  92. package/payload/server/public/assets/AdminShell-D9zaUxPd.js +0 -2
  93. package/payload/server/public/assets/Checkbox-DcbbAIe5.js +0 -1
  94. package/payload/server/public/assets/activity-C9P5NjEZ.js +0 -1
  95. package/payload/server/public/assets/chat-RyH-WYQC.js +0 -1
  96. package/payload/server/public/assets/chevron-left-DmXCr6nB.js +0 -1
  97. package/payload/server/public/assets/data-BaY_LkLR.js +0 -1
  98. package/payload/server/public/assets/graph-labels-DD-GqS4v.js +0 -1
  99. package/payload/server/public/assets/useSubAccountSwitcher-D9knHOKq.css +0 -1
  100. /package/payload/server/public/assets/{useSubAccountSwitcher-BZnzc5C3.js → useSubAccountSwitcher-v8LyBzZN.js} +0 -0
@@ -0,0 +1,462 @@
1
+ # Task 1818 — Widen the loop gate to all of `app/**` — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** `scripts/check-loops-registered.mjs` scans the whole of `app/**` rather than only `app/lib/**`, so a standing server loop armed anywhere under `app/` fails the build instead of running invisibly.
6
+
7
+ **Architecture:** One line changes behaviour — `SCAN_ROOTS` becomes `['server', 'app']`. That widening immediately fails the real tree on fifteen pre-existing browser timers, so the fifteen `// loop-exempt:client` markers must land in the same commit or `npm run prebuild` is broken between commits. The gate's SCOPE header currently asserts the opposite boundary and moves with the behaviour.
8
+
9
+ **Tech Stack:** Node ESM script (no dependencies), vitest, TypeScript/TSX source under `platform/ui`.
10
+
11
+ **Design doc:** `platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md`
12
+
13
+ ## Global Constraints
14
+
15
+ - All paths below are relative to `platform/ui/` inside the `maxy-code/` tree.
16
+ - The marker convention and the closed category set are settled by Task 1814 and must not change. Valid categories are exactly `per-connection`, `demand-singleton`, `client`, `registry-primitive`.
17
+ - Marker form is `// loop-exempt:<category> <reason>` appended to the physical line containing `setInterval(`. For a multi-line arrow body the comment goes after the opening brace, following the established form at `server/routes/public-reader.ts:257`.
18
+ - A reason must contain at least three consecutive letters (`REASON = /[A-Za-z]{3}/`) and must state something true and specific about that timer. Repeated boilerplate across the fifteen sites is a plan failure.
19
+ - The gate parses nothing. Do not add comment-stripping, string-awareness, or any TypeScript parsing — that is what killed its first version.
20
+ - Do not add a `registerLoop` call. The census found no standing loop in the newly covered area; adding one would be inventing a requirement.
21
+ - Every commit carries `Session: aad2dcb4-bc6c-4502-9ef9-e75995fb974b` and `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`.
22
+
23
+ **Baseline, measured before any change:**
24
+
25
+ ```
26
+ [check-loops-registered] ok files=284 sites=10 marked=9 allowlisted=1 (per-connection=6 demand-singleton=1 client=1 registry-primitive=1)
27
+ ```
28
+
29
+ **Predicted after:** `files=393 sites=25 marked=24 allowlisted=1 (per-connection=6 demand-singleton=1 client=16 registry-primitive=1)`
30
+
31
+ ---
32
+
33
+ ## File Structure
34
+
35
+ | File | Responsibility | Change |
36
+ |---|---|---|
37
+ | `scripts/check-loops-registered.mjs` | The gate. Walks scan roots, matches raw lines, validates markers. | Modify: `SCAN_ROOTS` constant + SCOPE header block |
38
+ | `scripts/__tests__/check-loops-registered.test.ts` | Committed behaviour suite for the gate. | Modify: one existing case; add two; raise real-tree floors |
39
+ | 11 source files under `app/` outside `app/lib/` | Browser components and hooks that arm timers. | Modify: append a marker comment to 15 lines |
40
+
41
+ ---
42
+
43
+ ### Task 1: Move the test surface to the new boundary
44
+
45
+ The existing suite contains a case that **encodes the hole this task closes**. It proves "files outside the two scan roots are ignored" using `app/chat/page.tsx` — a file that becomes in-scope. Left alone it would fail; edited carelessly it would silently stop testing anything. It must be re-pointed at a path that is genuinely outside both roots.
46
+
47
+ **Files:**
48
+ - Modify: `scripts/__tests__/check-loops-registered.test.ts:87-91` (the mis-pointed case)
49
+ - Modify: `scripts/__tests__/check-loops-registered.test.ts` (add two cases after line 91)
50
+
51
+ **Interfaces:**
52
+ - Consumes: `runGate(files: Record<string, string>): { code: number; out: string }` — the existing helper at line 25. It writes the given files into a temp dir and runs the gate with `--root <dir>`, returning exit code and combined stdout+stderr.
53
+ - Produces: nothing consumed by later tasks.
54
+
55
+ - [ ] **Step 1: Re-point the mis-pointed case and add the two new ones**
56
+
57
+ Replace lines 87–91 of `scripts/__tests__/check-loops-registered.test.ts`:
58
+
59
+ ```typescript
60
+ it('ignores files outside the two scan roots', () => {
61
+ const r = runGate({ 'app/chat/page.tsx': 'setInterval(f, 1)\n' })
62
+ expect(r.code).toBe(0)
63
+ expect(r.out).toContain('sites=0')
64
+ })
65
+ ```
66
+
67
+ with:
68
+
69
+ ```typescript
70
+ // Task 1818 re-pointed this case. It previously proved the point with
71
+ // `app/chat/page.tsx`, which is now IN scope — the precise hole 1818 closes.
72
+ // `scripts/` is outside both roots and is where the gate itself lives, so it
73
+ // is a stable choice that will not quietly migrate in-scope later.
74
+ it('ignores files outside the scan roots', () => {
75
+ const r = runGate({ 'scripts/some-tool.mjs': 'setInterval(f, 1)\n' })
76
+ expect(r.code).toBe(0)
77
+ expect(r.out).toContain('sites=0')
78
+ })
79
+
80
+ // Task 1818 — the gap 1814 left. A standing loop armed in a component or
81
+ // route file under `app/` but outside `app/lib/` was invisible to the gate.
82
+ it('fails on an unmarked setInterval under app/ outside app/lib', () => {
83
+ const r = runGate({ 'app/chat/page.tsx': 'setInterval(() => poll(), 1000)\n' })
84
+ expect(r.code).toBe(1)
85
+ expect(r.out).toContain('app/chat/page.tsx')
86
+ })
87
+
88
+ it('passes a marked client site under app/ outside app/lib', () => {
89
+ const r = runGate({
90
+ 'app/Sidebar.tsx':
91
+ 'const t = setInterval(poll, 5000) // loop-exempt:client sidebar poll in the browser\n',
92
+ })
93
+ expect(r.code).toBe(0)
94
+ expect(r.out).toContain('sites=1')
95
+ expect(r.out).toContain('client=1')
96
+ })
97
+ ```
98
+
99
+ - [ ] **Step 2: Run the suite to verify the two new cases fail**
100
+
101
+ ```bash
102
+ cd maxy-code/platform/ui && npx vitest run scripts/__tests__/check-loops-registered.test.ts
103
+ ```
104
+
105
+ Expected: the re-pointed `ignores files outside the scan roots` PASSES (`scripts/` is outside both roots today and after). `fails on an unmarked setInterval under app/ outside app/lib` FAILS — the gate exits 0 because `app/chat/page.tsx` is not scanned. `passes a marked client site under app/ outside app/lib` FAILS on `expect(r.out).toContain('sites=1')`, receiving `sites=0` for the same reason.
106
+
107
+ Two failures is the RED state. If either new case passes here, the scan root was already wider than the baseline and the premise of this task needs re-checking before continuing.
108
+
109
+ - [ ] **Step 3: Commit the red tests**
110
+
111
+ ```bash
112
+ git add maxy-code/platform/ui/scripts/__tests__/check-loops-registered.test.ts
113
+ git commit -m "test: assert the loop gate covers app/ outside app/lib (red)
114
+
115
+ Re-points the case that proved the hole using app/chat/page.tsx, which this
116
+ task brings in scope, and adds the two cases 1818 requires.
117
+
118
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119
+ Session: aad2dcb4-bc6c-4502-9ef9-e75995fb974b"
120
+ ```
121
+
122
+ ---
123
+
124
+ ### Task 2: Widen the scan root and mark the fifteen browser timers
125
+
126
+ These land together. Widening alone fails the real tree on fifteen unmarked sites, which breaks `npm run prebuild` for every other sprint in the repo until the markers land.
127
+
128
+ **Files:**
129
+ - Modify: `scripts/check-loops-registered.mjs:53-60` (SCOPE header), `scripts/check-loops-registered.mjs:74-75` (`SCAN_ROOTS`)
130
+ - Modify: `app/AdminShell.tsx:228`, `app/Sidebar.tsx:512,534,1609`, `app/activity/page.tsx:428`, `app/chat/page.tsx:700,721`, `app/chat/transcript-render.tsx:80`, `app/graph/page.tsx:1456`, `app/graph/settle-lock.ts:137`, `app/hooks/useViewMode.ts:110`, `app/hooks/useVoiceRecorder.ts:142`, `app/useAdminAuth.ts:219`, `app/useElapsedTimer.ts:29,35`
131
+
132
+ **Interfaces:**
133
+ - Consumes: nothing from Task 1.
134
+ - Produces: the gate's counter line, whose exact predicted value Task 3 asserts.
135
+
136
+ - [ ] **Step 1: Widen `SCAN_ROOTS`**
137
+
138
+ Replace lines 74–75 of `scripts/check-loops-registered.mjs`:
139
+
140
+ ```javascript
141
+ /** The two trees that contain server-side code. */
142
+ const SCAN_ROOTS = ['server', 'app/lib']
143
+ ```
144
+
145
+ with:
146
+
147
+ ```javascript
148
+ /** `app/lib` is deliberately NOT listed alongside `app` — it is a subtree of
149
+ * it, and naming both would walk it twice, doubling its contribution to the
150
+ * counts printed below. Those counts are the evidence that the scan actually
151
+ * read the tree, so inflating them defeats their purpose. */
152
+ const SCAN_ROOTS = ['server', 'app']
153
+ ```
154
+
155
+ - [ ] **Step 2: Rewrite the SCOPE header block**
156
+
157
+ Replace lines 53–60 of `scripts/check-loops-registered.mjs`:
158
+
159
+ ```javascript
160
+ // SCOPE — exact, because a gate believed to cover more than it does is worse
161
+ // than none.
162
+ //
163
+ // Covers `server/**` and `app/lib/**`, excluding `__tests__`. Every standing
164
+ // server loop in the tree lives in one of those two, including all four gateway
165
+ // sweeps. NOT covered: the rest of `app/**`, which is Next.js route and
166
+ // component code running in the visitor's browser, and test files, which are
167
+ // not production loops.
168
+ ```
169
+
170
+ with:
171
+
172
+ ```javascript
173
+ // SCOPE — exact, because a gate believed to cover more than it does is worse
174
+ // than none.
175
+ //
176
+ // Covers `server/**` and `app/**`, excluding `__tests__`. Not covered: test
177
+ // files, which are not production loops, and everything outside those two
178
+ // trees (`scripts/`, `e2e/`, config), which does not run in the server process.
179
+ //
180
+ // Task 1818 widened the second root from `app/lib` to all of `app`. 1814 had
181
+ // scanned `app/lib` only, on a census showing every standing server loop lived
182
+ // there. That was true and is not the point: the gate's job is what could be
183
+ // added, not what is present. Two findings made the narrower root indefensible.
184
+ //
185
+ // 1. There is no route-file surface to enumerate instead. This package is a
186
+ // Vite SPA (ten HTML entries) plus a Hono server bundled from exactly two
187
+ // entries, `server/index.ts` and `server/edge.ts`. There is no Next.js and
188
+ // no `app/api/`, so "scan the server-executing subtrees of app/" has no
189
+ // subtree to name, and any list would be a list again — the property 1814
190
+ // removed.
191
+ // 2. Server-reachable code outside `app/lib/` already exists.
192
+ // `server/routes/whatsapp-reader.ts` imports SUPPORTED_MIME_SET,
193
+ // MAX_FILE_BYTES and MAX_ATTACHMENTS — runtime values, not types — from
194
+ // `app/admin-types.ts`. It arms no timer, so nothing was hidden, but the
195
+ // boundary "server code stops at app/lib" was already false.
196
+ //
197
+ // `'use client'` is not the discriminator and cannot become one: three of the
198
+ // eleven files under `app/` that arm a browser timer carry no such directive
199
+ // (`chat/page.tsx`, `chat/transcript-render.tsx`, `graph/settle-lock.ts`).
200
+ ```
201
+
202
+ - [ ] **Step 3: Mark all fifteen sites**
203
+
204
+ Append to the end of each listed line, exactly as given. Each reason states what that specific timer does; do not substitute a shared phrase.
205
+
206
+ `app/AdminShell.tsx:228`
207
+ ```typescript
208
+ const id = setInterval(read, 60000) // loop-exempt:client polls /api/health for the Claude auth badge in the admin shell, browser-side
209
+ ```
210
+
211
+ `app/Sidebar.tsx:512`
212
+ ```typescript
213
+ timer = setInterval(() => { void fetchChannelConvos() }, CHANNEL_CONVO_POLL_MS) // loop-exempt:client refreshes the sidebar's channel conversation list in the browser
214
+ ```
215
+
216
+ `app/Sidebar.tsx:534`
217
+ ```typescript
218
+ timer = setInterval(tick, SESSIONS_POLL_MS) // loop-exempt:client refreshes the sidebar's session list in the browser
219
+ ```
220
+
221
+ `app/Sidebar.tsx:1609`
222
+ ```typescript
223
+ timer = setInterval(() => { void poll() }, SYSTEM_STATS_POLL_MS) // loop-exempt:client refreshes the sidebar's system-stats readout in the browser
224
+ ```
225
+
226
+ `app/activity/page.tsx:428`
227
+ ```typescript
228
+ const t = setInterval(() => void load(range), 15_000) // loop-exempt:client reloads the /activity table in the browser; the loops it displays are the registered ones
229
+ ```
230
+
231
+ `app/chat/page.tsx:700`
232
+ ```typescript
233
+ const interval = setInterval(() => { void fetchPointer() }, POINTER_POLL_MS) // loop-exempt:client polls the transcript pointer for the open chat tab
234
+ ```
235
+
236
+ `app/chat/page.tsx:721`
237
+ ```typescript
238
+ const id = setInterval(read, 60000) // loop-exempt:client polls /api/health for the Claude auth badge on the chat page, browser-side
239
+ ```
240
+
241
+ `app/chat/transcript-render.tsx:80`
242
+ ```typescript
243
+ const id = setInterval(() => setNow(Date.now()), 1000) // loop-exempt:client one-second render clock advancing relative timestamps in the transcript
244
+ ```
245
+
246
+ `app/graph/page.tsx:1456`
247
+ ```typescript
248
+ const intervalId = window.setInterval(poll, POLL_MS) // loop-exempt:client polls graph layout progress in the browser; window-scoped by construction
249
+ ```
250
+
251
+ `app/graph/settle-lock.ts:137`
252
+ ```typescript
253
+ timer = setInterval(() => { // loop-exempt:client samples vis-network node positions to freeze canvas physics once the layout is still
254
+ ```
255
+
256
+ `app/hooks/useViewMode.ts:110`
257
+ ```typescript
258
+ pollInterval.current = setInterval(() => { // loop-exempt:client watches whether the popout window was closed, so the parent tab can restore its own view
259
+ ```
260
+
261
+ `app/hooks/useVoiceRecorder.ts:142`
262
+ ```typescript
263
+ timerRef.current = setInterval(() => { // loop-exempt:client advances the recording elapsed-time display while the mic is open
264
+ ```
265
+
266
+ `app/useAdminAuth.ts:219`
267
+ ```typescript
268
+ const interval = setInterval(async () => { // loop-exempt:client polls /api/health to keep the browser's auth signal current while a chat is open
269
+ ```
270
+
271
+ `app/useElapsedTimer.ts:29`
272
+ ```typescript
273
+ sessionTickRef.current = setInterval(() => { // loop-exempt:client advances the per-session turn clock rendered in the browser
274
+ ```
275
+
276
+ `app/useElapsedTimer.ts:35`
277
+ ```typescript
278
+ elapsedRef.current = setInterval(() => { // loop-exempt:client advances the elapsed-seconds counter rendered in the browser
279
+ ```
280
+
281
+ - [ ] **Step 4: Run the gate against the real tree**
282
+
283
+ ```bash
284
+ cd maxy-code/platform/ui && node scripts/check-loops-registered.mjs
285
+ ```
286
+
287
+ Expected, exactly:
288
+
289
+ ```
290
+ [check-loops-registered] ok files=393 sites=25 marked=24 allowlisted=1 (per-connection=6 demand-singleton=1 client=16 registry-primitive=1)
291
+ ```
292
+
293
+ If it exits 1, a marker is missing or malformed — the error names the file and line. If it exits 0 but `files=` is not 393, the walk is not reading what this plan predicts; stop and re-census rather than adjusting the expected number to match.
294
+
295
+ - [ ] **Step 5: Run the gate's suite**
296
+
297
+ ```bash
298
+ cd maxy-code/platform/ui && npx vitest run scripts/__tests__/check-loops-registered.test.ts
299
+ ```
300
+
301
+ Expected: all cases pass, including Task 1's two, which were red.
302
+
303
+ - [ ] **Step 6: Commit**
304
+
305
+ ```bash
306
+ git add maxy-code/platform/ui/scripts/check-loops-registered.mjs \
307
+ maxy-code/platform/ui/app
308
+ git commit -m "feat: scan all of app/ in the loop gate, not just app/lib
309
+
310
+ Widens SCAN_ROOTS to ['server', 'app'] and marks the fifteen pre-existing
311
+ browser timers loop-exempt:client. Both land together: widening alone fails
312
+ prebuild on fifteen unmarked sites.
313
+
314
+ Header rewritten — it asserted the rest of app/ was Next.js route code, which
315
+ this package has none of, and that app/lib was where server code stopped,
316
+ which server/routes/whatsapp-reader.ts already disproves.
317
+
318
+ files=284 -> 393, sites=10 -> 25, client=1 -> 16.
319
+
320
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
321
+ Session: aad2dcb4-bc6c-4502-9ef9-e75995fb974b"
322
+ ```
323
+
324
+ ---
325
+
326
+ ### Task 3: Prove the signal is live, then raise the real-tree floors
327
+
328
+ Task 1's new cases run under `--root` against a synthetic tree. They prove the gate's *logic* handles `app/`, but they cannot prove the *real* scan root moved — a gate still hardcoded to `app/lib` internally would pass them if the fixture happened to be reachable. The task requires the gate observed failing on a real violation in the newly covered area. That is what this task does.
329
+
330
+ **Files:**
331
+ - Modify: `scripts/__tests__/check-loops-registered.test.ts:110-116` (the real-tree floors)
332
+ - Temporarily modify then revert: `app/Sidebar.tsx`
333
+
334
+ **Interfaces:**
335
+ - Consumes: the counter line produced by Task 2.
336
+ - Produces: nothing.
337
+
338
+ - [ ] **Step 1: Mutation check — insert a real violation**
339
+
340
+ Add this line at module scope in `app/Sidebar.tsx`, immediately after the final `import` statement. Module scope, not inside a component, so it is a genuine standing-loop shape rather than a mount-scoped one:
341
+
342
+ ```typescript
343
+ setInterval(() => { void fetch('/api/health') }, 30000)
344
+ ```
345
+
346
+ - [ ] **Step 2: Observe the gate fail on it**
347
+
348
+ ```bash
349
+ cd maxy-code/platform/ui && node scripts/check-loops-registered.mjs; echo "exit=$?"
350
+ ```
351
+
352
+ Expected: `exit=1`, with output naming `app/Sidebar.tsx`, the line number of the inserted statement, and `no loop-exempt marker`.
353
+
354
+ **Record the exact output in the debrief.** A pass here is the whole point of the task — if the gate exits 0, the scan root did not actually move and Task 2 is not done, regardless of what the synthetic tests say.
355
+
356
+ - [ ] **Step 3: Revert the mutation**
357
+
358
+ ```bash
359
+ cd maxy-code/platform/ui && git checkout app/Sidebar.tsx
360
+ node scripts/check-loops-registered.mjs; echo "exit=$?"
361
+ ```
362
+
363
+ Expected: `exit=0` and the counter line back to `files=393 sites=25 marked=24 allowlisted=1`.
364
+
365
+ Confirm the revert is complete before continuing:
366
+
367
+ ```bash
368
+ git diff --stat -- app/Sidebar.tsx
369
+ ```
370
+
371
+ Expected: no output. If `Sidebar.tsx` still differs, the marker from Task 2 was reverted along with the mutation — re-apply it.
372
+
373
+ - [ ] **Step 4: Raise the real-tree floors**
374
+
375
+ Replace lines 110–116 of `scripts/__tests__/check-loops-registered.test.ts`:
376
+
377
+ ```typescript
378
+ it('reports non-zero counts against the real tree, so a dead scanner is visible', () => {
379
+ const out = execFileSync('node', [GATE], { encoding: 'utf8' })
380
+ const files = Number(/files=(\d+)/.exec(out)?.[1] ?? 0)
381
+ const sites = Number(/sites=(\d+)/.exec(out)?.[1] ?? 0)
382
+ expect(files).toBeGreaterThanOrEqual(20)
383
+ expect(sites).toBeGreaterThanOrEqual(9)
384
+ })
385
+ ```
386
+
387
+ with:
388
+
389
+ ```typescript
390
+ it('reports non-zero counts against the real tree, so a dead scanner is visible', () => {
391
+ const out = execFileSync('node', [GATE], { encoding: 'utf8' })
392
+ const files = Number(/files=(\d+)/.exec(out)?.[1] ?? 0)
393
+ const sites = Number(/sites=(\d+)/.exec(out)?.[1] ?? 0)
394
+ expect(files).toBeGreaterThanOrEqual(20)
395
+ expect(sites).toBeGreaterThanOrEqual(9)
396
+ })
397
+
398
+ // Task 1818 — the floors above predate the widening and would still pass with
399
+ // `app/` unscanned, so they cannot detect this task being reverted. These are
400
+ // keyed to the widened tree instead. They are floors, not equalities: source
401
+ // files get added routinely and an equality would fail on unrelated work.
402
+ // A revert to the `app/lib`-only root collapses files to ~284 and client to
403
+ // 1, so both of these fail, which is what they are for.
404
+ it('scans the whole of app/, not just app/lib', () => {
405
+ const out = execFileSync('node', [GATE], { encoding: 'utf8' })
406
+ const files = Number(/files=(\d+)/.exec(out)?.[1] ?? 0)
407
+ const client = Number(/client=(\d+)/.exec(out)?.[1] ?? 0)
408
+ expect(files).toBeGreaterThanOrEqual(380)
409
+ expect(client).toBeGreaterThanOrEqual(16)
410
+ })
411
+ ```
412
+
413
+ - [ ] **Step 5: Run the full gate suite**
414
+
415
+ ```bash
416
+ cd maxy-code/platform/ui && npx vitest run scripts/__tests__/check-loops-registered.test.ts
417
+ ```
418
+
419
+ Expected: all cases pass, including the new floor case.
420
+
421
+ - [ ] **Step 6: Run the whole prebuild gate chain**
422
+
423
+ ```bash
424
+ cd maxy-code/platform/ui && npm run prebuild
425
+ ```
426
+
427
+ Expected: every gate exits 0. `check-loops-registered` prints the Task 2 counter line. This confirms the widened gate has not broken any sibling gate and that the real build path is clean.
428
+
429
+ - [ ] **Step 7: Commit**
430
+
431
+ ```bash
432
+ git add maxy-code/platform/ui/scripts/__tests__/check-loops-registered.test.ts
433
+ git commit -m "test: floor the loop gate's counts to the widened tree
434
+
435
+ The pre-existing floors (files>=20, sites>=9) pass with app/ unscanned, so
436
+ they cannot detect this task being reverted. Adds floors keyed to the widened
437
+ root: files>=380, client>=16.
438
+
439
+ Mutation-checked: a module-scope setInterval inserted into app/Sidebar.tsx is
440
+ observed failing the gate at exit 1, then reverted.
441
+
442
+ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
443
+ Session: aad2dcb4-bc6c-4502-9ef9-e75995fb974b"
444
+ ```
445
+
446
+ ---
447
+
448
+ ## Verification surface
449
+
450
+ Run from `maxy-code/platform/ui`.
451
+
452
+ | Check | Command | Expected |
453
+ |---|---|---|
454
+ | Gate, real tree | `node scripts/check-loops-registered.mjs` | `files=393 sites=25 marked=24 allowlisted=1 (… client=16 …)` |
455
+ | Gate suite | `npx vitest run scripts/__tests__/check-loops-registered.test.ts` | all pass |
456
+ | Full prebuild chain | `npm run prebuild` | all gates exit 0 |
457
+ | Mutation check | Task 3 Steps 1–3 | exit 1 naming `app/Sidebar.tsx`, then exit 0 after revert |
458
+ | No behaviour change in app | `git diff -- app/` | comment-only additions; no statement altered |
459
+
460
+ There is no browser verification step. This task changes a build gate and appends comments; it alters no rendered output and no runtime behaviour. `git diff -- app/` showing comment-only changes is the evidence for that claim.
461
+
462
+ **Known pre-existing red, not caused by this task:** two tests in `server/lib/top-level-labels.test.ts` and `server/routes/__tests__/graph-default-topLevel.test.ts` fail on `main` and in any worktree, tracked as Task 1819. Neither imports anything this task touches. Do not attempt to fix them here.