@rubytech/create-sitedesk-code 0.1.524 → 0.1.525

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 (117) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/superpowers-sprint/SKILL.md +26 -0
  3. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  4. package/payload/platform/plugins/memory/PLUGIN.md +10 -0
  5. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +5 -1
  6. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
  7. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts +2 -0
  8. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts.map +1 -0
  9. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js +141 -0
  10. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js.map +1 -0
  11. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts +1 -1
  12. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts.map +1 -1
  13. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +6 -0
  14. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
  15. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts +45 -0
  16. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts.map +1 -0
  17. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js +122 -0
  18. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js +26 -0
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js.map +1 -1
  21. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts +2 -0
  22. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts.map +1 -0
  23. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js +201 -0
  24. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js.map +1 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +89 -33
  27. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
  29. package/payload/platform/plugins/scheduling/PLUGIN.md +6 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts +2 -0
  31. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts.map +1 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js +148 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js.map +1 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +83 -6
  35. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
  36. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +90 -5
  37. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
  38. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js +111 -2
  39. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js.map +1 -1
  40. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +38 -0
  41. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  42. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +116 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  44. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +13 -1
  45. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  46. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +81 -8
  47. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js +39 -0
  49. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js.map +1 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +9 -1
  52. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  53. package/payload/platform/scripts/__tests__/check-canonical-tool-names.test.sh +162 -0
  54. package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +53 -0
  55. package/payload/platform/scripts/__tests__/task-id-citation.test.sh +72 -0
  56. package/payload/platform/scripts/check-canonical-tool-names.mjs +36 -4
  57. package/payload/platform/scripts/check-no-task-id-leaks.mjs +80 -62
  58. package/payload/platform/scripts/lib/canonical-tool-names.mjs +130 -26
  59. package/payload/platform/scripts/lib/task-id-citation.mjs +67 -0
  60. package/payload/platform/.docs/search-surface-contract.md +0 -58
  61. package/payload/platform/docs/superpowers/plans/2026-06-02-task-610-follower-202-retry.md +0 -372
  62. package/payload/platform/docs/superpowers/plans/2026-06-04-public-agent-knowledge-delivery.md +0 -230
  63. package/payload/platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +0 -544
  64. package/payload/platform/docs/superpowers/plans/2026-07-11-task-1557-operator-on-behalf-preference-attribution.md +0 -542
  65. package/payload/platform/docs/superpowers/plans/2026-07-13-account-schema-ontology-projection.md +0 -547
  66. package/payload/platform/docs/superpowers/plans/2026-07-14-graph-top-level-labels-ontology-single-source.md +0 -458
  67. package/payload/platform/docs/superpowers/plans/2026-07-17-task-1736-retire-mcp-stderr-tee.md +0 -397
  68. package/payload/platform/docs/superpowers/plans/2026-07-18-graph-native-ledger.md +0 -807
  69. package/payload/platform/docs/superpowers/plans/2026-07-18-ledger-write-path-fix.md +0 -109
  70. package/payload/platform/docs/superpowers/plans/2026-07-20-storage-pages-create.md +0 -82
  71. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +0 -673
  72. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +0 -1244
  73. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +0 -462
  74. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +0 -321
  75. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1829-platform-ui-typecheck.md +0 -830
  76. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +0 -2177
  77. package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +0 -124
  78. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +0 -107
  79. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +0 -205
  80. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +0 -748
  81. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +0 -1716
  82. package/payload/platform/docs/superpowers/plans/2026-07-23-task-1930-adherence-enforcement.md +0 -422
  83. package/payload/platform/docs/superpowers/plans/2026-07-24-task-1942-subaccount-switcher-brand-head.md +0 -327
  84. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1974-uploads-intake-inbox.md +0 -235
  85. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1976-intra-folder-hygiene.md +0 -215
  86. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +0 -555
  87. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1926-portal-push-concurrency-guard.md +0 -482
  88. package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +0 -394
  89. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +0 -624
  90. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md +0 -303
  91. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2052-account-machinery-write-fence.md +0 -250
  92. package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +0 -65
  93. package/payload/platform/docs/superpowers/specs/2026-06-02-task-610-follower-202-retry-design.md +0 -116
  94. package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +0 -170
  95. package/payload/platform/docs/superpowers/specs/2026-07-11-task-1557-operator-on-behalf-preference-attribution-design.md +0 -90
  96. package/payload/platform/docs/superpowers/specs/2026-07-13-account-schema-ontology-projection-design.md +0 -178
  97. package/payload/platform/docs/superpowers/specs/2026-07-14-graph-top-level-labels-ontology-single-source-design.md +0 -119
  98. package/payload/platform/docs/superpowers/specs/2026-07-17-task-1736-retire-mcp-stderr-tee-design.md +0 -128
  99. package/payload/platform/docs/superpowers/specs/2026-07-18-graph-native-ledger-design.md +0 -241
  100. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +0 -177
  101. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +0 -201
  102. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +0 -217
  103. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +0 -80
  104. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +0 -97
  105. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md +0 -163
  106. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +0 -163
  107. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +0 -121
  108. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +0 -103
  109. package/payload/platform/docs/superpowers/specs/2026-07-23-task-1930-adherence-enforcement-design.md +0 -81
  110. package/payload/platform/docs/superpowers/specs/2026-07-24-task-1942-subaccount-switcher-brand-head-design.md +0 -35
  111. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +0 -232
  112. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1926-portal-push-concurrency-guard-design.md +0 -200
  113. package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +0 -169
  114. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +0 -139
  115. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +0 -127
  116. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2052-account-machinery-write-fence-design.md +0 -150
  117. package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +0 -155
