@rubytech/create-sitedesk-code 0.1.511 → 0.1.513

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 (48) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md +303 -0
  3. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +126 -0
  4. package/payload/platform/lib/account-schema-regions/dist/index.d.ts +5 -0
  5. package/payload/platform/lib/account-schema-regions/dist/index.d.ts.map +1 -1
  6. package/payload/platform/lib/account-schema-regions/dist/index.js +5 -0
  7. package/payload/platform/lib/account-schema-regions/dist/index.js.map +1 -1
  8. package/payload/platform/lib/account-schema-regions/src/index.ts +5 -0
  9. package/payload/platform/package.json +3 -0
  10. package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard-bash.test.sh +14 -0
  11. package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +38 -0
  12. package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh +11 -0
  13. package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +25 -1
  14. package/payload/platform/plugins/admin/skills/deck-pages/SKILL.md +2 -2
  15. package/payload/platform/plugins/admin/skills/skill-builder/SKILL.md +1 -1
  16. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  17. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +15 -34
  18. package/payload/platform/plugins/business-assistant/skills/e-sign/stamp.mjs +269 -0
  19. package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
  20. package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +1 -1
  21. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +5 -5
  22. package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +2 -2
  23. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +1 -1
  24. package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
  25. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
  26. package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +26 -0
  27. package/payload/platform/scripts/check-no-esm-require.mjs +6 -0
  28. package/payload/platform/scripts/lib/account-schema-owned-dirs.py +40 -4
  29. package/payload/platform/scripts/logs-read.sh +154 -50
  30. package/payload/platform/scripts/logs-read.test.sh +276 -0
  31. package/payload/platform/scripts/provision-worktree.sh +80 -0
  32. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  33. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +13 -0
  34. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  35. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -3
  36. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  37. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +17 -11
  38. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  39. package/payload/server/{chunk-2WAXM5N2.js → chunk-S6HYTIU3.js} +2 -3
  40. package/payload/server/maxy-edge.js +7 -3
  41. package/payload/server/public/assets/admin-6QN-k3zQ.js +1 -0
  42. package/payload/server/public/assets/admin-6QN-k3zQ.js.br +0 -0
  43. package/payload/server/public/assets/admin-6QN-k3zQ.js.gz +0 -0
  44. package/payload/server/public/index.html +1 -1
  45. package/payload/server/server.js +860 -384
  46. package/payload/server/public/assets/admin-Do53K90C.js +0 -1
  47. package/payload/server/public/assets/admin-Do53K90C.js.br +0 -0
  48. package/payload/server/public/assets/admin-Do53K90C.js.gz +0 -0
@@ -26,7 +26,7 @@
26
26
  # For per-session types (agent-stream/system/session/error/public): sessionKey
27
27
  # is required in the first mode. For platform-scoped types (server/vnc/
28
28
  # heartbeat/mem): the id is ignored. chat-attempts is --tail-only:
29
- # cross-grep of edge.log + server.log for the chat-ingress quartet within
29
+ # cross-grep of edge.log + server.log for the chat-ingress checkpoints within
30
30
  # the most recent N minutes.
31
31
  #
32
32
  # Every invocation writes a one-line trailer describing files searched,
@@ -154,11 +154,21 @@ is_per_conversation_type() {
154
154
  SEARCH_TYPES="agent-stream error session public server mcp vnc"
155
155
  VALID_TYPES="agent-stream, system, session, error, heartbeat, public, server, mcp, vnc, mem, chat-attempts"
156
156
 
157
- # chat-attempts cross-grep prefixes. One row per chat-ingress
158
- # checkpoint emitted across edge.log + server.log: edge-side inbound/outcome,
159
- # admin-auth gate verdict, chat-route handler entry. Absence of a row at any
160
- # checkpoint localises the failure to one stage of the ingress pipeline.
161
- CHAT_ATTEMPT_GREP='\[edge-admin\] inbound|\[edge-admin\] outcome|\[admin-auth\] outcome|\[chat-route\] entered'
157
+ # chat-attempts cross-grep prefixes. Four checkpoint patterns across
158
+ # edge.log + server.log: edge-side inbound/outcome, admin-auth gate verdict,
159
+ # chat-route handler entry. Any one chat emits THREE of the four, and which
160
+ # three depends on the surface: the admin webchat (POST /api/webchat/send)
161
+ # carries an admin-auth row and no chat-route row, public chat
162
+ # (POST /api/chat) carries a chat-route row and no admin-auth row. Absence of
163
+ # a row the surface does emit localises the failure to one ingress stage.
164
+ #
165
+ # The `ts=` group is optional and sits between the tag and the keyword because
166
+ # that is where Task 2024 put it: the shipped line is
167
+ # `[edge-admin] ts=<ISO> inbound …` (edge.ts:188, 199; edge-admin.ts:48, 52).
168
+ # Requiring tag and keyword adjacent matched zero lines and reported the
169
+ # absence as an edge-side silence (Task 2032). Optional rather than required,
170
+ # so the pattern is not pinned to one format version a second time.
171
+ CHAT_ATTEMPT_GREP='\[edge-admin\] (ts=[^ ]+ )?(inbound|outcome)|\[admin-auth\] outcome|\[chat-route\] entered'
162
172
 
163
173
  # Authoritative sessionKey character set.
164
174
  # Writer-side emits `sk_<hex16>` filenames; the underscore must
@@ -179,7 +189,7 @@ Usage:
179
189
  is_error result (silent-failure audit)
180
190
  logs-read.sh --tail [type] [lines] Tail most recent log of type
181
191
  logs-read.sh --tail chat-attempts [min] Cross-grep edge.log + server.log
182
- for the chat-ingress quartet
192
+ for the chat-ingress checkpoints
183
193
  logs-read.sh --grep <sessionKey> [type] Legacy grep across log files
184
194
 
185
195
  Types: $VALID_TYPES
@@ -193,9 +203,12 @@ transcript, flagging subagent is_error results inline. An explicit <type>
193
203
  keeps the legacy per-account stream-log read.
194
204
  Per-session stream-log types (agent-stream, system, session, error, public)
195
205
  require sessionKey; platform-scoped types (server, vnc, heartbeat, mem) ignore the id.
196
- chat-attempts is --tail-only — it aggregates the four ingress checkpoints
197
- ([edge-admin] inbound, [admin-auth] outcome, [chat-route] entered,
198
- [edge-admin] outcome) into one timeline.
206
+ chat-attempts is --tail-only — it aggregates four ingress checkpoint patterns
207
+ ([edge-admin] ts=<ISO> inbound, [admin-auth] outcome, [chat-route] entered,
208
+ [edge-admin] ts=<ISO> outcome) into one timeline. Any one chat emits three of
209
+ them: the admin webchat (POST /api/webchat/send) emits the two edge rows plus
210
+ [admin-auth] outcome and no [chat-route] row; public chat (POST /api/chat)
211
+ emits the two edge rows plus [chat-route] entered and no [admin-auth] row.
199
212
  EOF
200
213
  exit 2
201
214
  }
