@rubytech/create-maxy-code 0.1.486 → 0.1.488

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 (69) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -3
  3. package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +30 -8
  4. package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +22 -2
  5. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +23 -0
  6. package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +43 -0
  7. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +12 -2
  8. package/payload/platform/plugins/memory/PLUGIN.md +1 -0
  9. package/payload/platform/plugins/memory/references/schema-construction.md +13 -0
  10. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts +2 -0
  11. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.d.ts.map +1 -0
  12. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.js +12 -0
  13. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/free-busy-url.test.js.map +1 -0
  14. package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.d.ts +8 -0
  15. package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.d.ts.map +1 -0
  16. package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.js +12 -0
  17. package/payload/platform/plugins/scheduling/mcp/dist/lib/free-busy-url.js.map +1 -0
  18. package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +12 -2
  19. package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -1
  20. package/payload/platform/plugins/storage-broker/PLUGIN.md +1 -0
  21. package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +22 -2
  22. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
  23. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +23 -2
  24. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
  25. package/payload/server/public/activity.html +6 -6
  26. package/payload/server/public/assets/{AdminLoginScreens-DF9S5Rca.js → AdminLoginScreens-Dic_fPt6.js} +1 -1
  27. package/payload/server/public/assets/{AdminShell-B0PNDCAb.js → AdminShell-JN7rZ8aG.js} +1 -1
  28. package/payload/server/public/assets/{Checkbox-s5X6dso4.js → Checkbox-CB74kINA.js} +1 -1
  29. package/payload/server/public/assets/{activity-BRBsaalV.js → activity-DbfpHg3i.js} +1 -1
  30. package/payload/server/public/assets/{admin-BoVCP4Mw.js → admin-CThbXaYH.js} +1 -1
  31. package/payload/server/public/assets/{bot-BVO9Uqr_.js → bot-DnuyRsLS.js} +1 -1
  32. package/payload/server/public/assets/{browser-BVW4FFii.js → browser-q4EFqKvR.js} +1 -1
  33. package/payload/server/public/assets/{calendar-CWsD3uLk.js → calendar-C-q9kWmA.js} +1 -1
  34. package/payload/server/public/assets/chat-DcefIV9q.js +1 -0
  35. package/payload/server/public/assets/chevron-down-Blma_CKl.js +1 -0
  36. package/payload/server/public/assets/chevron-left-xeYDxFiC.js +1 -0
  37. package/payload/server/public/assets/chevron-right-BonbtjqJ.js +1 -0
  38. package/payload/server/public/assets/data-D5dp6nj6.js +1 -0
  39. package/payload/server/public/assets/{graph-mJ9ky5dc.js → graph-CNgt7ygi.js} +1 -1
  40. package/payload/server/public/assets/{graph-labels-C8-sIKuv.js → graph-labels-DA48HrRR.js} +1 -1
  41. package/payload/server/public/assets/{maximize-2-yucCepu_.js → maximize-2-k3Fu5hdP.js} +1 -1
  42. package/payload/server/public/assets/{operator-911z_Fy_.js → operator-8NVpNLeD.js} +1 -1
  43. package/payload/server/public/assets/{page-Cv0qUwMG.js → page-Bvx0A2jB.js} +1 -1
  44. package/payload/server/public/assets/{page-CgSgj8Cy.js → page-DhwPG5Zf.js} +1 -1
  45. package/payload/server/public/assets/{public-BzwveFwz.js → public-BJ5Oo2Fw.js} +1 -1
  46. package/payload/server/public/assets/{rotate-ccw-BH51cf2Q.js → rotate-ccw-D-ov5jM4.js} +1 -1
  47. package/payload/server/public/assets/tasks-DHMxTZVD.js +1 -0
  48. package/payload/server/public/assets/{time-entry-format-DPdGkp1C.js → time-entry-format-EmV5lz6o.js} +1 -1
  49. package/payload/server/public/assets/{triangle-alert-DO2rjjfV.js → triangle-alert-CivyPfAL.js} +1 -1
  50. package/payload/server/public/assets/{useCopyFeedback-HMSM7-Ir.js → useCopyFeedback-hbnPS-r9.js} +1 -1
  51. package/payload/server/public/assets/{useSubAccountSwitcher-2ojOOYNo.css → useSubAccountSwitcher-kwhG-aJj.css} +1 -1
  52. package/payload/server/public/assets/{useVoiceRecorder-BcukQIza.js → useVoiceRecorder-QN9_GqB_.js} +1 -1
  53. package/payload/server/public/browser.html +4 -4
  54. package/payload/server/public/calendar.html +6 -6
  55. package/payload/server/public/chat.html +13 -13
  56. package/payload/server/public/data.html +10 -10
  57. package/payload/server/public/graph.html +11 -11
  58. package/payload/server/public/index.html +16 -16
  59. package/payload/server/public/operator.html +15 -15
  60. package/payload/server/public/public.html +13 -13
  61. package/payload/server/public/tasks.html +6 -6
  62. package/payload/server/server.js +155 -103
  63. package/payload/server/public/assets/chat-DKGREuIO.js +0 -1
  64. package/payload/server/public/assets/chevron-down-m581tl1J.js +0 -1
  65. package/payload/server/public/assets/chevron-left-BAY0xfmR.js +0 -1
  66. package/payload/server/public/assets/chevron-right-Dy_REdYZ.js +0 -1
  67. package/payload/server/public/assets/data-CtOxa94P.js +0 -1
  68. package/payload/server/public/assets/tasks-21egY7A6.js +0 -1
  69. /package/payload/server/public/assets/{useSubAccountSwitcher-DLvqvfoc.js → useSubAccountSwitcher-DotTrvX3.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.486",
3
+ "version": "0.1.488",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -71,7 +71,7 @@ The setup-done claim only fires when `curl -I https://<hostname>` issued from ou
71
71
 
72
72
  ## Data-portal folder index
73
73
 
74
- The portal shows a sub-account's deliverable folders alongside what the client uploaded. Which folders those are is never a list in the code: it is derived per account from that account's own `SCHEMA.md` — the buckets in its `<!-- ontology-buckets -->` region plus `output`, intersected with its `allowed-top-level` block. A brand on a different vertical exposes different folders with no code change. `bin/schema-exposed-dirs.mjs` is the only place that decision is made.
74
+ The portal shows a sub-account's deliverable folders alongside what the client uploaded. Which folders those are is never a list in the code: it is derived per account from that account's own `SCHEMA.md` — the buckets in its `<!-- ontology-buckets -->` region plus `output`, intersected with its `allowed-top-level` block. A brand on a different vertical exposes different folders with no code change. `bin/schema-exposed-dirs.mjs` is the only place that decision is made. An operator can add non-ontology folders (a workflow folder such as `inbound-invoices`) by listing them in `exposeFolders` in the account's `data-portal.json`; each must match `[A-Za-z0-9_-]+` and is never a system folder (`agents`, `secrets`, `specialists`, `state`, `memory`, `cache`, `logs`, `tmp`) or dot-prefixed. Those extras union on top of the schema-derived set and, unlike ontology buckets, are not intersected with `allowed-top-level`; empty or absent behaves as before.
75
75
 
76
76
  `bin/portal-index-push.mjs` walks those folders and full-replaces the account's rows in the portal's D1 `directory` table each cycle. Metadata only — names, sizes, times — so listing survives the device being offline, and a deleted or renamed file converges on the next cycle without a tombstone. The scheduling heartbeat runs it hourly for every account carrying a `data-portal.json`; it is also runnable standalone, which is how it stays operable while the heartbeat spawn path is unproven on a device.
77
77
 
@@ -82,7 +82,7 @@ Exposure is narrowed twice. The install re-derives the account's exposed set fro
82
82
  What to watch, on the device:
83
83
 
84
84
  ```
85
- [portal-index] op=resolve account=<id> schemaPresent=<bool> allowed=<n> domainBuckets=<dirs|none> exposed=<dirs|none>
85
+ [portal-index] op=resolve account=<id> schemaPresent=<bool> allowed=<n> domainBuckets=<dirs|none> extraFolders=<n> exposed=<dirs|none>
86
86
  [portal-index] op=replace account=<id> deleted=all inserted=<n>
87
87
  [portal-index] op=verify account=<id> rowsAfter=<n> expected=<n>
88
88
  [portal-index] op=collision account=<id> dir=<dir> claimedBy=plugin-owned
@@ -90,7 +90,7 @@ What to watch, on the device:
90
90
  [portal-index] op=audit accounts=<n> stale=<n> never-ran=<n>
91
91
  ```
92
92
 
93
- `exposed=none` where deliverables exist means the schema was missing or carried no ontology region — not that the walk found nothing. `rowsAfter` disagreeing with `expected` is a half-applied replace. An `op=collision` line explains a folder that has silently vanished from the client's view, which nothing else would report. `op=exposed-changed` fires only on a delta, so it is rare and always worth reading; a `removed=` naming a populated bucket is the client-visible regression.
93
+ `exposed=none` where deliverables exist means the schema was missing or carried no ontology region — not that the walk found nothing. `rowsAfter` disagreeing with `expected` is a half-applied replace. An `op=collision` line explains a folder that has silently vanished from the client's view, which nothing else would report. `op=exposed-changed` fires only on a delta, so it is rare and always worth reading; a `removed=` naming a populated bucket is the client-visible regression. `extraFolders=<n>` counts the folders the operator requested in `exposeFolders`; a non-zero `extraFolders` with none of those names in `exposed=` means the guard dropped every one.
94
94
 
95
95
  `op=audit` is the standing check, and it is the only one that speaks when the push does **not**. It runs in-process inside the heartbeat rather than as a spawn, and reads only local state, so it cannot be blinded by the same failure it is watching for. It fires hourly even when everything is healthy — an audit that goes quiet on success is indistinguishable from an audit that stopped running. `result=never-ran` on an account with a `data-portal.json` means no cycle has ever succeeded for it; `result=stale` with `ageMin` means cycles have stopped. Its input is written only after `op=verify`, so a stale entry means the push stopped succeeding rather than merely stopped logging.
96
96
 
@@ -42,6 +42,20 @@ function readEnvFile(path) {
42
42
  return out
43
43
  }
44
44
 
45
+ /** Operator-named extra folders for an account, from its data-portal.json.
46
+ * Absent file, absent key, or a non-array value all yield []. The resolver
47
+ * applies the charset-and-system guard; this reader does not pre-filter, so the
48
+ * `extraFolders=<n>` count reflects what the operator asked for.
49
+ * @param {string} accountDir @returns {Promise<string[]>} */
50
+ export async function readExposeFolders(accountDir) {
51
+ try {
52
+ const cfg = JSON.parse(await readFile(join(accountDir, 'data-portal.json'), 'utf8'))
53
+ return Array.isArray(cfg?.exposeFolders) ? cfg.exposeFolders.filter((f) => typeof f === 'string') : []
54
+ } catch {
55
+ return []
56
+ }
57
+ }
58
+
45
59
  /**
46
60
  * Files under `exposed` dirs, at any depth. Dot-prefixed entries are skipped at
47
61
  * every level: `.uploads-tmp/` holds in-flight upload partials (files.ts:25) and
@@ -85,7 +99,7 @@ export async function walkExposed(accountDir, exposed) {
85
99
  * @param {{ accountDir: string, accountId: string, dbName: string,
86
100
  * client: { query: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]> },
87
101
  * log: (line: string) => void, nowIso: string,
88
- * previousExposed?: string[] }} opts
102
+ * exposeFolders?: string[], previousExposed?: string[] }} opts
89
103
  * @returns {Promise<{ exposed: string[], rows: number, schemaPresent: boolean }>}
90
104
  */
91
105
  export async function pushAccount(opts) {
@@ -98,10 +112,15 @@ export async function pushAccount(opts) {
98
112
  schemaText = null
99
113
  }
100
114
 
101
- const r = resolveExposedDirs(schemaText)
115
+ const exposeFolders = Array.isArray(opts.exposeFolders) ? opts.exposeFolders : []
116
+ const r = resolveExposedDirs(schemaText, exposeFolders)
117
+ // extraFolders is the REQUESTED count, not the accepted count: a non-zero
118
+ // extraFolders with none of those names in exposed= means the guard dropped
119
+ // every one, which is distinct from none configured (extraFolders=0).
102
120
  log(
103
121
  `${TAG} op=resolve account=${accountId} schemaPresent=${r.schemaPresent} allowed=${r.allowed.length} ` +
104
- `domainBuckets=${r.domainBuckets.join(',') || 'none'} exposed=${r.exposed.join(',') || 'none'}`,
122
+ `domainBuckets=${r.domainBuckets.join(',') || 'none'} extraFolders=${exposeFolders.length} ` +
123
+ `exposed=${r.exposed.join(',') || 'none'}`,
105
124
  )
106
125
  // The generator gives a plugin-claimed dir to the plugin-owned region only
107
126
  // (account-schema-owned-dirs.py:287), so this is normally silent. When it is
@@ -212,10 +231,10 @@ function arg(flag) {
212
231
  * Every account carrying a data-portal.json has a portal. The file's presence is
213
232
  * the enrolment signal — there is no separate flag to keep in sync with it.
214
233
  * @param {string} accountsRoot
215
- * @returns {Promise<{accountDir: string, accountId: string, dbName: string}[]>}
234
+ * @returns {Promise<{accountDir: string, accountId: string, dbName: string, exposeFolders: string[]}[]>}
216
235
  */
217
236
  async function portalAccounts(accountsRoot) {
218
- /** @type {{accountDir: string, accountId: string, dbName: string}[]} */ const out = []
237
+ /** @type {{accountDir: string, accountId: string, dbName: string, exposeFolders: string[]}[]} */ const out = []
219
238
  /** @type {string[]} */ let names
220
239
  try {
221
240
  names = await readdir(accountsRoot)
@@ -227,7 +246,10 @@ async function portalAccounts(accountsRoot) {
227
246
  try {
228
247
  const cfg = JSON.parse(await readFile(join(accountDir, 'data-portal.json'), 'utf8'))
229
248
  if (typeof cfg?.portalDbName === 'string' && cfg.portalDbName) {
230
- out.push({ accountDir, accountId, dbName: cfg.portalDbName })
249
+ const exposeFolders = Array.isArray(cfg?.exposeFolders)
250
+ ? cfg.exposeFolders.filter((f) => typeof f === 'string')
251
+ : []
252
+ out.push({ accountDir, accountId, dbName: cfg.portalDbName, exposeFolders })
231
253
  } else {
232
254
  console.error(`${TAG} op=skip account=${accountId} reason=no-portalDbName`)
233
255
  }
@@ -292,7 +314,7 @@ async function main() {
292
314
  process.exit(1)
293
315
  }
294
316
 
295
- /** @type {{accountDir: string, accountId: string, dbName: string}[]} */ let targets
317
+ /** @type {{accountDir: string, accountId: string, dbName: string, exposeFolders: string[]}[]} */ let targets
296
318
  if (process.argv.includes('--all-accounts')) {
297
319
  // Same derivation as the platform's own DATA_ROOT (data-path.ts:19), from
298
320
  // the PLATFORM_ROOT the heartbeat already passes.
@@ -307,7 +329,7 @@ async function main() {
307
329
  console.error(`${TAG} op=usage --account-dir <dir> --account <id> --db <name>`)
308
330
  process.exit(1)
309
331
  }
310
- targets = [{ accountDir, accountId, dbName }]
332
+ targets = [{ accountDir, accountId, dbName, exposeFolders: await readExposeFolders(accountDir) }]
311
333
  }
312
334
 
313
335
  const state = readState(platformRoot)
@@ -21,6 +21,18 @@ const OWNED_END = '<!-- plugin-owned-dirs:end -->'
21
21
  /** The delivery convention, always exposed when the account allows it. */
22
22
  export const ALWAYS_EXPOSED = 'output'
23
23
 
24
+ /** Folders that must never be named into exposure, even by an operator. The
25
+ * dot-prefixed `.git`/`.claude` are already excluded by the charset guard
26
+ * below. This is the not-system half of the extra-folder bound. */
27
+ const SYSTEM_DIRS = new Set(['agents', 'secrets', 'specialists', 'state', 'memory', 'cache', 'logs', 'tmp'])
28
+
29
+ /** An operator-named extra folder is accepted only as a single safe segment
30
+ * (the same charset the per-person `--folders` grant enforces) and never a
31
+ * system dir. This drops dot-prefixed names, slashes, and traversal such as
32
+ * `jobs/../secrets`, which would otherwise let the push walk index files under
33
+ * a system dir that the fetch route would refuse to serve. */
34
+ const SAFE_FOLDER = /^[A-Za-z0-9_-]+$/
35
+
24
36
  /**
25
37
  * The generator writes the ontology region with an ASCII hyphen and the
26
38
  * plugin-owned region with an em dash (account-schema-owned-dirs.py:294 vs
@@ -53,10 +65,11 @@ function allowedBlockOrNull(text) {
53
65
 
54
66
  /**
55
67
  * @param {string | null} schemaText Contents of accounts/<id>/SCHEMA.md, or null when absent.
68
+ * @param {string[]} [exposeFolders] Operator-named extra top-level folders. Empty or absent = today.
56
69
  * @returns {{ schemaPresent: boolean, allowed: string[], domainBuckets: string[],
57
70
  * pluginOwned: string[], collisions: string[], exposed: string[] }}
58
71
  */
59
- export function resolveExposedDirs(schemaText) {
72
+ export function resolveExposedDirs(schemaText, exposeFolders = []) {
60
73
  const empty = {
61
74
  schemaPresent: false,
62
75
  allowed: /** @type {string[]} */ ([]),
@@ -97,7 +110,14 @@ export function resolveExposedDirs(schemaText) {
97
110
  const collisions = domainBuckets.filter((d) => pluginOwned.includes(d))
98
111
 
99
112
  const candidates = [ALWAYS_EXPOSED, ...domainBuckets.filter((d) => !collisions.includes(d))]
100
- const exposed = [...new Set(candidates)].filter((d) => allowed.includes(d)).sort()
113
+ const ontologyExposed = [...new Set(candidates)].filter((d) => allowed.includes(d))
114
+ // Extras are unioned AFTER the ontology step and are deliberately NOT
115
+ // intersected with `allowed`: a workflow folder is legitimately outside the
116
+ // ontology set. The charset-plus-deny guard is the whole bound.
117
+ const extras = (Array.isArray(exposeFolders) ? exposeFolders : []).filter(
118
+ (f) => typeof f === 'string' && SAFE_FOLDER.test(f) && !SYSTEM_DIRS.has(f),
119
+ )
120
+ const exposed = [...new Set([...ontologyExposed, ...extras])].sort()
101
121
 
102
122
  return { schemaPresent: true, allowed, domainBuckets, pluginOwned, collisions, exposed }
103
123
  }
@@ -173,4 +173,27 @@ describe('pushAccount', () => {
173
173
  await push(fakeClient(), { log: (l: string) => lines.push(l) })
174
174
  expect(lines.some((l) => l.includes('schemaPresent=false') && l.includes('exposed=none'))).toBe(true)
175
175
  })
176
+
177
+ it('op=resolve carries extraFolders and the walk includes an operator folder', async () => {
178
+ // inbound-invoices is NOT in the fixture allowed block, so this also proves
179
+ // an extra bypasses the ontology/allowed intersection.
180
+ file('inbound-invoices/INV-42.pdf')
181
+ const lines: string[] = []
182
+ const c = fakeClient()
183
+ await push(c, { log: (l: string) => lines.push(l), exposeFolders: ['inbound-invoices'] })
184
+ expect(lines.some((l) => /op=resolve .*extraFolders=1/.test(l))).toBe(true)
185
+ expect(lines.some((l) => /exposed=.*inbound-invoices/.test(l))).toBe(true)
186
+ const inserted = c.calls.filter((x) => /INSERT INTO directory/i.test(x.sql)).map((x) => String(x.params[1]))
187
+ expect(inserted).toContain('inbound-invoices/INV-42.pdf')
188
+ })
189
+
190
+ it('with no exposeFolders the operator folder is not walked and extraFolders=0', async () => {
191
+ file('inbound-invoices/INV-42.pdf')
192
+ const lines: string[] = []
193
+ const c = fakeClient()
194
+ await push(c, { log: (l: string) => lines.push(l) })
195
+ const inserted = c.calls.filter((x) => /INSERT INTO directory/i.test(x.sql)).map((x) => String(x.params[1]))
196
+ expect(inserted).not.toContain('inbound-invoices/INV-42.pdf')
197
+ expect(lines.some((l) => /op=resolve .*extraFolders=0/.test(l))).toBe(true)
198
+ })
176
199
  })
@@ -85,4 +85,47 @@ describe('resolveExposedDirs', () => {
85
85
  const r = resolveExposedDirs('# no fence here')
86
86
  expect(r.exposed).toEqual([])
87
87
  })
88
+
89
+ it('unions an operator-named folder into exposed', () => {
90
+ const r = resolveExposedDirs(schema({ ontology: ONTOLOGY }), ['inbound-invoices'])
91
+ expect(r.exposed).toEqual(['inbound-invoices', 'output', 'quotes'])
92
+ })
93
+
94
+ it('exposes an extra folder that is absent from the allowed block', () => {
95
+ // inbound-invoices is deliberately NOT in ALLOWED — extras bypass that intersection.
96
+ const r = resolveExposedDirs(schema({ ontology: ONTOLOGY }), ['inbound-invoices'])
97
+ expect(r.exposed).toContain('inbound-invoices')
98
+ })
99
+
100
+ it('drops a system folder named as an extra', () => {
101
+ const r = resolveExposedDirs(schema({ ontology: ONTOLOGY }), ['secrets'])
102
+ expect(r.exposed).not.toContain('secrets')
103
+ expect(r.exposed).toEqual(['output', 'quotes'])
104
+ })
105
+
106
+ it('drops a traversal or dotted extra', () => {
107
+ const r = resolveExposedDirs(schema({ ontology: ONTOLOGY }), ['jobs/../secrets', '.git', '..'])
108
+ expect(r.exposed).toEqual(['output', 'quotes'])
109
+ })
110
+
111
+ it('an empty extras list equals today', () => {
112
+ const base = resolveExposedDirs(schema({ ontology: ONTOLOGY }))
113
+ expect(resolveExposedDirs(schema({ ontology: ONTOLOGY }), []).exposed).toEqual(base.exposed)
114
+ })
115
+
116
+ it('never applies extras when the schema is missing', () => {
117
+ const r = resolveExposedDirs(null, ['inbound-invoices'])
118
+ expect(r.exposed).toEqual([])
119
+ expect(r.schemaPresent).toBe(false)
120
+ })
121
+
122
+ it('never applies extras when the allowed block is absent', () => {
123
+ const r = resolveExposedDirs('# no fence here', ['inbound-invoices'])
124
+ expect(r.exposed).toEqual([])
125
+ })
126
+
127
+ it('dedupes an extra that is already an ontology bucket', () => {
128
+ const r = resolveExposedDirs(schema({ ontology: ONTOLOGY }), ['quotes'])
129
+ expect(r.exposed).toEqual(['output', 'quotes'])
130
+ })
88
131
  })
@@ -182,17 +182,27 @@ An account with no `SCHEMA.md` publishes nothing at all. That is deliberate and
182
182
  what the write-path guard does — the guard fails open so an unseeded account is not write-blocked,
183
183
  while a surface facing a client must fail closed.
184
184
 
185
+ An operator can publish extra folders that are not ontology buckets by listing them in
186
+ `exposeFolders` in the account's `data-portal.json`, for example a workflow folder like
187
+ `inbound-invoices`. Each name must be a single folder matching `[A-Za-z0-9_-]+` and is refused if it
188
+ is a system folder (`agents`, `secrets`, `specialists`, `state`, `memory`, `cache`, `logs`, `tmp`) or
189
+ dot-prefixed. These extras are added on top of the schema-derived set and, unlike ontology buckets,
190
+ are not intersected with the `allowed-top-level` block. An empty or absent list behaves exactly as
191
+ before.
192
+
185
193
  The heartbeat runs this hourly for every account carrying a `data-portal.json`. Run it once by hand
186
194
  after assembly so the client has something to see on their first sign-in, and check the output names
187
195
  the folders you expect:
188
196
 
189
197
  ```
190
- [portal-index] op=resolve account=<id> schemaPresent=true allowed=22 domainBuckets=quotes exposed=output,quotes
198
+ [portal-index] op=resolve account=<id> schemaPresent=true allowed=22 domainBuckets=quotes extraFolders=1 exposed=inbound-invoices,output,quotes
191
199
  [portal-index] op=verify account=<id> rowsAfter=41 expected=41
192
200
  ```
193
201
 
194
202
  `exposed=none` on an account that has deliverables means the schema was missing or its ontology
195
- region was absent — not that the walk found nothing.
203
+ region was absent — not that the walk found nothing. `extraFolders=<n>` counts the folders the
204
+ operator requested in `exposeFolders`; a non-zero `extraFolders` with none of those names in
205
+ `exposed=` means the guard dropped every one (a system or dot name, or a bad spelling).
196
206
 
197
207
  ## How a client browses, downloads and uploads
198
208
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: memory
3
3
  description: "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-edge (create/delete typed directed edges between pre-existing nodes), memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match — a Person, which carries no `name`, is matched on its composed `givenName` + `familyName` display name instead — optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 splits across two read-only tools: `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks). Ships three thinking-tool skills: `challenge` (adversarial retrieval — strongest counter-evidence from the graph for a given claim), `connect` (bridge-finding between two topics via shared graph neighbors), and `emerge` (clusters uncategorised KnowledgeDocument and Section nodes into operator-approved Concept proposals)."
4
+ account-owned-dirs: [{"dir": "archive", "description": "Archived obsidian imports and their embedded attachments."}]
4
5
  tools:
5
6
  - name: memory-search
6
7
  publicAllowlist: false
@@ -29,6 +29,7 @@ field-service domain where the captured reference does not document the entity a
29
29
  | Quote | `Quote` | `Job` | `schema:Order` (estimate) | `accountId`, `ref`, `status`, `totalPaymentDue`, `date` | authored |
30
30
  | Part | `Part` | | `schema:Product` | `accountId`, `partId`, `name`, `sku`, `partCategory` | authored |
31
31
  | Purchase Order | `PurchaseOrder` | `Job` | `schema:Order` | `accountId`, `poNumber`, `status`, `completionStatus`, `dateRaised` | JobLogic-exact |
32
+ | Supplier | `Supplier` | | `schema:Organization` | `accountId`, `supplierId`, `accountNumber` | authored |
32
33
  | PPM Contract | `PpmContract` | | `schema:Service` (contract) | `accountId`, `contractId`, `name`, `startDate`, `endDate` | authored |
33
34
  | WhatsApp Group | `WhatsAppGroup` | | `cdm:Channel` | `accountId`, `groupId`, `jobId`, `clientName` | n/a (transport) |
34
35
 
@@ -38,6 +39,11 @@ bucket. A named `Owner` (here `Job`) is a job-owned entity: its records live und
38
39
  the owning bucket's folder (`jobs/<jobId>/…`), not a sibling root bucket. The
39
40
  account SCHEMA.md projection derives a root bucket only for blank-`Owner` labels.
40
41
 
42
+ `Supplier` appears in this table so the projection derives its `suppliers/` bucket,
43
+ which is where a received supplier invoice is filed. It remains an additional label on
44
+ `:Organization` as described below; the row adds a filesystem bucket, not a standalone
45
+ node type.
46
+
41
47
  `Job.status` is one of JobLogic's job statuses: `New Job`, `Allocated`, `Attended`,
42
48
  `Parts To Fit`, `Awaiting Parts`, `Completed`, `Costed`, `Reqs. Invoice`, `Invoiced`, `Paid`,
43
49
  `Cancelled`, `Recall`. `Job.jobType` is the highest categorisation (Call out, Installation,
@@ -184,6 +190,13 @@ one sheet per valuation cycle is a `:Valuation`, a variations sheet is the `:Var
184
190
  register, a costs sheet is the `:JobCost` lines, and per-supplier sheets are the procurement
185
191
  packages. Folder and workbook are two views of one job; both project into this same graph.
186
192
 
193
+ The `Suppliers/` row above is the per-job procurement package, held inside one job folder.
194
+ Separately, the account root carries a projected `suppliers/` bucket, one folder per
195
+ `Supplier` record, holding that supplier's artefacts across every job. A received supplier
196
+ invoice is filed at `suppliers/<supplier>/Invoices/`. It is never filed to a top-level
197
+ folder named after the `InboundInvoice` label; that label describes the node, not the
198
+ folder.
199
+
187
200
  Operators vary this template — they rename folders, add trades, restructure their workbook. This
188
201
  template is the shipped-generic default the agent projects against; the graph follows this one
189
202
  canonical ontology, not a per-operator schema. Where a real folder does not fit the default, the
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=free-busy-url.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"free-busy-url.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/free-busy-url.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildFreeBusyUrl } from '../free-busy-url.js';
3
+ describe('buildFreeBusyUrl', () => {
4
+ it('carries the account id and the escaped from/to', () => {
5
+ const url = buildFreeBusyUrl('19500', 'dddb9148-7ba4-4de7-a0da-6eadc5743eda', '2026-07-21T00:00:00Z', '2026-08-05T00:00:00Z');
6
+ expect(url).toContain('http://127.0.0.1:19500/api/calendar/free-busy');
7
+ expect(url).toContain('account=dddb9148-7ba4-4de7-a0da-6eadc5743eda');
8
+ expect(url).toContain('from=2026-07-21T00%3A00%3A00Z');
9
+ expect(url).toContain('to=2026-08-05T00%3A00%3A00Z');
10
+ });
11
+ });
12
+ //# sourceMappingURL=free-busy-url.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"free-busy-url.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/free-busy-url.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEtD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,EAAE,sCAAsC,EAAE,sBAAsB,EAAE,sBAAsB,CAAC,CAAA;QAC7H,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,+CAA+C,CAAC,CAAA;QACtE,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,8CAA8C,CAAC,CAAA;QACrE,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC,CAAA;QACtD,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAA;IACtD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Build the device-local free/busy URL the publish dispatcher fetches. The
3
+ * `account` param names which booking-site account's free/busy to compute, so a
4
+ * multi-tenant install refreshes every site rather than only the one the server
5
+ * resolves by default (Task 1890). Values are URL-encoded for the query string.
6
+ */
7
+ export declare function buildFreeBusyUrl(port: string, accountId: string, from: string, to: string): string;
8
+ //# sourceMappingURL=free-busy-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"free-busy-url.d.ts","sourceRoot":"","sources":["../../src/lib/free-busy-url.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAMlG"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Build the device-local free/busy URL the publish dispatcher fetches. The
3
+ * `account` param names which booking-site account's free/busy to compute, so a
4
+ * multi-tenant install refreshes every site rather than only the one the server
5
+ * resolves by default (Task 1890). Values are URL-encoded for the query string.
6
+ */
7
+ export function buildFreeBusyUrl(port, accountId, from, to) {
8
+ return (`http://127.0.0.1:${port}/api/calendar/free-busy` +
9
+ `?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}` +
10
+ `&account=${encodeURIComponent(accountId)}`);
11
+ }
12
+ //# sourceMappingURL=free-busy-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"free-busy-url.js","sourceRoot":"","sources":["../../src/lib/free-busy-url.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,SAAiB,EAAE,IAAY,EAAE,EAAU;IACxF,OAAO,CACL,oBAAoB,IAAI,yBAAyB;QACjD,SAAS,kBAAkB,CAAC,IAAI,CAAC,OAAO,kBAAkB,CAAC,EAAE,CAAC,EAAE;QAChE,YAAY,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAC5C,CAAA;AACH,CAAC"}
@@ -28,6 +28,7 @@ import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync, mkdtemp
28
28
  import { resolve, join, basename } from "node:path";
29
29
  import { homedir, tmpdir } from "node:os";
30
30
  import { resolveHouseScopedToken } from "../lib/house-scoped-token.js";
31
+ import { buildFreeBusyUrl } from "../lib/free-busy-url.js";
31
32
  function resolveConfigDir() {
32
33
  const platformRoot = process.env.PLATFORM_ROOT;
33
34
  if (!platformRoot)
@@ -41,7 +42,14 @@ function resolveConfigDir() {
41
42
  return brand.configDir;
42
43
  }
43
44
  const CONFIG_DIR = resolve(homedir(), resolveConfigDir());
44
- const LOCK_FILE = resolve(CONFIG_DIR, "publish-availability.lock");
45
+ // Per-account lock (Task 1890): the server spawns one dispatcher per booking-site
46
+ // account each tick, so a single shared lock would make all but one bail
47
+ // result=skipped reason=lock-held. Keyed on the account this run publishes,
48
+ // resolved the same way as `accountId` in main() (env, else the account dir's
49
+ // basename) so a run with only PUBLISH_ACCOUNT_DIR set still locks per account.
50
+ const LOCK_ACCOUNT = process.env.PUBLISH_ACCOUNT_ID ??
51
+ (process.env.PUBLISH_ACCOUNT_DIR ? basename(process.env.PUBLISH_ACCOUNT_DIR) : "default");
52
+ const LOCK_FILE = resolve(CONFIG_DIR, `publish-availability-${LOCK_ACCOUNT}.lock`);
45
53
  function acquireLock() {
46
54
  mkdirSync(CONFIG_DIR, { recursive: true });
47
55
  if (existsSync(LOCK_FILE)) {
@@ -182,7 +190,9 @@ async function main() {
182
190
  const from = utcMidnight(now, 0);
183
191
  const to = utcMidnight(now, daysAhead + 1);
184
192
  const nowIso = now.toISOString();
185
- const url = `http://127.0.0.1:${port}/api/calendar/free-busy?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;
193
+ // Name the account so a multi-tenant install computes this site's free/busy,
194
+ // not whatever the endpoint resolves by default (Task 1890).
195
+ const url = buildFreeBusyUrl(port, accountId, from, to);
186
196
  let payload;
187
197
  try {
188
198
  // Bound the read so a wedged server cannot hold the PID lock.
@@ -1 +1 @@
1
- {"version":3,"file":"publish-availability.js","sourceRoot":"","sources":["../../src/scripts/publish-availability.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"publish-availability.js","sourceRoot":"","sources":["../../src/scripts/publish-availability.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: storage-broker
3
3
  description: Per-account Cloudflare D1/R2 storage broker. The only agent-reachable path to storage; the account-wide credential stays house-only and every operation is scoped to the caller's account.
4
+ account-owned-dirs: [{"dir": "pages", "description": "Cloudflare Pages deploy staging for the account's public sites."}]
4
5
  tools:
5
6
  - name: storage-d1-list
6
7
  publicAllowlist: false
@@ -73,6 +73,20 @@ ALLOWED_MX="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_MX/SCH
73
73
  assert_nogrep "quoting" "$ALLOWED_MX" "mx-no-quoting"
74
74
  assert_nogrep "Plugin-owned top-level workspaces" "$(cat "$A_MX/SCHEMA.md")" "mx-no-section"
75
75
 
76
+ # --- platform plugins declare pages (storage-broker) + archive (memory) ---
77
+ # Task 1891: these two plugin-owned root dirs must land in the allowed block so
78
+ # the account-dir reconcile stops quarantining them. Symlink the real plugin
79
+ # dirs so the case exercises the shipped PLUGIN.md declaration.
80
+ T_PA="$(make_tree '[]')"
81
+ ln -s "$REAL_PROJECT_DIR/plugins/storage-broker" "$T_PA/platform/plugins/storage-broker"
82
+ ln -s "$REAL_PROJECT_DIR/plugins/memory" "$T_PA/platform/plugins/memory"
83
+ A_PA="$(make_account "$T_PA")"
84
+ export PROJECT_DIR="$T_PA/platform"
85
+ merge_owned_dirs_into_schema "$A_PA" >/dev/null
86
+ ALLOWED_PA="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_PA/SCHEMA.md")"
87
+ assert_grep "pages" "$ALLOWED_PA" "pa-allowlist-has-pages"
88
+ assert_grep "archive" "$ALLOWED_PA" "pa-allowlist-has-archive"
89
+
76
90
  # --- reconcile: present=false on bare schema, present=true after merge ---
77
91
  A_RC="$(make_account "$T_SD")"
78
92
  export PROJECT_DIR="$T_SD/platform"
@@ -117,11 +131,11 @@ OUT_C="$(merge_owned_dirs_into_schema "$A_C")"
117
131
  # Match a non-empty derived list, not the bare key (which prints even for zero).
118
132
  assert_grep "domainBuckets=jobs" "$OUT_C" "con-domain-log"
119
133
  # Summary reports the full root set and the job-owned entities with their parent.
120
- assert_grep "roots=jobs,customers,customer-sites,assets,parts,ppm-contracts" "$OUT_C" "con-roots-log"
134
+ assert_grep "roots=jobs,customers,customer-sites,assets,parts,suppliers,ppm-contracts" "$OUT_C" "con-roots-log"
121
135
  assert_grep "nested=Visit<-Job,Quote<-Job,PurchaseOrder<-Job" "$OUT_C" "con-nested-log"
122
136
  ALLOWED_C="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_C/SCHEMA.md")"
123
137
  # Root entities (blank Owner) get a top-level bucket.
124
- for d in jobs customers customer-sites assets parts ppm-contracts; do
138
+ for d in jobs customers customer-sites assets parts ppm-contracts suppliers; do
125
139
  assert_grep "$d" "$ALLOWED_C" "con-allow-$d"
126
140
  done
127
141
  # Job-owned entities (Owner=Job) do NOT get a root bucket.
@@ -137,6 +151,10 @@ assert_grep '`Quote` records live under `jobs/` (owned by `Job`), not a root buc
137
151
  B_C="$(shasum "$A_C/SCHEMA.md" | awk '{print $1}')"
138
152
  merge_owned_dirs_into_schema "$A_C" >/dev/null
139
153
  assert_eq "$(shasum "$A_C/SCHEMA.md" | awk '{print $1}')" "$B_C" "con-idempotent"
154
+ # Supplier is a root bucket (Task 1892): received invoices file under
155
+ # suppliers/<supplier>/Invoices/, not a stray top-level inbound-invoices/.
156
+ RCS="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_C")"
157
+ assert_grep "ontologyEntity=Supplier bucket=suppliers present=true" "$RCS" "con-supplier-root-present"
140
158
 
141
159
  # --- estate-agent vertical: root buckets only, listing-owned entities nested ---
142
160
  T_E="$(make_vertical_tree '["real-agent"]' 'schema-estate-agent')"; A_E="$(make_account "$T_E")"
@@ -151,6 +169,8 @@ for d in viewings offers; do
151
169
  assert_nogrep "$d" "$ALLOWED_E" "ea-noroot-$d"
152
170
  done
153
171
  assert_nogrep "jobs" "$ALLOWED_E" "ea-no-sitedesk-jobs"
172
+ # The suppliers/ bucket is construction-only; it must not leak into another vertical.
173
+ assert_nogrep "suppliers" "$ALLOWED_E" "ea-no-suppliers"
154
174
 
155
175
  # --- no vertical declared: generic base only ---
156
176
  T_N="$(make_tree '[]')"; A_N="$(make_account "$T_N")"
@@ -1 +1 @@
1
- {"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AA2EA,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAE9D;AAED;;8EAE8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAcpE;AA0FD;;;gFAGgF;AAChF,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,MAAM,GAAG,eAAe,CAwDvF;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;oCAGoC;AACpC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAwBtD;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED;oDACoD;AACpD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,aAAa,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3C,yBAAyB,CAiB3B"}
1
+ {"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AA2FA,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAE9D;AAED;;8EAE8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAcpE;AA0FD;;;gFAGgF;AAChF,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,MAAM,GAAG,eAAe,CA6DvF;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;oCAGoC;AACpC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAwBtD;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED;oDACoD;AACpD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,aAAa,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3C,yBAAyB,CAiB3B"}
@@ -37,7 +37,17 @@ const QUARANTINE_DIR = '.quarantine';
37
37
  * an account carrying a stale/older-template/hand-edited schema whose allowed
38
38
  * block omits one of these must not have it moved. This is a fail-SAFE belt on
39
39
  * the control plane, the mirror of the guard's fail-OPEN on a missing schema —
40
- * operator strays are still quarantined; the account's own machinery is not. */
40
+ * operator strays are still quarantined; the account's own machinery is not.
41
+ *
42
+ * The second group is platform-written control-plane FILES the platform reads
43
+ * and writes at the account root every run. Unlike the tool-owned buckets, no
44
+ * plugin declares them into the SCHEMA.md allowed-top-level block, so without
45
+ * this belt the sweep quarantines them and the feature that reads each one goes
46
+ * dark: calendar-availability.json is the booking page's availability config (a
47
+ * quarantine froze a live booking page, Task 1877 fix); wa/webchat/telegram
48
+ * -channel-bindings.json route each channel's inbound; canonical-webchat-session
49
+ * .json pins the webchat session; session-titles.json holds operator renames.
50
+ * Each is `join(accountDir, <name>)` in platform code, none is operator data. */
41
51
  const PROTECTED_TOP_LEVEL = new Set([
42
52
  '.git',
43
53
  '.claude',
@@ -45,6 +55,12 @@ const PROTECTED_TOP_LEVEL = new Set([
45
55
  'SCHEMA.md',
46
56
  'account.json',
47
57
  'AGENTS.md',
58
+ 'calendar-availability.json',
59
+ 'wa-channel-bindings.json',
60
+ 'webchat-channel-bindings.json',
61
+ 'telegram-channel-bindings.json',
62
+ 'canonical-webchat-session.json',
63
+ 'session-titles.json',
48
64
  ]);
49
65
  /** Buckets whose depth fs-schema-guard.sh checks, with the same 3-segment cap.
50
66
  * Every other top-level bucket is tool-owned and unbounded — not walked. This
@@ -208,7 +224,12 @@ export function reconcileAccount(accountDir, now) {
208
224
  catch {
209
225
  return empty;
210
226
  }
211
- const strays = rootEntries.filter((name) => !PROTECTED_TOP_LEVEL.has(name) && !allowed.has(name));
227
+ // A dot-prefixed top-level entry is tool/system state, never operator data —
228
+ // the same judgement countBadNameEntries applies when it skips dotfiles. The
229
+ // stray check must agree, or it quarantines .remember/.superpowers/.wrangler
230
+ // and breaks the tool that owns each (Task 1891). .git/.claude/.quarantine
231
+ // stay covered by PROTECTED_TOP_LEVEL regardless.
232
+ const strays = rootEntries.filter((name) => !name.startsWith('.') && !PROTECTED_TOP_LEVEL.has(name) && !allowed.has(name));
212
233
  let quarantined = 0;
213
234
  if (strays.length > 0) {
214
235
  const quarantineRoot = join(accountDir, QUARANTINE_DIR);