@@ -1,35 +0,0 @@
1
- # Task 1942 — Sub-account switcher moves to the sidebar brand head
2
-
3
- Design doc. Source task: `.tasks/1942-subaccount-switcher-moves-to-sidebar-brand-head.md`.
4
-
5
- ## Goal
6
-
7
- Open the sub-account picker by clicking the account name at the top of the sidebar, not from the footer kebab. Remove the switcher from the footer menu.
8
-
9
- ## Resolved decisions (brainstorm)
10
-
11
- 1. **Click target:** the whole brand head (`side-brand`: icon + two-line lockup) becomes one button when the switcher is available.
12
- 2. **Affordance:** a small down-chevron renders after the business name to signal the dropdown.
13
- 3. **Close:** re-clicking the head toggles it shut, and a transparent full-screen scrim behind the panel closes it on any outside click. Mirrors the footer-menu scrim it replaces.
14
-
15
- ## Availability gate (unchanged from today)
16
-
17
- Switcher is available only when `subAccounts.length > 1 && Boolean(onSwitchAccount)`. Unavailable → the head stays a plain non-interactive `<div>`, no chevron, no picker. This is the current `switcherAvailable` gate, moved from `AccountMenu` to `Sidebar`.
18
-
19
- ## Components and flow
20
-
21
- - **`Sidebar.tsx`** owns `switcherOpen` state. The brand head renders as `<button aria-haspopup="menu" aria-expanded={switcherOpen}>` when available, else the current `<div>`. Click toggles `switcherOpen`; the closed→open transition fires `refreshAccounts?.()` and emits `[admin-ui] op=subaccount-switcher-toggle anchor=brand-head open=<bool> available=<bool> accounts=<N>`. `SubAccountPicker` renders inside `.side-brand`, anchored beneath it, with a sibling scrim that closes on click.
22
- - **`AccountMenu.tsx`** loses the `switcherAvailable`/`switcherOpen` block, the `SubAccountPicker` import, and the five switcher props. `UserRound` import dropped if otherwise unused.
23
- - **`globals.css`** gives `.side-brand` `position: relative` and re-anchors `.sub-account-picker-panel` left-aligned under the head (drop the retired header-title `translateX(-50%)` centering). Adds `.side-brand` button-reset styling (transparent, inherit, pointer) and the chevron. Deletes the dead `.side-foot-menu .sub-account-picker-panel` / `-list` overrides.
24
-
25
- ## Collapsed rail (edge, in scope to not break)
26
-
27
- When `sidebar-collapsed`, existing CSS hides `.side-brand-lockup` (name + chevron), leaving only the icon. The head-button still functions on the icon, but no new collapsed affordance is added (per task out-of-scope). The picker anchors under `.side-brand` in both states.
28
-
29
- ## Testing
30
-
31
- vitest + jsdom (Node 22). New `Sidebar` test: available → head is a button, click mounts the picker and fires `refreshAccounts`, re-click closes; unavailable → not a button, no picker. `AccountMenu.test.tsx`: no "Switch account", switcher props dropped. `SubAccountPicker.test.tsx` unchanged.
32
-
33
- ## Out of scope
34
-
35
- `SubAccountPicker` internals, the switch mechanism, other `AccountMenu` items, a dedicated collapsed-rail affordance, maxy-lite, Task 1940's mobile lite tab bar.
@@ -1,232 +0,0 @@
1
- # Task 1902 — every account-root entry is declared by the component that owns it
2
-
3
- Design agreed 2026-07-26. Source task: `.tasks/pending/1902-account-owned-entry-declarations-are-incomplete.md`.
4
-
5
- ## The problem this removes
6
-
7
- `account-schema-owned-dirs.py` unions each plugin's `account-owned-dirs`
8
- declaration into the account's `SCHEMA.md` `allowed-top-level` block on provision,
9
- and the standing reconcile
10
- (`platform/services/claude-session-manager/src/account-dir-schema-reconcile.ts`)
11
- names every root entry outside that block on the `op=strays` line. Three plugins
12
- declare anything today: `storage-broker/PLUGIN.md:4` (`pages`),
13
- `memory/PLUGIN.md:4` (`archive`), and
14
- `premium-plugins/sitedesk/plugins/sitedesk-job/PLUGIN.md:34` (`quoting`).
15
-
16
- Two classes of entry are outside that mechanism.
17
-
18
- Platform-written control-plane files at the account root are kept out of the stray
19
- count by `PROTECTED_TOP_LEVEL`, a hand-maintained constant in the reconcile that
20
- has been extended once per casualty (`calendar-availability.json` was Task 1877,
21
- `agents-disabled.json` was Task 1996). It holds seven such files. The mechanism
22
- that should own them covers directories only, so no plugin can declare a file, and
23
- `data-portal.json` was never added to either surface and reports as a stray on
24
- every run.
25
-
26
- Directories an agent created at an account root are in no declaration and have no
27
- writer in the shipped tree. Since Task 1899 removed the move, each costs one
28
- `op=strays` name per five-minute cycle rather than a broken feature. The cost is a
29
- permanently noisy signal an operator learns to ignore, which is how the next real
30
- stray gets missed.
31
-
32
- ## Evidence
33
-
34
- Grep over `platform/plugins`, `platform/scripts`, `platform/services`,
35
- `platform/templates` and `premium-plugins` for an account-root write of each
36
- undeclared name:
37
-
38
- - `e-sign/` has one writer: `platform/plugins/business-assistant/skills/e-sign/SKILL.md:452`
39
- (`mkdir -p "${ACCOUNT_DIR}/e-sign/<DOC_REF>"`), plus the persisted base and the
40
- per-signer stamped copies alongside it.
41
- - `clients`, `workers`, `specs`, `templates`, `sites-staging` and `repo` have no
42
- writer. By the source task's own rule an entry with no creator in the tree is
43
- agent work product, recorded as such rather than declared.
44
-
45
- Writers for the seven protected files and for `data-portal.json`:
46
-
47
- | Root file | Writer | Declared on |
48
- |---|---|---|
49
- | `calendar-availability.json` | `plugins/scheduling/mcp/src/scripts/publish-availability.ts` | scheduling |
50
- | `data-portal.json` | `plugins/cloudflare/bin/portal-index-push.mjs` | cloudflare |
51
- | `wa-channel-bindings.json` | `services/claude-session-manager/src/wa-channel-store.ts` | admin |
52
- | `telegram-channel-bindings.json` | `services/claude-session-manager/src/telegram-channel-store.ts` | admin |
53
- | `webchat-channel-bindings.json` | `services/claude-session-manager/src/webchat-channel-store.ts` | admin |
54
- | `canonical-webchat-session.json` | `ui/server/canonical-webchat-override.ts` | admin |
55
- | `session-titles.json` | `services/claude-session-manager/src/account-title-stores.ts` | admin |
56
- | `agents-disabled.json` | `ui/server/routes/admin/agents.ts` | admin |
57
-
58
- Six of the eight are written by a shared platform service or the UI server, not by
59
- the plugin they are declared on. The session manager and the UI server carry no
60
- manifest the resolver reads, so their files are declared on `admin`, and the rule
61
- is reach rather than naming: **a declaration is only present on a brand whose
62
- payload carries the declaring plugin.** `brand.json` `plugins.excluded` drops the
63
- directory outright (`packages/create-maxy-code/scripts/bundle.js`), and
64
- `realagent-code` and `property-administrators` both exclude `telegram` while the
65
- session manager still constructs `TelegramChannelStore`
66
- (`services/claude-session-manager/src/index.ts`). Declaring
67
- `telegram-channel-bindings.json` on the telegram plugin would therefore leave
68
- those two brands reporting it as a stray forever, which is the failure this task
69
- exists to remove. `admin` is excluded by no brand.
70
-
71
- The three remaining declarations sit on the plugin whose own code writes them
72
- (`scheduling`, `cloudflare`, `business-assistant`). That is self-consistent: when
73
- such a plugin is excluded, its writer is excluded with it and the file is never
74
- created, so the declaration is present exactly when the file can be.
75
-
76
- `setup-account.sh:69` runs `reconcile_all_accounts_owned_dirs "$ACCOUNTS_DIR"` on
77
- every install, and that sweep calls `merge_owned_dirs_into_schema` for every
78
- account carrying an `account.json`. A declaration therefore reaches every existing
79
- account on the next install, not only accounts provisioned after it lands.
80
-
81
- ## Units
82
-
83
- ### 1. `platform/scripts/lib/account-schema-owned-dirs.py`
84
-
85
- `_parse_plugin_md` gains a second frontmatter key, `account-owned-files`, parsed
86
- with the same regex-then-`json.loads` shape as `account-owned-dirs` and keyed
87
- `file` instead of `dir`:
88
-
89
- ```
90
- account-owned-files: [{"file": "data-portal.json", "description": "..."}]
91
- ```
92
-
93
- A declaration that fails to parse is skipped and the remaining declarations still
94
- merge, matching how the dirs key already behaves (`except Exception: pass`).
95
-
96
- `resolve` returns one list carrying both kinds, each entry gaining a `kind` field
97
- of `dir` or `file`, so a single ordered union feeds the merge. Dirs keep their
98
- existing position ahead of files from the same manifest.
99
-
100
- `merge` unions declared file names into the same `allowed-top-level` block as
101
- dirs. The block is already a flat list of names that carries `SCHEMA.md`,
102
- `account.json` and `AGENTS.md`, and `parseAllowedTopLevel` in the reconcile
103
- compares it against `readdirSync` entries without distinguishing files from
104
- directories, so no consumer changes. The prune rule is unchanged: an entry the
105
- resolver no longer targets is dropped when empty or absent and kept-and-logged
106
- when it holds content, and `_dir_holds_files` already treats a non-empty regular
107
- file as content.
108
-
109
- The plugin-owned descriptive region renders a file without the trailing slash a
110
- directory gets:
111
-
112
- ```
113
- - `pages/` — Cloudflare Pages deploy staging for the account's public sites.
114
- - `data-portal.json` — the account's data-portal index, written by portal-index-push.
115
- ```
116
-
117
- `reconcile` prints `ownedFile=<name> present=<true|false>`, the file twin of the
118
- existing `ownedDir=` line, so a declaration that has not reached an account is
119
- visible per name.
120
-
121
- ### 2. Declarations
122
-
123
- One frontmatter line added per plugin, in the shape the resolver parses:
124
-
125
- - `plugins/business-assistant/PLUGIN.md` — `account-owned-dirs` gains `e-sign`.
126
- - `plugins/cloudflare/PLUGIN.md` — `account-owned-files` gains `data-portal.json`.
127
- - `plugins/scheduling/PLUGIN.md` — `calendar-availability.json`.
128
- - `plugins/admin/PLUGIN.md` — `wa-channel-bindings.json`,
129
- `telegram-channel-bindings.json`, `webchat-channel-bindings.json`,
130
- `canonical-webchat-session.json`, `session-titles.json`, `agents-disabled.json`.
131
-
132
- The resolver reads every `$PROJECT_DIR/plugins/*/PLUGIN.md` present in the
133
- installed tree, with only premium bundles gated by `shipsPremiumBundles`. A brand
134
- that excludes a plugin has no manifest to read, which is why the six
135
- service-written files sit on `admin`: every brand then allows all nine names,
136
- which is what `PROTECTED_TOP_LEVEL` did for seven of them.
137
-
138
- ### 3. `platform/services/claude-session-manager/src/account-dir-schema-reconcile.ts`
139
-
140
- `PROTECTED_TOP_LEVEL` drops the seven file entries and keeps `.git`, `.claude`,
141
- `.quarantine`, `SCHEMA.md`, `account.json` and `AGENTS.md`. Its comment is
142
- rewritten to state the new division: the constant holds the account's own
143
- machinery, which exists whatever the schema says, and every feature file is held
144
- by the declaration that owns it.
145
-
146
- The behavioural consequence, stated rather than hidden: those seven files are now
147
- kept out of the stray count by the account's `SCHEMA.md`, not by a constant. An
148
- account whose schema was never re-merged reports them. The sweep in
149
- `setup-account.sh` closes that window on the install that ships this change, and
150
- the `ownedFile=` reconcile line names any account it has not reached.
151
-
152
- ### 4. Tests
153
-
154
- `platform/scripts/__tests__/account-schema-owned-dirs.test.sh` gains, on a
155
- synthetic tree in the style of the existing shape-b case:
156
-
157
- - a plugin declaring `account-owned-files: [{"file": "x.json", ...}]` puts `x.json`
158
- in the merged `allowed-top-level` block, describes it in the plugin-owned region
159
- without a trailing slash, and reports `ownedFile=x.json present=true` from
160
- `reconcile`;
161
- - a plugin whose `account-owned-files` value is malformed JSON is skipped while a
162
- sibling plugin's valid declaration still merges;
163
- - the merge stays byte-identical on a second run with a file declaration present.
164
-
165
- **Mutation check.** Drop the files branch from the resolver and the new cases must
166
- fail. Revert.
167
-
168
- `platform/services/claude-session-manager/src/__tests__/account-dir-schema-reconcile.test.ts`
169
- carries two tests that prove the code belt: `never counts platform-written
170
- control-plane root files absent from the allowed block` and `does not count the
171
- disabled store itself as a stray`. Both seed an account whose `SCHEMA.md` lists no
172
- such file, which after this change is a stale account rather than a current one.
173
- They are rewritten to seed the schema a reprovisioned account carries, listing the
174
- declared file names, and then assert the same outcome: the control-plane files are
175
- absent from `strayNames` and a genuine operator stray alongside them is still
176
- counted and named. That is the surface the reconcile reads, so the rewritten tests
177
- prove the delivered behaviour rather than the retired constant.
178
-
179
- ### 5. The region parsers and the doc
180
-
181
- Three readers parse the plugin-owned region and each anchors on the trailing
182
- slash inside the backticks: `platform/lib/account-schema-regions/src/index.ts:65`
183
- (shared by the `/data` page grouping and the reconcile's parity audit) and
184
- `platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs:42-43` (the public
185
- data-portal exposure set, which keeps its own copy under a parity test). A file
186
- line therefore yields no bucket in any of them, which is the required behaviour
187
- and needs no code change. It is pinned by a case in
188
- `platform/ui/server/lib/__tests__/account-schema-regions.test.ts`, because a
189
- future edit relaxing that regex would put a control-plane file in front of a
190
- client.
191
-
192
- `.docs/data-portal-folder-index.md` is the authoritative description of how the
193
- allowed set is built and currently describes plugin-owned as directories. It
194
- gains the second kind and the trailing-slash rule.
195
-
196
- ## Scope boundaries
197
-
198
- ### In scope
199
-
200
- Units 1 to 5.
201
-
202
- ### Out of scope
203
-
204
- - Reprovisioning the laptop's accounts, and the two device checks that depend on
205
- it (`op=strays` naming only recorded work product, and the negative check that
206
- removing one declaration makes its entry reappear). Both need this change
207
- published and installed, and publish awaits an explicit operator command. Task
208
- `2027-reprovision-laptop-accounts-after-1902-declarations.md`.
209
- - Declaring `clients`, `workers`, `specs`, `templates`, `sites-staging` or `repo`.
210
- No shipped component writes them, so declaring them would make the allowed list
211
- a record of what happens to exist rather than of what is owned. They continue to
212
- appear in `op=strays`, which is the correct report for debris.
213
- - Relocating agent work product into schema buckets. Task 1894 covers the glsmith
214
- case.
215
- - Teaching `fs-schema-guard.sh` to tell a declared file from an operator bucket.
216
- The guard reads the same `allowed-top-level` fence as an allow-list for
217
- `Write`/`Edit`/`NotebookEdit`, so declaring these eight files makes them
218
- agent-writable where they were blocked before. Measured: the same envelope
219
- returns exit 2 against the pre-merge schema and exit 0 against the merged one.
220
- The fence already permits `account.json`, `SCHEMA.md`, `secrets/` and
221
- `.claude/`, so it is a layout guard rather than a trust boundary, and this
222
- widens it rather than breaches it. Task
223
- `2028-fs-schema-guard-cannot-tell-a-declared-file-from-an-operator-bucket.md`.
224
- - The reconcile's report-only posture. Task 1899 settled it.
225
-
226
- ## Observability
227
-
228
- The `op=strays` line from Task 1899 is the instrument. A declared entry never
229
- appears in it, so anything that does is either debris or an undeclared owner, and
230
- the two are separated by asking what created it. The `ownedDir=` and `ownedFile=`
231
- lines from `reconcile` are the second instrument: they name, per account, a
232
- declaration that has not yet reached that account's `SCHEMA.md`.
@@ -1,200 +0,0 @@
1
- # Task 1926 — same-account portal push concurrency guard (design)
2
-
3
- **Date:** 2026-07-26
4
- **Task:** [`.tasks/1926-concurrent-same-account-portal-push-lets-the-pre-stage-clear-delete-the-live-tree.md`](../../../../.tasks/1926-concurrent-same-account-portal-push-lets-the-pre-stage-clear-delete-the-live-tree.md)
5
- **Touches:** `platform/plugins/cloudflare/bin/portal-index-push.mjs`, `platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts`
6
-
7
- ## Problem
8
-
9
- `pushAccount` stages an account's file index at `next = currentGeneration + 1`, flips
10
- the `directory_state` pointer to `next`, then sweeps every other generation. Task 1923
11
- added a pre-stage `DELETE FROM directory WHERE accountId = ? AND generation = ?` bound
12
- to `next`, so a stage that died mid-batch could self-heal instead of colliding forever
13
- on `UNIQUE (accountId, relPath, generation)`.
14
-
15
- Two pushes of one account break that. The second writer reads `current = 5` before the
16
- first writer's flip, computes `next = 6`, and by the time its pre-stage DELETE runs the
17
- pointer already names generation 6. The DELETE removes the live tree. A client listing
18
- in the gap between that DELETE and the re-insert sees an empty portal.
19
-
20
- The overlap is reachable: the admin server's 60s loop calls `pushAllAccounts`, and a
21
- hand-run `node portal-index-push.mjs --account <id>` calls the same `pushAccount` from a
22
- separate OS process.
23
-
24
- ## What success looks like
25
-
26
- Two concurrent pushes of one account never leave a reader seeing an empty or partial
27
- tree, and never delete rows at the pointer's generation.
28
-
29
- ## Why the three candidate approaches in the task file were rejected
30
-
31
- **A per-account advisory lock.** The two racers are separate OS processes. A JavaScript
32
- mutex inside the admin server does not exist in the CLI process, so it guards nothing.
33
- A lock that does work across processes has to live in D1, which means new columns, a
34
- lease, and a lease expiry. A lease expiry reintroduces the wedge class Task 1923 just
35
- removed: a holder that dies wedges the account until the lease runs out, and a push
36
- slower than its lease keeps writing after another writer has taken over.
37
-
38
- **`next` from `MAX(generation)` alone.** This stops two writers sharing a staging
39
- generation, but the slower writer still flips the pointer down onto its own generation
40
- after the faster writer has already swept it. The reader still sees an empty tree, so
41
- the success criterion is not met.
42
-
43
- **Scoping the clear to `generation = next AND generation != current`.** `current` is a
44
- value read at the top of the push. By the time the DELETE runs, the pointer has moved.
45
- Re-reading the pointer immediately before the DELETE narrows the window without closing
46
- it, because the peer can flip between the re-read and the DELETE.
47
-
48
- ## Design
49
-
50
- Three changes to `pushAccount`. No lock, no lease, no schema change, no new table.
51
-
52
- ### 1. Claim a generation above everything that exists
53
-
54
- Keep the existing pointer read, and add one statement beside it:
55
-
56
- ```sql
57
- SELECT MAX(generation) AS g FROM directory WHERE accountId = ?
58
- ```
59
-
60
- `next = Math.max(current, maxStagedGeneration) + 1`.
61
-
62
- A peer that has already inserted at least one row is visible in that MAX, so the two
63
- writers claim different generations and neither stages into the other's rows.
64
-
65
- Two statements rather than one nested `UNION ALL`: each is trivially readable, each is
66
- trivially fakeable in a test, and the pointer read stays the push's first statement,
67
- which an existing test asserts.
68
-
69
- ### 2. The flip only ever moves the pointer forward
70
-
71
- ```sql
72
- INSERT INTO directory_state (accountId, currentGeneration) VALUES (?, ?)
73
- ON CONFLICT (accountId) DO UPDATE SET currentGeneration = excluded.currentGeneration
74
- WHERE excluded.currentGeneration > directory_state.currentGeneration
75
- ```
76
-
77
- SQLite has supported a `WHERE` clause on upsert's `DO UPDATE` since 3.24, and D1 is
78
- SQLite. This is what makes a losing writer harmless: it cannot drag readers back onto a
79
- generation the winner has already swept.
80
-
81
- The D1 client returns rows, not affected-row meta, so the flip is followed by a pointer
82
- readback. That readback is the branch point:
83
-
84
- - **Pointer equals `next`.** We won. Log `op=flip`, sweep, verify, return. Unchanged
85
- behaviour.
86
- - **Pointer is above `next`.** A peer won. Log
87
- `op=stage-contended account=<id> generation=<next> pointer=<actual> action=superseded`
88
- and return without sweeping and without verifying. Both of those would measure the
89
- peer's tree against our expected row count, and `op=verify` would read as a failure
90
- when nothing failed.
91
-
92
- Our staged rows at the losing generation are left in place. They are invisible (no
93
- reader sees a generation the pointer does not name) and the winner's next sweep collects
94
- them.
95
-
96
- ### 3. The sweep only ever removes older generations
97
-
98
- ```sql
99
- DELETE FROM directory WHERE accountId = ? AND generation < ?
100
- ```
101
-
102
- `< next` rather than `!= next`, so a writer never deletes a peer's higher in-flight
103
- stage. Rows above `next` from an abandoned stage still converge: the following cycle
104
- claims a generation above them and sweeps them once it flips.
105
-
106
- ### 4. The Task 1923 pre-stage clear is removed
107
-
108
- Because `next` is now always above every existing generation for the account, the orphan
109
- `SELECT COUNT(*) ... generation = next` can only ever return 0 and the pre-stage DELETE
110
- can never match a row. Leaving unreachable code whose comment claims it is the wedge
111
- heal would be false narration, so the orphan count, the DELETE, the `op=stage-clear` log
112
- line and its two tests go.
113
-
114
- Task 1923's outcome is preserved by construction and is stronger than before. A stage
115
- that died mid-batch leaves rows at generation 6; the next run claims 7, so there is no
116
- UNIQUE collision to freeze on, and the gen-6 orphans are swept after the flip. The heal
117
- now happens with no DELETE before the flip at all, which is exactly what removes this
118
- task's hazard rather than narrowing it.
119
-
120
- This was confirmed with the operator before the spec was written, because Task 1926
121
- lists Task 1923's heal as out of scope.
122
-
123
- ## Cases covered, and the one left fail-closed
124
-
125
- **Peer reads the generation after we have staged a row.** It claims a higher generation.
126
- Both pushes complete. The forward-only flip picks the winner. The reader sees the old
127
- complete tree, then one complete new tree. Neither push fails.
128
-
129
- **Both read the generation before either inserts.** Both claim the same generation. The
130
- second writer's first INSERT hits `UNIQUE (accountId, relPath, generation)` and that push
131
- throws. Both walks resolve the same exposed set and `walkExposed` emits the exposed dirs
132
- in the same order, so the second writer collides on its first row and contributes
133
- nothing. The reader is safe: the winner's tree is complete and the pointer never moved
134
- backward. The loser logs `op=failed` and the next 60s cycle succeeds.
135
-
136
- This second case is left fail-closed rather than retried. The task's success criterion is
137
- about what a reader can see, and a loud failure that self-corrects one cycle later meets
138
- it. A retry-on-collision path would be an invented requirement.
139
-
140
- **The one residual, stated rather than fixed.** The claim above relies on both walks
141
- starting with the same row. If the account's `SCHEMA.md` or `data-portal.json` changes in
142
- the window between the two generation reads, the two walks can begin with different rows,
143
- so the second writer contributes some non-overlapping rows at the shared generation before
144
- colliding on a shared one. The winner then flips onto its own complete tree plus those few
145
- extra rows. That is not an empty tree and not a partial one, so it does not breach the
146
- success criterion, and the next cycle's sweep removes the whole generation. No task file is
147
- filed for it because nothing is deferred: the case is inside what this design covers, and
148
- its outcome is a stale row for one cycle rather than a reader-visible break.
149
-
150
- ## Observability
151
-
152
- `op=stage-contended account=<id> generation=<next> pointer=<actual> action=superseded`
153
- is emitted when a push's flip was superseded by a peer. This deviates from the task
154
- file's `action=<waited|skipped>` wording: the design never waits and never skips work, so
155
- neither value describes what happened. `superseded` names the actual outcome, and
156
- `pointer=` names the generation that won, which is what an operator needs to correlate
157
- the two runs.
158
-
159
- `op=stage-clear` is retired with the code that emitted it. The task file's line that a
160
- `stage-clear` with `cleared=` equal to the live tree size must never appear is satisfied
161
- absolutely: no `stage-clear` line can appear at all.
162
-
163
- ## Testing
164
-
165
- Both new tests drive two interleaved `pushAccount` calls against one shared fake store
166
- and snapshot the reader's view after every statement. The assertion is the success
167
- criterion stated directly: the visible tree is never empty and never a strict subset of
168
- a complete tree.
169
-
170
- 1. **The task file's interleave.** Writer B reads the generation, then pauses. Writer A
171
- runs to completion. B resumes. Red on current code, because B's pre-stage DELETE binds
172
- the generation the pointer now names and the snapshot goes empty. Green after, because
173
- B collides on UNIQUE and A's tree is untouched. This test also asserts that no DELETE
174
- is ever issued naming the pointer's current generation, which is the other half of the
175
- success criterion.
176
-
177
- 2. **The higher-claim interleave.** Writer A stages fully but pauses before its flip.
178
- Writer B reads the generation, sees A's staged rows in the MAX, claims the next one up,
179
- and completes. A resumes and its flip is a no-op. Asserts the pointer never moves
180
- backward, that A emits `op=stage-contended` with `action=superseded`, and that A
181
- issues no sweep.
182
-
183
- The existing 27 tests stay green apart from the two Task 1923 stage-clear tests, which
184
- are removed with the code they cover.
185
-
186
- ## Scope boundaries
187
-
188
- **In scope:** the generation claim, the forward-only flip, the backward-only sweep, the
189
- removal of the pre-stage clear, and the two-writer regression tests.
190
-
191
- **Out of scope:** INSERT batching (Task 1842). Cross-account contention (separate
192
- generation counters, no contention). Any change to `runTargets`, `recordSuccess`, or the
193
- portal reader in `files.ts`.
194
-
195
- ## Deployment
196
-
197
- `portal-index-push.mjs` ships inside the installer payload and is inlined into the admin
198
- server bundle. No D1 migration is required: every statement runs against the existing
199
- `directory` and `directory_state` tables as defined in
200
- `platform/plugins/cloudflare/skills/data-portal/schema.sql`.
@@ -1,169 +0,0 @@
1
- # Task 2014 design — cut the jsdom the `platform/ui` suite does not use
2
-
3
- Date: 2026-07-26
4
- Task: `.tasks/pending/2014-platform-ui-suite-is-nondeterministic-under-parallel-load.md`
5
- Supersedes brief: 1765, folded into 2014 on 2026-07-26
6
-
7
- ## The measurement this design rests on
8
-
9
- All figures from the dev Mac, Node 22.22.0, vitest 4.1.2, worktree
10
- `task-2014-ui-suite-nondeterminism`, 2026-07-26.
11
-
12
- ### Most apparent worktree flakiness is missing build output, not flakiness
13
-
14
- `platform/node_modules` and `platform/lib/*/dist` are gitignored build output. A
15
- fresh worktree has neither. Four full-suite runs, each after adding one more
16
- piece of provisioning:
17
-
18
- | Run | Worktree state | Failed | Duration |
19
- |---|---|---|---|
20
- | 1 | `platform/ui` deps only | 63 files / 32 tests | 435s |
21
- | 2 | + 3 libs built | 5 files / 8 tests | 180s |
22
- | 3 | + `platform` deps | 5 files / 3 tests | 111s |
23
- | 4 | + all 24 libs built | 0 | 143s |
24
-
25
- Runs 1 and 2 failed on `Cannot find module .../dist/index.js` and
26
- `Failed to resolve import "neo4j-driver"`. Those are absent artifacts, not
27
- timing. Only run 3's three failures are the defect this task describes, and all
28
- three are in the brief's population: both `/login/start` sub-account cases in
29
- `whatsapp-self-or-house-scope.test.ts` at 5013ms and 515ms, and
30
- `whatsapp-conversation-graph-state-projection.test.ts` "sub-account all-senders
31
- mode returns only its bound thread" at 5017ms. 5000ms is the vitest default
32
- `testTimeout`.
33
-
34
- ### Environment setup costs six times what the tests cost
35
-
36
- Run 4, fully green:
37
-
38
- ```
39
- tests 267s
40
- environment 1571s
41
- import 257s
42
- setup 196s
43
- transform 42s
44
- ```
45
-
46
- `vitest.config.ts` sets `environment: 'jsdom'` globally. 530 test files pay it.
47
-
48
- ### Roughly half the suite boots a DOM it never touches
49
-
50
- 266 `.test.ts` files do not declare `@vitest-environment node`. Both runs
51
- back to back, same machine load:
52
-
53
- | | Duration | environment CPU |
54
- |---|---|---|
55
- | as-is (jsdom) | 103.0s | 1317.6s |
56
- | `--environment node` | 30.9s | 0.074s |
57
-
58
- Three consecutive node-environment passes over those 266 files agreed on 17
59
- `.ts` files that genuinely need a DOM. A file that appeared in only one pass
60
- (`route-cache-watchers.test.ts`) is the known flake, not a DOM dependency. A
61
- `.tsx` file in the failure list arrived because vitest CLI arguments are
62
- substring filters rather than exact paths.
63
-
64
- So the suite's problem is not 4757 tests. The tests are 267s of CPU. It is that
65
- about half the files run a browser DOM environment for nothing, and that is the
66
- CPU starving the workers the named tests race against.
67
-
68
- ### Machine context
69
-
70
- The dev Mac ran these measurements at load average 100 to 350 with 60 `claude`
71
- processes across six peer worktree sessions. Self-contention from 20 vitest
72
- forks is not the only contender for CPU, and any verification has to record the
73
- load it ran under or a red run cannot be attributed.
74
-
75
- ## Design
76
-
77
- ### 1. Route the test environment by file extension
78
-
79
- `platform/ui/vitest.config.ts` replaces its single global `environment: 'jsdom'`
80
- with two projects:
81
-
82
- - `dom`: `**/*.{test,spec}.?(c|m)[jt]sx`, environment `jsdom`.
83
- - `node`: `**/*.{test,spec}.?(c|m)[jt]s` plus `../scripts/__tests__/**`,
84
- environment `node`.
85
-
86
- The existing `resolve.dedupe`, `setupFiles`, `globals`, and `exclude` settings
87
- carry into both projects unchanged.
88
-
89
- The rule is structural, not a maintained list: a test that renders JSX gets a
90
- DOM, and anything else opts in explicitly. No `.tsx` file is edited.
91
-
92
- ### 2. Seventeen `.ts` files opt back into jsdom
93
-
94
- Each gets `// @vitest-environment jsdom` with a short reason. Verified on vitest
95
- 4.1.2 that a per-file docblock overrides a project's `environment` setting.
96
-
97
- ```
98
- app/__tests__/spinning-class-global.test.ts
99
- app/__tests__/wa-viewer-width.test.ts
100
- app/graph/__tests__/side-panel-helpers.test.ts
101
- app/hooks/__tests__/useCopyFeedback.test.ts
102
- app/hooks/__tests__/useMediaQuery.test.ts
103
- app/hooks/__tests__/useVoiceRecorder.deviceId.test.ts
104
- app/hooks/__tests__/useVoiceRecorder.mic-result.test.ts
105
- app/lib/__tests__/admin-auth-login-remote-auth.test.ts
106
- app/lib/__tests__/switch-account-scope.test.ts
107
- app/lib/__tests__/use-admin-auth-claude-connected.test.ts
108
- app/lib/__tests__/use-admin-auth-heartbeat-remote-auth.test.ts
109
- app/lib/__tests__/useAdminFetch.test.ts
110
- app/lib/__tests__/useSubAccountSwitcher.test.ts
111
- app/public/__tests__/extract-seed-question.test.ts
112
- app/whatsapp/__tests__/useTranscriptStream-activity.test.ts
113
- app/whatsapp/__tests__/useTranscriptStream-endpoint.test.ts
114
- app/whatsapp/__tests__/useTranscriptStream.test.ts
115
- ```
116
-
117
- A `.ts` test that needs a DOM and lacks the docblock fails loudly on
118
- `document is not defined`. It does not pass silently.
119
-
120
- ### 3. A prebuild gate so the waste cannot creep back
121
-
122
- New `platform/ui/scripts/check-jsdom-budget.mjs`, joining the 24 checks already
123
- in the `prebuild` script. It counts the files that will boot jsdom, which is
124
- every `.test.tsx`/`.spec.tsx` plus every `.ts` test declaring the jsdom
125
- docblock, and fails the build when that count exceeds a ceiling written in the
126
- script. Adding a jsdom file becomes a deliberate one-line bump with a reason,
127
- not an accident. The failure message names the files above the ceiling and the
128
- CPU cost per jsdom file measured here, so a reader knows what the number buys.
129
-
130
- ### 4. The 141 existing `@vitest-environment node` docblocks stay
131
-
132
- They remain accurate and are now redundant. Removing them is 141 files of churn
133
- for no behaviour change.
134
-
135
- ## What this does not claim
136
-
137
- Removing the environment waste reduces the contention the named tests race
138
- against. It is not proven, before measurement, that the ten named tests stop
139
- failing. If group A's long-press tests still race React Testing Library's
140
- 1000ms `findBy` default once contention drops, that is a second and separate
141
- lever, and it gets measured and brought back rather than stacked on top as a
142
- speculative second fix.
143
-
144
- ## Verification
145
-
146
- **Group A and the suite.** Ten consecutive `npm test` runs in `platform/ui` on
147
- the merged branch, `uptime` captured before and after each run, exit code and
148
- any `×` lines collected to one file. Success is ten `exit=0` and zero `×`
149
- matches across the collection, reported alongside the load each ran under. A red
150
- run with its load recorded is attributable; a red run without is not.
151
-
152
- **Group B.** Ten runs do not test a one-in-twenty failure rate. Instead measure
153
- the wall-clock cost of the four group B seed calls under full-suite parallel
154
- load, before and after the change, and state the headroom inside the 5000ms
155
- default. The measurement is the evidence, not the run count.
156
-
157
- **The jsdom cut itself.** The suite must still report 530 files and 4757 tests.
158
- A file silently dropping out of the run because a glob missed it would look like
159
- a pass.
160
-
161
- ## Out of scope
162
-
163
- - The `platform/lib` build artifacts that made runs 1 to 3 look flaky. That is
164
- worktree provisioning friction and gets its own task file.
165
- - A blanket `testTimeout` bump, excluded by the brief.
166
- - Deleting, skipping, or loosening the four group B tests, excluded by the
167
- brief: they are the only place the cross-account 403 ordering is asserted.
168
- - Removing the 141 redundant node docblocks.
169
- - Any product change to the whatsapp routes, the data browser, or graph-write.