@@ -209,25 +222,48 @@ validate_type() {
209
222
  }
210
223
 
211
224
  # chat-attempts cross-grep mode. Combines edge.log + server.log
212
- # lines matching the chat-ingress quartet, filtered to the last N minutes,
225
+ # lines matching the chat-ingress checkpoints, filtered to the last N minutes,
213
226
  # sorted chronologically. Default window: 5 minutes (matches the typical
214
227
  # "agent doesn't respond" report window — operator sees the silence,
215
228
  # screenshots the chat, then runs this within a few minutes).
216
229
  #
217
230
  # Output preserves source-file prefix so operators can see whether the line
218
231
  # came from the edge process (edge.log) or maxy-ui (server.log). On a
219
- # successful chat POST the timeline reads:
232
+ # successful chat POST the timeline reads THREE rows, and which three depends
233
+ # on the surface.
234
+ #
235
+ # admin webchat, POST /api/webchat/send:
236
+ # [edge-admin] ts=… inbound method=POST path=/api/webchat/send … (edge.log)
237
+ # [admin-auth] outcome=accept path=/api/webchat/send cacheKey=… (server.log)
238
+ # [edge-admin] ts=… outcome method=POST path=… status=200 … (edge.log)
239
+ #
240
+ # public chat, POST /api/chat:
241
+ # [edge-admin] ts=… inbound method=POST path=/api/chat … (edge.log)
242
+ # [chat-route] entered route=public method=POST (server.log)
243
+ # [edge-admin] ts=… outcome method=POST path=… status=200 … (edge.log)
244
+ #
245
+ # Nothing emits [chat-route] on the admin surface and nothing emits
246
+ # [admin-auth] on the public one, so the row the OTHER surface would have
247
+ # shown is not evidence of anything. Reading admin against a four-row
248
+ # sequence is what made every healthy admin chat look like a routing
249
+ # regression (Task 2042).
220
250
  #
221
- # [edge-admin] inbound method=POST path=/api/admin/chat token=present … (edge.log)
222
- # [admin-auth] outcome=accept … (server.log)
223
- # [chat-route] entered route=admin method=POST cacheKey=… … (server.log)
224
- # [edge-admin] outcome method=POST path=/api/admin/chat status=200 … (edge.log)
251
+ # A row missing from the surface's OWN sequence is diagnostic.
252
+ # Both surfaces: zero edge inbound = browser/network problem; edge outcome
253
+ # missing = upstream proxy crash.
254
+ # Admin: [admin-auth] outcome is the handler-side checkpoint. Missing against
255
+ # a present inbound = handler unreachable; outcome=reject = auth failure;
256
+ # outcome=accept already proves the route matched, because requireAdminSession
257
+ # is middleware mounted on /send (webchat.ts), so no further handler-side row
258
+ # is expected.
259
+ # Public: [chat-route] entered is the handler-side checkpoint. Missing against
260
+ # a present inbound for /api/chat = public-route routing regression.
225
261
  #
226
- # Any missing row in that sequence is itself diagnostic. Three-strike rule:
227
- # zero edge inbound = browser/network problem; inbound present + admin-auth
228
- # missing = handler unreachable; admin-auth=reject = auth failure; entered
229
- # missing after accept = Hono routing regression; outcome missing = upstream
230
- # proxy crash.
262
+ # The two log families are stamped differently and neither is negotiable here:
263
+ # edge.log self-stamps `ts=` inside the line (Task 2024), server.log is stamped
264
+ # at the line start by server-init-line-stamper.cjs. Reading only the leading
265
+ # 19 characters yielded `[edge-admin] ts=202` on every edge line, so the window
266
+ # filter and the sort both have to read whichever form the line carries.
231
267
  chat_attempts_mode() {
232
268
  local minutes="${1:-5}"
233
269
  if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then
@@ -236,57 +272,125 @@ chat_attempts_mode() {
236
272
  fi
237
273
 
238
274
  local EDGE_LOG="$CONFIG_DIR/logs/edge.log"
239
- local since_epoch
240
- since_epoch=$(date -u -v-"${minutes}"M +%s 2>/dev/null || date -u -d "${minutes} minutes ago" +%s 2>/dev/null || true)
241
- if [[ -z "$since_epoch" ]]; then
275
+ local since_iso
276
+ since_iso=$(date -u -v-"${minutes}"M +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -u -d "${minutes} minutes ago" +%Y-%m-%dT%H:%M:%S 2>/dev/null || true)
277
+ if [[ -z "$since_iso" ]]; then
242
278
  echo "Error: could not compute time window — neither BSD nor GNU date available" >&2
243
279
  exit 2
244
280
  fi
245
281
 
246
- # Each log line carries an ISO-8601 timestamp at the head (server.log via
247
- # pino, edge.log via console both written line-buffered with leading
248
- # `YYYY-MM-DDTHH:MM:SS` then either `.sssZ` or a space). awk filters to
249
- # the time window; sed prefixes the source filename so the timeline
250
- # remains attributable; sort -k1 keeps chronological order.
282
+ # Emits `<ts>\t<source>\t<line>` into $tmp, where <ts> is the line's own
283
+ # timestamp to millisecond precision, or `-` when the line carries none.
284
+ # ISO-8601 UTC strings compare lexicographically in time order, which is why
285
+ # the window test is a string comparison and not a `date` subprocess per
286
+ # matched line, which is what the previous form spawned on both dialects.
287
+ #
288
+ # Milliseconds are load-bearing, not decoration. Both families emit
289
+ # `toISOString()`, and a chat's three checkpoints normally complete inside
290
+ # one wall-clock second, so a key truncated to seconds collides on every row
291
+ # and the sort falls back to file-append order — which puts the edge rows
292
+ # first and reproduces the clustering this mode was fixed to stop.
293
+ #
294
+ # An undated line reaches $tmp with `-` as its key. It cannot be placed in
295
+ # the window or in the sequence, so it is listed separately below rather
296
+ # than merged into the timeline. It is never discarded: a matched line
297
+ # dropped in silence is the exact fault this mode was fixed for.
251
298
  local filter_awk='
252
- BEGIN { since = '"$since_epoch"' }
253
299
  {
254
- ts = substr($0, 1, 19)
255
- gsub("T", " ", ts)
256
- cmd = "date -u -j -f \"%Y-%m-%d %H:%M:%S\" \"" ts "\" +%s 2>/dev/null || date -u -d \"" ts "\" +%s 2>/dev/null"
257
- cmd | getline epoch
258
- close(cmd)
259
- if (epoch >= since) print
300
+ ts = "-"
301
+ if (match($0, /ts=[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9][0-9][0-9]/)) {
302
+ ts = substr($0, RSTART + 3, 23)
303
+ } else if ($0 ~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9][0-9][0-9]/) {
304
+ ts = substr($0, 1, 23)
305
+ }
306
+ if (ts == "-" || ts >= since) print ts "\t" src "\t" $0
260
307
  }
261
308
  '
262
309
 
263
- local hits=0
264
310
  local tmp
265
311
  tmp=$(mktemp -t chat-attempts.XXXXXX)
266
312
  trap 'rm -f "$tmp"' EXIT
267
313
 
268
314
  if [[ -f "$EDGE_LOG" ]]; then
269
- grep -E "$CHAT_ATTEMPT_GREP" "$EDGE_LOG" 2>/dev/null | awk "$filter_awk" | sed 's|^|edge.log |' >> "$tmp" || true
315
+ grep -E "$CHAT_ATTEMPT_GREP" "$EDGE_LOG" 2>/dev/null \
316
+ | awk -v since="$since_iso" -v src="edge.log" "$filter_awk" >> "$tmp" || true
270
317
  fi
271
318
  if [[ -f "$SERVER_LOG" ]]; then
272
- grep -E "$CHAT_ATTEMPT_GREP" "$SERVER_LOG" 2>/dev/null | awk "$filter_awk" | sed 's|^|server.log|' >> "$tmp" || true
319
+ grep -E "$CHAT_ATTEMPT_GREP" "$SERVER_LOG" 2>/dev/null \
320
+ | awk -v since="$since_iso" -v src="server.log" "$filter_awk" >> "$tmp" || true
273
321
  fi
274
322
 
275
- hits=$(wc -l < "$tmp" | tr -d ' ')
276
- if [[ "$hits" -eq 0 ]]; then
277
- echo "# chat-attempts (last ${minutes} min) no events" >&2
278
- echo "-- trailer: type=chat-attempts window_min=${minutes} matches=0 searched=edge.log,server.log" >&2
323
+ # Per-source tallies. One aggregate count is what made the miss silent:
324
+ # `edge=0` folded into the same "no events" line as a genuinely quiet window,
325
+ # so a reader that could not see the edge read as an edge that said nothing.
326
+ local counts edge_hits edge_undated server_hits server_undated hits undated
327
+ counts=$(awk -F'\t' '
328
+ { if ($1 == "-") u[$2]++; else m[$2]++ }
329
+ END { printf "%d %d %d %d", m["edge.log"], u["edge.log"], m["server.log"], u["server.log"] }
330
+ ' "$tmp")
331
+ read -r edge_hits edge_undated server_hits server_undated <<< "$counts"
332
+ hits=$((edge_hits + server_hits))
333
+ undated=$((edge_undated + server_undated))
334
+
335
+ local edge_state="present"
336
+ [[ -f "$EDGE_LOG" ]] || edge_state="absent:$EDGE_LOG"
337
+ local server_state="present"
338
+ [[ -f "$SERVER_LOG" ]] || server_state="absent:$SERVER_LOG"
339
+
340
+ local trailer="-- trailer: type=chat-attempts window_min=${minutes} matches=${hits} edge=${edge_hits} server=${server_hits} undated=${undated} searched=edge.log,server.log"
341
+
342
+ # Nothing matched at all, dated or not. Only here can the mode say anything
343
+ # about whether a request arrived, and even here only if it could read the
344
+ # edge log — a file it never opened proves nothing about the traffic.
345
+ if [[ "$hits" -eq 0 && "$undated" -eq 0 ]]; then
346
+ echo "# chat-attempts (last ${minutes} min) — no ingress checkpoints matched" >&2
347
+ echo "# edge.log=${edge_state} server.log=${server_state}" >&2
348
+ if [[ -f "$EDGE_LOG" ]]; then
349
+ # What a genuine zero means, now that the patterns track the shipped
350
+ # lines: the first checkpoint is the edge accepting a connection, so its
351
+ # absence places the fault before this device. Saying it here is the
352
+ # point of the fix — the reader used to produce this same output when it
353
+ # simply could not see the edge lines.
354
+ echo "# The edge patterns track the shipped \`[edge-admin] ts=<ISO> …\` shape, so this" >&2
355
+ echo "# is an absence in the logs, not in the reader: no request reached the edge in" >&2
356
+ echo "# this window. Look at the browser, DNS, or the tunnel — the admin server is" >&2
357
+ echo "# never in the path of a request that never arrived." >&2
358
+ else
359
+ echo "# edge.log is not on disk, so the edge checkpoints could not be read at all." >&2
360
+ echo "# This says nothing about whether a request arrived. Check that the edge service" >&2
361
+ echo "# is running and writing to the path above before drawing any conclusion." >&2
362
+ fi
363
+ echo "$trailer" >&2
279
364
  exit 1
280
365
  fi
281
366
 
282
- echo "# chat-attempts (last ${minutes} min — chronological)"
283
- echo ""
284
- # Sort by the timestamp column that begins immediately after the source-file
285
- # prefix. Prefix is "edge.log " or "server.log" both 10 chars before the
286
- # timestamp begins, so column 11 onward is the chronological key.
287
- sort -k1.11 "$tmp"
367
+ if [[ "$hits" -gt 0 ]]; then
368
+ echo "# chat-attempts (last ${minutes} min — chronological)"
369
+ echo ""
370
+ # Sort on the extracted timestamp column, then drop it. The previous form
371
+ # sorted on a fixed byte offset that only landed on a timestamp for
372
+ # server.log lines, so edge rows clustered rather than interleaved. `-s`
373
+ # keeps edge before server on an exact millisecond tie; LC_ALL=C makes the
374
+ # byte ordering deterministic across locales.
375
+ LC_ALL=C sort -t$'\t' -k1,1 -s "$tmp" \
376
+ | awk -F'\t' '$1 != "-" { printf "%-10s %s\n", $2, $3 }'
377
+ else
378
+ echo "# chat-attempts (last ${minutes} min) — no datable checkpoint in the window"
379
+ fi
380
+
381
+ # Matched but undatable. Shown, because a matched edge line is evidence that
382
+ # a request DID arrive, and suppressing it while reporting silence is how
383
+ # this mode misread the edge in the first place. Listed in file order and
384
+ # outside the timeline, because an undated line has no position in it.
385
+ if [[ "$undated" -gt 0 ]]; then
386
+ echo ""
387
+ echo "# ${undated} matched line(s) carry no timestamp and cannot be placed in the window."
388
+ echo "# An edge line written before the \`ts=\` stamp carries no clock. File order:"
389
+ awk -F'\t' '$1 == "-" { printf "%-10s %s\n", $2, $3 }' "$tmp"
390
+ fi
391
+
288
392
  echo "" >&2
289
- echo "-- trailer: type=chat-attempts window_min=${minutes} matches=${hits} searched=edge.log,server.log" >&2
393
+ echo "$trailer" >&2
290
394
  exit 0
291
395
  }
292
396
 
@@ -188,11 +188,287 @@ case_mem_absent() {
188
188
  return 0
189
189
  }
190
190
 
191
+ # --- chat-attempts helpers (Task 2032) ---
192
+ # The fixtures are dated off the same clock the script computes its window
193
+ # from, so no case depends on a literal timestamp ageing out.
194
+ iso_minutes_ago() {
195
+ local m="$1"
196
+ date -u -v-"${m}"M +%Y-%m-%dT%H:%M:%S 2>/dev/null \
197
+ || date -u -d "${m} minutes ago" +%Y-%m-%dT%H:%M:%S 2>/dev/null
198
+ }
199
+
200
+ # --- Case 6 (Task 2032): all four patterns match, interleaved by time ---
201
+ case_chat_attempts_both_surfaces() {
202
+ local root="/tmp/maxy-logs-read-test-6-$$"
203
+ local script
204
+ script=$(setup_install_tree "$root")
205
+ local logs="$HOME/.$(basename "$root")/logs"
206
+ # Four checkpoint patterns in one window takes TWO chats, not one: no single
207
+ # surface emits four (Task 2042). A public chat then an admin chat, three
208
+ # rows each. Distinct milliseconds inside two minute-slots, so the ordering
209
+ # assertion below is strict and exercises the millisecond sort key.
210
+ local tp ta
211
+ tp=$(iso_minutes_ago 4); ta=$(iso_minutes_ago 2)
212
+
213
+ # Shipped shapes: edge.log self-stamps `ts=` after the tag (edge.ts:188,199);
214
+ # server.log is stamped at the line start by server-init-line-stamper.cjs.
215
+ # Both edge rows fire for any /api/ path (lib/proxy-log-path.ts:28-30).
216
+ {
217
+ echo "[edge-admin] ts=${tp}.100Z inbound method=POST path=/api/chat token=absent allowlist-branch=passthrough"
218
+ echo "[edge-admin] ts=${tp}.300Z outcome method=POST path=/api/chat status=200 duration_ms=200"
219
+ echo "[edge-admin] ts=${ta}.100Z inbound method=POST path=/api/webchat/send token=present allowlist-branch=passthrough"
220
+ echo "[edge-admin] ts=${ta}.300Z outcome method=POST path=/api/webchat/send status=200 duration_ms=200"
221
+ } > "$logs/edge.log"
222
+ {
223
+ echo "${tp}.200Z [chat-route] entered route=public method=POST"
224
+ echo "${ta}.200Z [admin-auth] outcome=accept path=/api/webchat/send cacheKey=deadbeef…"
225
+ } > "$logs/server.log"
226
+
227
+ local stdout stderr rc=0
228
+ stdout=$("$script" --tail chat-attempts 5 2>/tmp/maxy-logs-read-stderr-6-$$) || rc=$?
229
+ stderr=$(cat /tmp/maxy-logs-read-stderr-6-$$)
230
+ rm -f /tmp/maxy-logs-read-stderr-6-$$
231
+
232
+ cleanup_install_tree "$root"
233
+
234
+ if [[ $rc -ne 0 ]]; then echo " expected exit 0, got $rc"; return 1; fi
235
+ if [[ "$stdout" != *"inbound"* ]]; then echo " timeline missing edge inbound"; return 1; fi
236
+ if [[ "$stdout" != *"[edge-admin]"*"outcome"* ]]; then echo " timeline missing edge outcome"; return 1; fi
237
+ if [[ "$stdout" != *"[admin-auth] outcome=accept"* ]]; then echo " timeline missing admin-auth"; return 1; fi
238
+ if [[ "$stdout" != *"[chat-route] entered route=public"* ]]; then echo " timeline missing chat-route"; return 1; fi
239
+ if [[ "$stderr" != *"edge=4"* ]]; then echo " trailer missing edge=4: $stderr"; return 1; fi
240
+ if [[ "$stderr" != *"server=2"* ]]; then echo " trailer missing server=2: $stderr"; return 1; fi
241
+ if [[ "$stderr" != *"undated=0"* ]]; then echo " trailer missing undated=0: $stderr"; return 1; fi
242
+
243
+ # Chronological across the two log families: the public chat's edge inbound
244
+ # precedes its chat-route row, which precedes its edge outcome. A fixed-column
245
+ # sort put the two edge rows adjacent instead, which is the ordering defect
246
+ # this pins.
247
+ local first mid last
248
+ first=$(echo "$stdout" | grep -n 'inbound' | head -1 | cut -d: -f1)
249
+ mid=$(echo "$stdout" | grep -n '\[chat-route\] entered' | head -1 | cut -d: -f1)
250
+ last=$(echo "$stdout" | grep -n '\[edge-admin\].*outcome' | head -1 | cut -d: -f1)
251
+ if [[ -z "$first" || -z "$mid" || -z "$last" ]]; then
252
+ echo " could not locate rows: inbound=$first chat-route=$mid outcome=$last"; return 1
253
+ fi
254
+ if [[ "$first" -ge "$mid" || "$mid" -ge "$last" ]]; then
255
+ echo " rows not in chronological order: inbound=$first chat-route=$mid outcome=$last"
256
+ return 1
257
+ fi
258
+ return 0
259
+ }
260
+
261
+ # --- Case 7 (Task 2032): genuine zero says so distinctly ---
262
+ case_chat_attempts_genuine_zero() {
263
+ local root="/tmp/maxy-logs-read-test-7-$$"
264
+ local script
265
+ script=$(setup_install_tree "$root")
266
+ local logs="$HOME/.$(basename "$root")/logs"
267
+ local t0
268
+ t0=$(iso_minutes_ago 1)
269
+ # Both files exist and hold traffic; none of it is a chat-ingress checkpoint.
270
+ echo "[edge] ts=${t0}.000Z upstream http error path=/api/whatsapp err=ECONNREFUSED err-class=econnrefused-coldstart upstream=127.0.0.1:19201" > "$logs/edge.log"
271
+ echo "${t0}.000Z [server-init] watchdog enabled (ping-interval-ms=15000 deadline-ms=30000)" > "$logs/server.log"
272
+
273
+ local stderr rc=0
274
+ "$script" --tail chat-attempts 5 >/dev/null 2>/tmp/maxy-logs-read-stderr-7-$$ || rc=$?
275
+ stderr=$(cat /tmp/maxy-logs-read-stderr-7-$$)
276
+ rm -f /tmp/maxy-logs-read-stderr-7-$$
277
+
278
+ cleanup_install_tree "$root"
279
+
280
+ if [[ $rc -ne 1 ]]; then echo " expected exit 1, got $rc"; return 1; fi
281
+ if [[ "$stderr" != *"edge=0"* || "$stderr" != *"server=0"* ]]; then
282
+ echo " trailer missing per-source zeroes: $stderr"; return 1
283
+ fi
284
+ if [[ "$stderr" != *"no request reached the edge"* ]]; then
285
+ echo " zero-hit message does not state what a genuine zero means: $stderr"; return 1
286
+ fi
287
+ return 0
288
+ }
289
+
290
+ # --- Case 8 (Task 2032): the window excludes an older edge line ---
291
+ case_chat_attempts_window() {
292
+ local root="/tmp/maxy-logs-read-test-8-$$"
293
+ local script
294
+ script=$(setup_install_tree "$root")
295
+ local logs="$HOME/.$(basename "$root")/logs"
296
+ local recent old
297
+ recent=$(iso_minutes_ago 1)
298
+ old=$(iso_minutes_ago 90)
299
+ {
300
+ echo "[edge-admin] ts=${old}.000Z inbound method=POST path=/api/webchat/send token=present allowlist-branch=passthrough"
301
+ echo "[edge-admin] ts=${recent}.000Z inbound method=GET path=/api/admin/version token=absent allowlist-branch=passthrough"
302
+ } > "$logs/edge.log"
303
+ : > "$logs/server.log"
304
+
305
+ local stdout stderr rc=0
306
+ stdout=$("$script" --tail chat-attempts 5 2>/tmp/maxy-logs-read-stderr-8-$$) || rc=$?
307
+ stderr=$(cat /tmp/maxy-logs-read-stderr-8-$$)
308
+ rm -f /tmp/maxy-logs-read-stderr-8-$$
309
+
310
+ cleanup_install_tree "$root"
311
+
312
+ if [[ $rc -ne 0 ]]; then echo " expected exit 0, got $rc"; return 1; fi
313
+ if [[ "$stdout" == *"$old"* ]]; then echo " out-of-window line was not filtered"; return 1; fi
314
+ if [[ "$stdout" != *"$recent"* ]]; then echo " in-window line missing"; return 1; fi
315
+ if [[ "$stderr" != *"edge=1"* ]]; then echo " trailer edge count wrong: $stderr"; return 1; fi
316
+ return 0
317
+ }
318
+
319
+ # --- Case 9 (Task 2032): all four patterns inside one second still order ---
320
+ # A chat POST completes in a few hundred milliseconds, so its checkpoints share
321
+ # a wall-clock second; an admin chat and a public chat overlapping in that same
322
+ # second put all four patterns there at once. A sort key truncated to seconds
323
+ # collides on every row and falls back to file-append order, which puts the edge
324
+ # rows first — the clustering this mode was fixed to stop. Six rows, because no
325
+ # single surface emits four checkpoints (Task 2042).
326
+ case_chat_attempts_subsecond_order() {
327
+ local root="/tmp/maxy-logs-read-test-9-$$"
328
+ local script
329
+ script=$(setup_install_tree "$root")
330
+ local logs="$HOME/.$(basename "$root")/logs"
331
+ local s
332
+ s=$(iso_minutes_ago 1)
333
+ {
334
+ echo "[edge-admin] ts=${s}.010Z inbound method=POST path=/api/webchat/send token=present allowlist-branch=passthrough"
335
+ echo "[edge-admin] ts=${s}.060Z inbound method=POST path=/api/chat token=absent allowlist-branch=passthrough"
336
+ echo "[edge-admin] ts=${s}.090Z outcome method=POST path=/api/chat status=200 duration_ms=30"
337
+ echo "[edge-admin] ts=${s}.230Z outcome method=POST path=/api/webchat/send status=200 duration_ms=220"
338
+ } > "$logs/edge.log"
339
+ {
340
+ echo "${s}.050Z [admin-auth] outcome=accept path=/api/webchat/send cacheKey=deadbeef"
341
+ echo "${s}.070Z [chat-route] entered route=public method=POST"
342
+ } > "$logs/server.log"
343
+
344
+ local stdout rc=0
345
+ stdout=$("$script" --tail chat-attempts 5 2>/dev/null) || rc=$?
346
+
347
+ cleanup_install_tree "$root"
348
+
349
+ if [[ $rc -ne 0 ]]; then echo " expected exit 0, got $rc"; return 1; fi
350
+ local n_in n_auth n_route n_out
351
+ n_in=$(echo "$stdout" | grep -n 'inbound' | head -1 | cut -d: -f1)
352
+ n_auth=$(echo "$stdout" | grep -n '\[admin-auth\]' | head -1 | cut -d: -f1)
353
+ n_route=$(echo "$stdout" | grep -n '\[chat-route\]' | head -1 | cut -d: -f1)
354
+ n_out=$(echo "$stdout" | grep -n '\[edge-admin\].*outcome' | head -1 | cut -d: -f1)
355
+ if [[ -z "$n_in" || -z "$n_auth" || -z "$n_route" || -z "$n_out" ]]; then
356
+ echo " missing a row: in=$n_in auth=$n_auth route=$n_route out=$n_out"; return 1
357
+ fi
358
+ if [[ "$n_in" -ge "$n_auth" || "$n_auth" -ge "$n_route" || "$n_route" -ge "$n_out" ]]; then
359
+ echo " sub-second rows out of order: in=$n_in auth=$n_auth route=$n_route out=$n_out"
360
+ echo "$stdout"
361
+ return 1
362
+ fi
363
+ return 0
364
+ }
365
+
366
+ # --- Case 10 (Task 2032): an undated match is never narrated as edge silence ---
367
+ # The grep matches the `[edge-admin] inbound` shape written before the `ts=`
368
+ # no timestamp. Such a line cannot be placed in the window, but it is evidence
369
+ # that a request DID reach the edge, so the mode must not answer with the
370
+ # genuine-zero verdict.
371
+ case_chat_attempts_undated_not_silence() {
372
+ local root="/tmp/maxy-logs-read-test-10-$$"
373
+ local script
374
+ script=$(setup_install_tree "$root")
375
+ local logs="$HOME/.$(basename "$root")/logs"
376
+ echo "[edge-admin] inbound method=POST path=/api/webchat/send token=present allowlist-branch=passthrough" > "$logs/edge.log"
377
+ : > "$logs/server.log"
378
+
379
+ local stdout stderr rc=0
380
+ stdout=$("$script" --tail chat-attempts 5 2>/tmp/maxy-logs-read-stderr-10-$$) || rc=$?
381
+ stderr=$(cat /tmp/maxy-logs-read-stderr-10-$$)
382
+ rm -f /tmp/maxy-logs-read-stderr-10-$$
383
+
384
+ cleanup_install_tree "$root"
385
+
386
+ if [[ $rc -ne 0 ]]; then echo " expected exit 0 (a match is a match), got $rc"; return 1; fi
387
+ if [[ "$stderr" == *"no request reached the edge"* || "$stdout" == *"no request reached the edge"* ]]; then
388
+ echo " matched an edge line and still claimed no request reached the edge"; return 1
389
+ fi
390
+ if [[ "$stdout" != *"carry no timestamp"* ]]; then
391
+ echo " undated match not surfaced in the output: $stdout"; return 1
392
+ fi
393
+ if [[ "$stdout" != *"[edge-admin] inbound"* ]]; then
394
+ echo " undated line itself not printed"; return 1
395
+ fi
396
+ if [[ "$stderr" != *"undated=1"* ]]; then echo " trailer undated count wrong: $stderr"; return 1; fi
397
+ return 0
398
+ }
399
+
400
+ # --- Case 11 (Task 2032): a missing edge.log is reader blindness, not silence ---
401
+ case_chat_attempts_edge_log_absent() {
402
+ local root="/tmp/maxy-logs-read-test-11-$$"
403
+ local script
404
+ script=$(setup_install_tree "$root")
405
+ local logs="$HOME/.$(basename "$root")/logs"
406
+ # server.log exists, edge.log does not.
407
+ : > "$logs/server.log"
408
+ rm -f "$logs/edge.log"
409
+
410
+ local stderr rc=0
411
+ "$script" --tail chat-attempts 5 >/dev/null 2>/tmp/maxy-logs-read-stderr-11-$$ || rc=$?
412
+ stderr=$(cat /tmp/maxy-logs-read-stderr-11-$$)
413
+ rm -f /tmp/maxy-logs-read-stderr-11-$$
414
+
415
+ cleanup_install_tree "$root"
416
+
417
+ if [[ $rc -ne 1 ]]; then echo " expected exit 1, got $rc"; return 1; fi
418
+ if [[ "$stderr" == *"no request reached the edge"* ]]; then
419
+ echo " claimed no request arrived while edge.log was unreadable"; return 1
420
+ fi
421
+ if [[ "$stderr" != *"edge.log is not on disk"* ]]; then
422
+ echo " did not state that the edge checkpoints were unreadable: $stderr"; return 1
423
+ fi
424
+ return 0
425
+ }
426
+
427
+ # --- Case 12 (Task 2042): usage names each surface, not four rows per chat ---
428
+ # The mode greps four checkpoint patterns, but no single chat emits four: the
429
+ # admin webchat has no [chat-route] row and public chat has no [admin-auth]
430
+ # row. Usage that promised four made every healthy admin chat read as a
431
+ # routing regression, so the count and both surface paths are pinned here.
432
+ case_usage_checkpoints_per_surface() {
433
+ local root="/tmp/maxy-logs-read-test-12-$$"
434
+ local script
435
+ script=$(setup_install_tree "$root")
436
+
437
+ local stderr rc=0
438
+ "$script" >/dev/null 2>/tmp/maxy-logs-read-stderr-12-$$ || rc=$?
439
+ stderr=$(cat /tmp/maxy-logs-read-stderr-12-$$)
440
+ rm -f /tmp/maxy-logs-read-stderr-12-$$
441
+
442
+ cleanup_install_tree "$root"
443
+
444
+ if [[ $rc -ne 2 ]]; then echo " expected usage exit 2, got $rc"; return 1; fi
445
+ if [[ "$stderr" == *"four ingress checkpoints"* ]]; then
446
+ echo " usage still claims four checkpoints per chat"; return 1
447
+ fi
448
+ if [[ "$stderr" == *"quartet"* ]]; then
449
+ echo " usage still calls the checkpoints a quartet"; return 1
450
+ fi
451
+ if [[ "$stderr" != *"/api/webchat/send"* ]]; then
452
+ echo " usage does not name the admin surface path"; return 1
453
+ fi
454
+ if [[ "$stderr" != *"/api/chat"* ]]; then
455
+ echo " usage does not name the public surface path"; return 1
456
+ fi
457
+ return 0
458
+ }
459
+
191
460
  run_case "sessionKey file present → exit 0" case_file_present
192
461
  run_case "no file → exit 1 + missing-on-resolve" case_missing
193
462
  run_case "ambiguous prefix → exit 1, refuses to pick" case_ambiguous
194
463
  run_case "--tail mem present → exit 0 + sampler lines" case_mem_present
195
464
  run_case "--tail mem absent → exit 1 + not-found message" case_mem_absent
465
+ run_case "chat-attempts matches both surfaces and interleaves" case_chat_attempts_both_surfaces
466
+ run_case "chat-attempts genuine zero states what it means" case_chat_attempts_genuine_zero
467
+ run_case "chat-attempts window excludes an older line" case_chat_attempts_window
468
+ run_case "chat-attempts orders all four patterns inside one second" case_chat_attempts_subsecond_order
469
+ run_case "chat-attempts undated match is not edge silence" case_chat_attempts_undated_not_silence
470
+ run_case "chat-attempts absent edge.log is reader blindness" case_chat_attempts_edge_log_absent
471
+ run_case "usage names each surface's checkpoints" case_usage_checkpoints_per_surface
196
472
 
197
473
  echo ""
198
474
  echo "================================================"
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ # The one documented action that makes a fresh worktree able to run the
3
+ # platform/ui suite. Task 2034.
4
+ #
5
+ # A git worktree carries tracked files only. platform/node_modules,
6
+ # platform/ui/node_modules and platform/lib/*/dist are gitignored build output, so
7
+ # a fresh worktree has none of them, and `npm install` inside platform/ui creates
8
+ # neither the hoisted platform/node_modules nor any lib dist. Without this, the
9
+ # suite fails on absent artifacts in a way that reads as flaky tests.
10
+ #
11
+ # Two things here are not obvious and are the reason this is a script:
12
+ #
13
+ # 1. tsc is called as platform/node_modules/.bin/tsc, never bare. platform's
14
+ # own package.json declares no typescript; it hoists one from 28 workspace
15
+ # members. Inside an npm script npm prepends node_modules/.bin to PATH and a
16
+ # bare `tsc` resolves; from a bare shell it does not, and the tsc on PATH is
17
+ # an unrelated program that refuses to run.
18
+ #
19
+ # 2. The libs are built in a loop, not through `npm run build:lib`. build:lib
20
+ # chains its 27 compiler runs with &&, and one lib emits its output while
21
+ # still reporting type errors, which would abort every run after it. So each
22
+ # lib is built independently and the final gate is artifact presence, not
23
+ # compiler exit status — the only check that separates "emitted with type
24
+ # errors" from "emitted nothing".
25
+
26
+ set -uo pipefail
27
+
28
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
29
+ PLATFORM="$(cd "$SCRIPT_DIR/.." && pwd -P)"
30
+ TSC="$PLATFORM/node_modules/.bin/tsc"
31
+ GATE="$PLATFORM/ui/scripts/check-worktree-provisioned.mjs"
32
+
33
+ echo "[provision] platform: $PLATFORM"
34
+
35
+ echo "[provision] stage 1/3: npm install in platform/"
36
+ if ! (cd "$PLATFORM" && npm install); then
37
+ echo "[provision] FAILED at stage 1: npm install in platform/" >&2
38
+ exit 1
39
+ fi
40
+
41
+ echo "[provision] stage 2/3: npm install in platform/ui"
42
+ if ! (cd "$PLATFORM/ui" && npm install); then
43
+ echo "[provision] FAILED at stage 2: npm install in platform/ui" >&2
44
+ exit 1
45
+ fi
46
+
47
+ if [ ! -x "$TSC" ]; then
48
+ echo "[provision] FAILED: $TSC absent after stage 1." >&2
49
+ echo "[provision] typescript hoists to platform/node_modules from the workspace" >&2
50
+ echo "[provision] members; if it is missing, stage 1 did not complete." >&2
51
+ exit 1
52
+ fi
53
+
54
+ echo "[provision] stage 3/3: building platform/lib/*/dist"
55
+ emitted_with_errors=()
56
+ for tsconfig in "$PLATFORM"/lib/*/tsconfig.json; do
57
+ lib="$(basename "$(dirname "$tsconfig")")"
58
+ if "$TSC" -p "$tsconfig" >/dev/null 2>&1; then
59
+ printf ' %-32s ok\n' "$lib"
60
+ elif [ -f "$PLATFORM/lib/$lib/dist/index.js" ]; then
61
+ printf ' %-32s emitted, with type errors\n' "$lib"
62
+ emitted_with_errors+=("$lib")
63
+ else
64
+ printf ' %-32s FAILED, emitted nothing\n' "$lib"
65
+ fi
66
+ done
67
+
68
+ if [ ${#emitted_with_errors[@]} -gt 0 ]; then
69
+ echo "[provision] ${#emitted_with_errors[@]} lib(s) emitted output while reporting type"
70
+ echo "[provision] errors: ${emitted_with_errors[*]}. Their dist is present, so the suite"
71
+ echo "[provision] can run. The type errors themselves are out of scope for provisioning."
72
+ fi
73
+
74
+ echo "[provision] verifying against the gate"
75
+ if ! node "$GATE"; then
76
+ echo "[provision] FAILED: the tree is still unprovisioned after all three stages." >&2
77
+ exit 1
78
+ fi
79
+
80
+ echo "[provision] done. platform/ui is ready: cd platform/ui && npm test"