@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.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/superpowers-sprint/SKILL.md +26 -0
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
- package/payload/platform/plugins/memory/PLUGIN.md +10 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +5 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js +141 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts +45 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js +122 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js +26 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js +201 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +89 -33
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +6 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js +148 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +83 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +90 -5
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js +111 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +38 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +116 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +13 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +81 -8
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js +39 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +9 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/scripts/__tests__/check-canonical-tool-names.test.sh +162 -0
- package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +53 -0
- package/payload/platform/scripts/__tests__/task-id-citation.test.sh +72 -0
- package/payload/platform/scripts/check-canonical-tool-names.mjs +36 -4
- package/payload/platform/scripts/check-no-task-id-leaks.mjs +80 -62
- package/payload/platform/scripts/lib/canonical-tool-names.mjs +130 -26
- package/payload/platform/scripts/lib/task-id-citation.mjs +67 -0
- package/payload/platform/.docs/search-surface-contract.md +0 -58
- package/payload/platform/docs/superpowers/plans/2026-06-02-task-610-follower-202-retry.md +0 -372
- package/payload/platform/docs/superpowers/plans/2026-06-04-public-agent-knowledge-delivery.md +0 -230
- package/payload/platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +0 -544
- package/payload/platform/docs/superpowers/plans/2026-07-11-task-1557-operator-on-behalf-preference-attribution.md +0 -542
- package/payload/platform/docs/superpowers/plans/2026-07-13-account-schema-ontology-projection.md +0 -547
- package/payload/platform/docs/superpowers/plans/2026-07-14-graph-top-level-labels-ontology-single-source.md +0 -458
- package/payload/platform/docs/superpowers/plans/2026-07-17-task-1736-retire-mcp-stderr-tee.md +0 -397
- package/payload/platform/docs/superpowers/plans/2026-07-18-graph-native-ledger.md +0 -807
- package/payload/platform/docs/superpowers/plans/2026-07-18-ledger-write-path-fix.md +0 -109
- package/payload/platform/docs/superpowers/plans/2026-07-20-storage-pages-create.md +0 -82
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +0 -673
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +0 -1244
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +0 -462
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +0 -321
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1829-platform-ui-typecheck.md +0 -830
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +0 -2177
- package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +0 -124
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +0 -107
- package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +0 -205
- package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +0 -748
- package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +0 -1716
- package/payload/platform/docs/superpowers/plans/2026-07-23-task-1930-adherence-enforcement.md +0 -422
- package/payload/platform/docs/superpowers/plans/2026-07-24-task-1942-subaccount-switcher-brand-head.md +0 -327
- package/payload/platform/docs/superpowers/plans/2026-07-25-task-1974-uploads-intake-inbox.md +0 -235
- package/payload/platform/docs/superpowers/plans/2026-07-25-task-1976-intra-folder-hygiene.md +0 -215
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +0 -555
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-1926-portal-push-concurrency-guard.md +0 -482
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +0 -394
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +0 -624
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md +0 -303
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2052-account-machinery-write-fence.md +0 -250
- package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +0 -65
- package/payload/platform/docs/superpowers/specs/2026-06-02-task-610-follower-202-retry-design.md +0 -116
- package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +0 -170
- package/payload/platform/docs/superpowers/specs/2026-07-11-task-1557-operator-on-behalf-preference-attribution-design.md +0 -90
- package/payload/platform/docs/superpowers/specs/2026-07-13-account-schema-ontology-projection-design.md +0 -178
- package/payload/platform/docs/superpowers/specs/2026-07-14-graph-top-level-labels-ontology-single-source-design.md +0 -119
- package/payload/platform/docs/superpowers/specs/2026-07-17-task-1736-retire-mcp-stderr-tee-design.md +0 -128
- package/payload/platform/docs/superpowers/specs/2026-07-18-graph-native-ledger-design.md +0 -241
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +0 -177
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +0 -201
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +0 -217
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +0 -80
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +0 -97
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md +0 -163
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +0 -163
- package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +0 -121
- package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +0 -103
- package/payload/platform/docs/superpowers/specs/2026-07-23-task-1930-adherence-enforcement-design.md +0 -81
- package/payload/platform/docs/superpowers/specs/2026-07-24-task-1942-subaccount-switcher-brand-head-design.md +0 -35
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +0 -232
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-1926-portal-push-concurrency-guard-design.md +0 -200
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +0 -169
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +0 -139
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +0 -127
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2052-account-machinery-write-fence-design.md +0 -150
- package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +0 -155
package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# Task 1829 — package-wide typecheck for `platform/ui` (design)
|
|
2
|
-
|
|
3
|
-
**Date:** 2026-07-20
|
|
4
|
-
**Task:** [`.tasks/pending/1829-platform-ui-typechecks-nowhere-so-every-type-contract-is-advisory.md`](../../../../.tasks/pending/1829-platform-ui-typechecks-nowhere-so-every-type-contract-is-advisory.md)
|
|
5
|
-
**Depends on:** 1825 (landed — the narrow loop-contract gate this replaces)
|
|
6
|
-
|
|
7
|
-
## Problem
|
|
8
|
-
|
|
9
|
-
Nothing in `platform/ui` runs the TypeScript compiler. `npm run build` is `vite build && tsup`,
|
|
10
|
-
both esbuild-backed, and esbuild strips types without reading them. There is no `lint` script and
|
|
11
|
-
no `typecheck` script. None of the eighteen prebuild gates invokes `tsc`, and no CI workflow does
|
|
12
|
-
either. A type error in this package ships.
|
|
13
|
-
|
|
14
|
-
Task 1825 built a compile-time contract for the loop-row wire shape, found nothing enforced it, and
|
|
15
|
-
added `scripts/check-loop-contract-types.mjs` — a gate that runs `tsc` and then discards every
|
|
16
|
-
error outside six named files. Six files are gated; roughly three hundred are not.
|
|
17
|
-
|
|
18
|
-
## Measurements
|
|
19
|
-
|
|
20
|
-
Every figure below was measured in the task-1829 worktree on 2026-07-20, Node 22.22.0, with
|
|
21
|
-
`platform/node_modules` and `platform/ui/node_modules` symlinked from the parent checkout.
|
|
22
|
-
|
|
23
|
-
The task file's stated figures do not survive measurement, and three of its premises change as a
|
|
24
|
-
result.
|
|
25
|
-
|
|
26
|
-
| Quantity | Task file | Measured |
|
|
27
|
-
|---|---|---|
|
|
28
|
-
| `tsc --noEmit` wall time | ~51s | 19s cold, 13s warm |
|
|
29
|
-
| Total errors | 103 | 97 before `build:lib`, 80 after |
|
|
30
|
-
| Error classes | 2 | 3 |
|
|
31
|
-
|
|
32
|
-
**The sibling `dist/` question is settled: it is a setup problem, not a source problem.** Running
|
|
33
|
-
`npm run build:lib` in `platform/` (clean, 24s) clears all 14 `TS2307` unresolved-import errors. It
|
|
34
|
-
also removes two files from the report entirely — `../lib/graph-search/src/index.ts` and
|
|
35
|
-
`../plugins/memory/mcp/src/tools/memory-delete.ts` — because with the dists present those imports
|
|
36
|
-
resolve to `dist/*.d.ts` rather than dragging the sibling `.ts` sources into the program. So the
|
|
37
|
-
gate's prerequisite is a documented build order, exactly as the task predicted.
|
|
38
|
-
|
|
39
|
-
**The runtime cost concern dissolves.** At 13–19s there is no case for `--incremental` with a
|
|
40
|
-
cached build-info file, and no case for moving the check to CI. It goes in prebuild.
|
|
41
|
-
|
|
42
|
-
**There is a third error class the task did not anticipate.** After `build:lib`, the 80 remaining
|
|
43
|
-
errors split three ways:
|
|
44
|
-
|
|
45
|
-
| Class | Count | Nature |
|
|
46
|
-
|---|---|---|
|
|
47
|
-
| Test-file errors | 62 | Genuine; implicit-`any` params and stale fixtures |
|
|
48
|
-
| Production-file errors, fixable in-tree | 8 | Genuine |
|
|
49
|
-
| `neo4j-driver` v5/v6 type skew | 10 | Cross-package version conflict |
|
|
50
|
-
|
|
51
|
-
## The `neo4j-driver` skew
|
|
52
|
-
|
|
53
|
-
`platform/ui` declares `neo4j-driver@^6.0.1`. Thirteen workspace packages under `platform/` declare
|
|
54
|
-
`^5.28.1`, which hoists to `platform/node_modules` as 5.28.3. `platform/lib/*` has no
|
|
55
|
-
`package.json`, so those libs compile against the hoisted v5, while `platform/ui` hands them v6
|
|
56
|
-
`Session` objects. That is the eight `TS2739` and two `TS2345` errors.
|
|
57
|
-
|
|
58
|
-
It is not a worktree artefact. It reproduces wherever both trees are installed.
|
|
59
|
-
|
|
60
|
-
At runtime it is currently inert but not honest. `neo4j-driver` is external to the tsup bundle,
|
|
61
|
-
while `platform/lib/*` is inlined into `server.js`. So lib code compiled against v5 types already
|
|
62
|
-
executes against v6 in the server process, and against v5 in each plugin MCP process. Both work
|
|
63
|
-
today.
|
|
64
|
-
|
|
65
|
-
`platform/ui` has carried `^6` since the fold-in commit; it was never deliberately bumped. It uses
|
|
66
|
-
only `neo4j.int`, `isInt`, `driver`, `auth`, the six temporal `is*` guards, and `type { Session }`.
|
|
67
|
-
Every one exists in v5 — the export diff shows v6 is a strict superset and v5 has nothing v6 lacks.
|
|
68
|
-
|
|
69
|
-
**Decision: align downward.** `platform/ui` moves to `^5.28.1`. This is one source file, it lands
|
|
70
|
-
`platform/ui` on the same version as the seventeen other packages and as what `platform/lib/*` is
|
|
71
|
-
compiled against, and it stays inside the task's scope boundary. Aligning upward would move every
|
|
72
|
-
platform plugin MCP, service and premium-plugin onto v6 and breach the task's stated out-of-scope
|
|
73
|
-
line; that is its own sprint.
|
|
74
|
-
|
|
75
|
-
`payload/server/package.json` is generated from `platform/ui/package.json` at bundle time
|
|
76
|
-
(`packages/create-maxy-code/scripts/bundle.js`, `externalDeps`), so the deployed server's driver
|
|
77
|
-
version follows this one file.
|
|
78
|
-
|
|
79
|
-
## The two production defects
|
|
80
|
-
|
|
81
|
-
Both were found by the compiler and neither is cosmetic.
|
|
82
|
-
|
|
83
|
-
**`ChannelIcon` — a live crash.** `GLYPHS` is typed `Record<ReaderChannel, …>` but holds only
|
|
84
|
-
`whatsapp` and `telegram`; `ReaderChannel` is `'whatsapp' | 'telegram' | 'webchat'`. At
|
|
85
|
-
`Sidebar.tsx:1499` a conversation row passes `c.channel`, and `select-sessions.ts:43` states that a
|
|
86
|
-
row can carry any of the three. A webchat row there makes `GLYPHS['webchat']` undefined and `g.fill`
|
|
87
|
-
throws, taking the sidebar subtree with it. `SessionChannelBadge`, in the same file, already routes
|
|
88
|
-
webchat to a globe and unknown tokens to a neutral bubble.
|
|
89
|
-
|
|
90
|
-
The fix is not to invent a webchat brand path. `ChannelIcon` renders brand glyphs by design, which
|
|
91
|
-
is why `BRAND_CHANNELS` maps only two. Its prop type narrows to the brand channels, and
|
|
92
|
-
`Sidebar.tsx:1499` uses `SessionChannelBadge`, the component built for the all-three case. For
|
|
93
|
-
whatsapp and telegram this is behaviour-preserving; `SessionChannelBadge` delegates to
|
|
94
|
-
`ChannelIcon` at the same size.
|
|
95
|
-
|
|
96
|
-
**`webchat-gateway` — the type is wrong, not the value.** `public-session-end-review.ts:41` declares
|
|
97
|
-
`dispatchFor: 'eviction' | 'exit'`. The local callback type at `webchat-gateway.ts:83` narrowed it
|
|
98
|
-
to `'eviction'`. Line 422 passing `'exit'` is the documented public-exit path. Widening line 83 is a
|
|
99
|
-
type-only fix with no behaviour change.
|
|
100
|
-
|
|
101
|
-
## What is built
|
|
102
|
-
|
|
103
|
-
**Root-cause fixes, 70 errors to 0.** No error is suppressed; each is removed at source.
|
|
104
|
-
|
|
105
|
-
| Fix | Clears |
|
|
106
|
-
|---|---|
|
|
107
|
-
| `platform/ui/package.json`: `neo4j-driver` `^6.0.1` → `^5.28.1` | 10 |
|
|
108
|
-
| Test files: annotate implicit-`any` params, update stale fixtures | 62 |
|
|
109
|
-
| `sessions.ts` ×4, `whatsapp-reader.ts` ×1 implicit-`any` | 5 |
|
|
110
|
-
| `ChannelIcon` narrowed; `Sidebar.tsx:1499` uses `SessionChannelBadge` | 1 |
|
|
111
|
-
| `webchat-gateway.ts:83` `dispatchFor` widened | 1 |
|
|
112
|
-
| `sidebar-sessions.ts:283` `port` typed `string`, `requirePortEnv` returns `number` | 1 |
|
|
113
|
-
|
|
114
|
-
**The gate.** `scripts/check-types.mjs` runs `tsc --noEmit` over the whole package and fails on any
|
|
115
|
-
diagnostic. Its ignore list is empty and it says so in its own output, so a future non-zero ignored
|
|
116
|
-
count is visible in build logs rather than inferred.
|
|
117
|
-
|
|
118
|
-
It keeps both of 1825's blind-pass protections, because they are why that gate could be trusted:
|
|
119
|
-
|
|
120
|
-
- A diagnostic with no `path(line,col)` prefix means the compiler failed *before* typechecking
|
|
121
|
-
rather than while typechecking, and is reported as a configuration failure. Exit status cannot
|
|
122
|
-
carry this distinction — `tsc` exits 2 on a normal run with errors and 1 when it cannot find
|
|
123
|
-
`tsconfig.json` at all.
|
|
124
|
-
- A non-zero exit with no parseable diagnostic means nothing was typechecked, and fails loudly
|
|
125
|
-
rather than reporting success.
|
|
126
|
-
|
|
127
|
-
It adds one pre-flight of its own: the sibling `lib/*/dist` artefacts that `platform/ui` imports
|
|
128
|
-
must exist. If they do not, it exits naming `npm run build:lib` instead of emitting fourteen
|
|
129
|
-
confusing unresolved-import errors. This is the documented build order the task asked for, enforced
|
|
130
|
-
as a state check rather than left in prose.
|
|
131
|
-
|
|
132
|
-
`check-loop-contract-types.mjs` is deleted. Two gates asserting the same invariant at different
|
|
133
|
-
scopes is the drift 1825 exists to prevent, reintroduced one layer up.
|
|
134
|
-
|
|
135
|
-
A `typecheck` script is added to `package.json` so the check is runnable on its own.
|
|
136
|
-
|
|
137
|
-
## Testing
|
|
138
|
-
|
|
139
|
-
The load-bearing case is a deliberately introduced type error in a file the narrow gate never
|
|
140
|
-
covered. It must be observed failing, and observed passing once removed. A gate that has only ever
|
|
141
|
-
been seen passing has not been shown to work.
|
|
142
|
-
|
|
143
|
-
Separately, the gate must be shown not to pass blind: pointed at a tree where `tsc` cannot run, it
|
|
144
|
-
exits non-zero rather than reporting success. And the dist pre-flight must be observed firing with a
|
|
145
|
-
lib dist absent.
|
|
146
|
-
|
|
147
|
-
The `neo4j-driver` downgrade is verified against a running Neo4j, not only against the compiler. A
|
|
148
|
-
dependency change that only ever passed a typecheck has not been shown to work either.
|
|
149
|
-
|
|
150
|
-
## Scope boundaries
|
|
151
|
-
|
|
152
|
-
**In scope:** making `tsc` run and pass over `platform/ui`; triaging every pre-existing error;
|
|
153
|
-
retiring the narrow loop-contract gate; aligning `platform/ui` onto `neo4j-driver` v5.
|
|
154
|
-
|
|
155
|
-
**Out of scope:** the other platform packages (`platform/lib/*`, `platform/plugins/*`), which still
|
|
156
|
-
typecheck nowhere — filed as a follow-up task rather than left as prose. Adding eslint. Changing
|
|
157
|
-
`tsconfig.json` strictness. Moving the seventeen v5 packages to v6. The loop-row contract itself,
|
|
158
|
-
which 1825 closed.
|
|
159
|
-
|
|
160
|
-
## Observability
|
|
161
|
-
|
|
162
|
-
None at runtime; this is a build-time contract with no runtime surface. The gate's own output names
|
|
163
|
-
the error count found and the count ignored.
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# Task 1831 — data portal class-derived allowlist (design)
|
|
2
|
-
|
|
3
|
-
Date: 2026-07-20. Task: [`.tasks/1831-data-portal-exposes-only-uploads-not-the-sub-accounts-folder-tree.md`](../../../../.tasks/1831-data-portal-exposes-only-uploads-not-the-sub-accounts-folder-tree.md).
|
|
4
|
-
|
|
5
|
-
## Problem
|
|
6
|
-
|
|
7
|
-
The portal only shows what the client uploaded. `processFiles`
|
|
8
|
-
(`skills/data-portal/template/functions/api/files.ts:11-24`) reads
|
|
9
|
-
`manifest WHERE ownerId = ?`, and `manifest` holds one row per `/api/upload`
|
|
10
|
-
object. There is no device-to-portal path at all, so an account can receive
|
|
11
|
-
documents from a client but cannot hand any back.
|
|
12
|
-
|
|
13
|
-
A fixed folder list cannot solve it. On SiteDesk account
|
|
14
|
-
`098a18a3-1741-447f-9778-65470645d57d` the deliverables sit in both `output/`
|
|
15
|
-
and `quotes/`, and `quotes/` is projected per account from the brand's vertical
|
|
16
|
-
ontology (`platform/scripts/lib/account-schema-owned-dirs.py:288-296`), not
|
|
17
|
-
shipped in the template. A different vertical projects different buckets.
|
|
18
|
-
|
|
19
|
-
Provenance cannot be recovered either. `upsertFileArtifact`
|
|
20
|
-
(`platform/ui/app/lib/file-index.ts:294-307`) stores no writer, and the watcher
|
|
21
|
-
runs agent writes and operator uploads through one path
|
|
22
|
-
(`platform/ui/app/lib/file-watcher.ts:30-34`).
|
|
23
|
-
|
|
24
|
-
## Approach
|
|
25
|
-
|
|
26
|
-
Key on each folder's declared class, not its name.
|
|
27
|
-
|
|
28
|
-
Exposed = the dirs named in the account's `<!-- ontology-buckets -->` region,
|
|
29
|
-
plus `output`, intersected with the ```` ```allowed-top-level ```` block. All
|
|
30
|
-
three are read from `accounts/<accountId>/SCHEMA.md`, which is generated per
|
|
31
|
-
account. A new vertical projects new buckets and they appear with no code change.
|
|
32
|
-
|
|
33
|
-
## Data flow
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
DEVICE (each heartbeat) D1 PORTAL
|
|
37
|
-
read SCHEMA.md
|
|
38
|
-
resolve exposed dirs
|
|
39
|
-
walk those dirs only ── metadata ──▶ directory ──── read ───▶ folder tree
|
|
40
|
-
│
|
|
41
|
-
click a file ───────────────┘
|
|
42
|
-
│
|
|
43
|
-
HEAD (alive?) ◀───────────┤
|
|
44
|
-
│
|
|
45
|
-
signed link, 5 min ──────┘
|
|
46
|
-
│
|
|
47
|
-
verify signature ◀──────────────┘
|
|
48
|
-
re-resolve exposed dirs
|
|
49
|
-
serve bytes
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Bytes never enter D1 or R2. Listing therefore survives the device being offline;
|
|
53
|
-
download does not, and says so plainly.
|
|
54
|
-
|
|
55
|
-
## Components
|
|
56
|
-
|
|
57
|
-
| File | Responsibility |
|
|
58
|
-
|---|---|
|
|
59
|
-
| `plugins/cloudflare/bin/schema-exposed-dirs.mjs` | `SCHEMA.md` text in, exposed dir names out. Pure, no I/O. |
|
|
60
|
-
| `plugins/cloudflare/bin/portal-index-push.mjs` | walk exposed dirs, full-replace the account's `directory` rows |
|
|
61
|
-
| `.../template/functions/api/_lib/portal-sign.mjs` | mint + verify the signed link, portal side (sits beside `passcode.mjs`, same precedent) |
|
|
62
|
-
| `plugins/cloudflare/skills/data-portal/schema.sql` | `directory` table, `people.accountId` column |
|
|
63
|
-
| `.../template/functions/api/files.ts` | folder entries beside today's uploads |
|
|
64
|
-
| `.../template/functions/api/download.ts` | uploads to R2 unchanged; indexed files get a signed link |
|
|
65
|
-
| `.../template/portal.js`, `index.html` | breadcrumb folder navigation |
|
|
66
|
-
| `platform/ui/server/routes/portal-fetch.ts` | verify signature, re-resolve exposure, serve |
|
|
67
|
-
|
|
68
|
-
## Parsing contract
|
|
69
|
-
|
|
70
|
-
Pinned against the generator, because two details are easy to get wrong and both
|
|
71
|
-
fail silently:
|
|
72
|
-
|
|
73
|
-
- The ontology region lists `` - `<dir>/` - one folder per <Label> record. `` with
|
|
74
|
-
an **ASCII hyphen**; the plugin-owned region uses an **em dash**
|
|
75
|
-
(`account-schema-owned-dirs.py:294` vs `:281`). Parsing on the em dash matches
|
|
76
|
-
nothing and reads as "this account has no buckets".
|
|
77
|
-
- An absent ontology region is normal, not corrupt: `merge()` strips both regions
|
|
78
|
-
before regenerating, and a brand with no declared vertical never gets one. It
|
|
79
|
-
yields `output` alone.
|
|
80
|
-
- A dir claimed by a plugin is described only in the plugin-owned region
|
|
81
|
-
(`:287`), so it will not parse as a domain bucket and drops out of the exposed
|
|
82
|
-
set. Instrumented, not fixed, here.
|
|
83
|
-
|
|
84
|
-
## Authentication
|
|
85
|
-
|
|
86
|
-
One secret per account, generated at portal assembly, written to the account's
|
|
87
|
-
secrets file and set as the Pages secret `PORTAL_FETCH_SECRET`. The install
|
|
88
|
-
origin is set alongside it as `PORTAL_INSTALL_ORIGIN`.
|
|
89
|
-
|
|
90
|
-
The portal signs `accountId`, `relPath` and an expiry with HMAC-SHA256 via Web
|
|
91
|
-
Crypto and puts the signature in the link. The install verifies it. The secret
|
|
92
|
-
itself never travels, and each grant covers one file for five minutes.
|
|
93
|
-
|
|
94
|
-
The install re-resolves the exposed dir list from `SCHEMA.md` on every request
|
|
95
|
-
rather than trusting the path it was handed. A valid signature over
|
|
96
|
-
`documents/private.pdf` is still refused. That check is the security boundary;
|
|
97
|
-
the signature only proves the request came from the portal.
|
|
98
|
-
|
|
99
|
-
**The two ends do not share a module.** No production code in `platform/ui`
|
|
100
|
-
imports from `platform/plugins` today — only one test does
|
|
101
|
-
(`server/lib/calendar-ics.test.ts:12`) — and this sprint will not invent that
|
|
102
|
-
coupling for fifteen lines of HMAC. The portal mints with
|
|
103
|
-
`_lib/portal-sign.mjs`; `portal-fetch.ts` carries its own verify. Both call Web
|
|
104
|
-
Crypto, which Node 22 and Workers both provide, so the algorithm is identical by
|
|
105
|
-
construction. A parity test mints with the portal module and verifies with the
|
|
106
|
-
install's function, which catches drift at the only place it could occur.
|
|
107
|
-
|
|
108
|
-
## Error handling
|
|
109
|
-
|
|
110
|
-
| Condition | Response |
|
|
111
|
-
|---|---|
|
|
112
|
-
| device unreachable (HEAD fails) | 503 `device-offline` |
|
|
113
|
-
| signature absent, altered, or expired | 401 |
|
|
114
|
-
| path outside the exposed dirs | 404, regardless of signature |
|
|
115
|
-
| path outside the account partition | 404 |
|
|
116
|
-
| `SCHEMA.md` missing or unparseable | expose nothing |
|
|
117
|
-
|
|
118
|
-
The last one deliberately inverts `fs-schema-guard.sh:76-78`, which fails open so
|
|
119
|
-
an unseeded legacy account is not write-blocked. A read surface facing a client
|
|
120
|
-
must fail closed. The code carries that reason inline.
|
|
121
|
-
|
|
122
|
-
## Testing
|
|
123
|
-
|
|
124
|
-
Unit only. No portal is deployed anywhere and no account exists on this Mac, so
|
|
125
|
-
nothing can be driven end to end.
|
|
126
|
-
|
|
127
|
-
- **Resolver.** SiteDesk-shaped text yields `output,quotes`. No ontology region
|
|
128
|
-
yields `output`. A dir absent from the allowed block is dropped. A region
|
|
129
|
-
written with an em dash yields zero buckets, which asserts the hyphen contract
|
|
130
|
-
rather than assuming it. Missing file yields nothing.
|
|
131
|
-
- **Walker.** Fixture tree carrying `output/`, `quotes/`, `documents/`,
|
|
132
|
-
`uploads/`, `.claude/` produces rows for the first two only, at any depth.
|
|
133
|
-
Delete leaves no stale row; rename produces one row at the new path; a
|
|
134
|
-
repeated run is identical.
|
|
135
|
-
- **Listing.** Account A never sees account B's rows. Prefix filtering
|
|
136
|
-
synthesises directories from paths. A `people` row with no `accountId` returns
|
|
137
|
-
an empty list, not an error.
|
|
138
|
-
- **Signing.** Round trip verifies. Expired fails. Tampered path fails. Tampered
|
|
139
|
-
account fails. Parity: a token minted by the portal module verifies with the
|
|
140
|
-
install's own function, so the two implementations cannot drift unnoticed.
|
|
141
|
-
- **Fetch route.** Well-signed request for a non-exposed path is refused. Path
|
|
142
|
-
traversal is refused. Exposed path serves with the right content type.
|
|
143
|
-
- **Regression.** The existing 139 tests across 10 files stay green.
|
|
144
|
-
`typecheck:templates` stays at exit 0.
|
|
145
|
-
|
|
146
|
-
## Out of scope
|
|
147
|
-
|
|
148
|
-
The upload ingestion sweep; client write-back, delete or rename; mirroring bytes
|
|
149
|
-
into R2; recording write provenance (the SiteDesk quote engine writes through its
|
|
150
|
-
own tool, not `Write`/`Edit`, so `quotes/` would never register); fixing the
|
|
151
|
-
plugin-owned name collision; search; the `/data` admin browser.
|
|
152
|
-
|
|
153
|
-
## Dependencies
|
|
154
|
-
|
|
155
|
-
`people.accountId` is new, so existing enrolments are re-run — the upsert on
|
|
156
|
-
`ownerId` makes that a re-run, not a migration. Each portal-serving account needs
|
|
157
|
-
a seeded `SCHEMA.md` or it exposes nothing. Downloads need the install reachable
|
|
158
|
-
at its tunnel hostname; listing does not.
|
|
159
|
-
|
|
160
|
-
The heartbeat spawn this copies
|
|
161
|
-
(`plugins/scheduling/mcp/src/scripts/check-due-events.ts:797`) has never been
|
|
162
|
-
observed running on a Pi, which is Task 1828. `portal-index-push.mjs` is
|
|
163
|
-
therefore runnable standalone, so it stays operable while 1828 is open.
|
package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
# Task 1899 — the account-dir reconcile reports and never moves
|
|
2
|
-
|
|
3
|
-
Design agreed 2026-07-22. Source task: `.tasks/1899-account-dir-reconcile-moves-live-state.md`.
|
|
4
|
-
|
|
5
|
-
## The problem this removes
|
|
6
|
-
|
|
7
|
-
`reconcileAccount` treats every account-root entry that is neither in the account's
|
|
8
|
-
`SCHEMA.md` `allowed-top-level` block nor in the hand-maintained
|
|
9
|
-
`PROTECTED_TOP_LEVEL` set as a stray, and moves it into `<account>/.quarantine/`.
|
|
10
|
-
The closed list has never matched what the platform and its agents write at an
|
|
11
|
-
account root, so every mismatch is a live feature or live data going dark with no
|
|
12
|
-
error anywhere. `PROTECTED_TOP_LEVEL` has been extended once per casualty
|
|
13
|
-
(`calendar-availability.json` was Task 1877). The sweep is what changes; the
|
|
14
|
-
allowed-list is not made complete here, because once nothing moves an undeclared
|
|
15
|
-
entry costs one log line rather than a broken feature.
|
|
16
|
-
|
|
17
|
-
## Units
|
|
18
|
-
|
|
19
|
-
### 1. `platform/services/claude-session-manager/src/account-dir-schema-reconcile.ts`
|
|
20
|
-
|
|
21
|
-
The move block, `quarantineDest`, and the `mkdirSync` / `renameSync` /
|
|
22
|
-
`appendFileSync` imports are deleted. `ReconcileCounts` loses `quarantined` and
|
|
23
|
-
stays the pure formatter's input, so `formatReconcileLine` still takes exactly
|
|
24
|
-
what it prints:
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
[fs-reconcile] stray-top-level=<n> over-deep=<n> bad-name=<n>
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
`reconcileAccount` returns a new `ReconcileResult`: the counts plus
|
|
31
|
-
`strayNames: string[]`. `PROTECTED_TOP_LEVEL` and `QUARANTINE_DIR` both stay.
|
|
32
|
-
They no longer protect anything from being moved, because nothing moves; they
|
|
33
|
-
keep known platform entries and existing quarantine directories out of the stray
|
|
34
|
-
count so the standing line stays readable and a clean account still reports zero.
|
|
35
|
-
|
|
36
|
-
### 2. `runReconcile` names the strays
|
|
37
|
-
|
|
38
|
-
One line per account that has any, emitted before the summary:
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
[fs-reconcile] op=strays account=<id> names=<a,b,c>
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
No cap on the name list. A count alone cannot distinguish six pieces of debris
|
|
45
|
-
from a live plugin config, and a truncated list would hide the entry an operator
|
|
46
|
-
is hunting for.
|
|
47
|
-
|
|
48
|
-
### 3. `platform/services/claude-session-manager/src/index.ts`
|
|
49
|
-
|
|
50
|
-
The call-site comment states that the pass quarantines. It is rewritten to state
|
|
51
|
-
that it reports and never moves, and why: a closed allowed-list that lags what
|
|
52
|
-
the platform writes turns a metric into data loss.
|
|
53
|
-
|
|
54
|
-
### 4. `platform/scripts/quarantine-restore.mjs`
|
|
55
|
-
|
|
56
|
-
One-shot, run per install with `--accounts-root <dir>`, plus `--dry-run`. For
|
|
57
|
-
each `<account>/.quarantine/manifest.jsonl` it walks records newest-first and for
|
|
58
|
-
each `originalPath`:
|
|
59
|
-
|
|
60
|
-
| On disk | Action | Log |
|
|
61
|
-
|---|---|---|
|
|
62
|
-
| Target free at the account root | move it back | `op=restore account=<id> name=<n> result=moved` |
|
|
63
|
-
| Target occupied by a copy the platform rewrote | leave the quarantined copy alone | `result=skipped-live-copy` |
|
|
64
|
-
| Target occupied by a newer copy this same run restored | leave the quarantined copy alone | `result=skipped-superseded` |
|
|
65
|
-
| Source absent (already restored by hand) | nothing | `result=absent` |
|
|
66
|
-
|
|
67
|
-
Three further outcomes were added during code review, each because its absence
|
|
68
|
-
would have been a silent skip of exactly the kind this task exists to end:
|
|
69
|
-
|
|
70
|
-
| Condition | Log | Effect |
|
|
71
|
-
|---|---|---|
|
|
72
|
-
| A manifest line that will not parse, or lacks a string `originalPath` / `quarantinePath` | `line=<n> result=unparsable` | counted, run continues |
|
|
73
|
-
| A record whose source escapes `<account>/.quarantine/` or whose target escapes the account | `line=<n> result=refused-outside-account` | counted, exit code 1 |
|
|
74
|
-
| The rename fails, or the manifest will not open | `result=failed` / `result=manifest-unreadable` | counted, exit code 1 |
|
|
75
|
-
|
|
76
|
-
The containment check exists because `.quarantine` is in the write guard's
|
|
77
|
-
allowed-top-level set, so an account-scoped agent can append to its own manifest,
|
|
78
|
-
while the script runs as the platform user across every account. Without it a
|
|
79
|
-
crafted record moves one account's live data into another. Every record the
|
|
80
|
-
reconcile ever wrote was a bare basename plus a path under `.quarantine/`, so the
|
|
81
|
-
check refuses nothing genuine.
|
|
82
|
-
|
|
83
|
-
Newest-first is what makes the occupied case correct for a name quarantined more
|
|
84
|
-
than once: the most recent copy is considered first, and every older copy of that
|
|
85
|
-
name then finds the target occupied. The live copy wins because the platform
|
|
86
|
-
rewrote it after the move, which the four `wa-channel-bindings.json` records on
|
|
87
|
-
account `5e99bba5` prove happened.
|
|
88
|
-
|
|
89
|
-
A restored record is appended to `<account>/.quarantine/restored.jsonl` and the
|
|
90
|
-
manifest is left intact, so the history of what moved and what came back
|
|
91
|
-
survives. The script never deletes.
|
|
92
|
-
|
|
93
|
-
### 5. `platform/templates/account-schema/SCHEMA.md`
|
|
94
|
-
|
|
95
|
-
The paragraph telling the agent that `.quarantine/` holds entries the reconcile
|
|
96
|
-
moved out of the top level describes behaviour this task deletes. It is reworded
|
|
97
|
-
to say the directory is a historical store from the pre-1899 sweep that nothing
|
|
98
|
-
adds to any more. Re-filing its contents through the `data-manager` specialist
|
|
99
|
-
still stands.
|
|
100
|
-
|
|
101
|
-
## Tests
|
|
102
|
-
|
|
103
|
-
New and rewritten cases go in the existing
|
|
104
|
-
`src/__tests__/account-dir-schema-reconcile.test.ts`; several of its 18 cases
|
|
105
|
-
assert quarantine moves and are rewritten to assert no move. The restore script
|
|
106
|
-
gets `platform/scripts/__tests__/quarantine-restore.test.sh`, matching how every
|
|
107
|
-
other `platform/scripts/` test is written.
|
|
108
|
-
|
|
109
|
-
## Out of scope
|
|
110
|
-
|
|
111
|
-
- Completing the `account-owned-dirs` declarations, and extending declarations
|
|
112
|
-
from dirs to files so `data-portal.json` has a declaration path. Task 1902.
|
|
113
|
-
- The portal index push. It is correct and was starved of its config.
|
|
114
|
-
- The standing portal audit going silent when a config disappears. Task 1901.
|
|
115
|
-
- `over-deep` and `bad-name`. Both already count without moving.
|
|
116
|
-
- Installer payload twins. The payload has no committed copy of any file touched
|
|
117
|
-
here; `packages/create-maxy-code/scripts/bundle.js` copies `platform/` at
|
|
118
|
-
bundle time.
|
|
119
|
-
- Publishing, installing on the laptop's three reconcile-carrying installs, and
|
|
120
|
-
running the restore. All await the operator's publish command, because the
|
|
121
|
-
currently-installed sweep would re-take anything restored before it.
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# Task 1910 — Two-way portal file exchange (design)
|
|
2
|
-
|
|
3
|
-
Design for [`.tasks/1910-two-way-portal-file-exchange.md`](../../../../.tasks/1910-two-way-portal-file-exchange.md). Operator decision 2026-07-22: the data portal becomes a two-way folder exchange, reversing the one-way scope decisions of Tasks 1831, 1905 and 1906.
|
|
4
|
-
|
|
5
|
-
## The problem in one line
|
|
6
|
-
|
|
7
|
-
Today the portal is two stores that never meet: a flat, destination-less R2 upload area the client writes to, and a read-only device folder index the client browses. Neither can reach the other.
|
|
8
|
-
|
|
9
|
-
## Manifest state machine
|
|
10
|
-
|
|
11
|
-
Three new columns on `manifest`: `targetPath` (the exposed folder chosen at upload, empty means untargeted), `devicePath` (where the bytes actually landed, written only after a verified device write, so its presence *is* the proof the move happened), and `deleted` (the client asked to withdraw a file that has already moved).
|
|
12
|
-
|
|
13
|
-
| State | Columns | Bytes live in | Uploads panel |
|
|
14
|
-
|---|---|---|---|
|
|
15
|
-
| In transit | `ingested=0`, `devicePath=''` | R2 | shown, downloads from R2 |
|
|
16
|
-
| Moved | `ingested=1`, `devicePath` set, R2 object gone | device | shown only when the file is not reachable in the folder tree |
|
|
17
|
-
| Withdrawn | `deleted=1` | device, pending removal | hidden |
|
|
18
|
-
|
|
19
|
-
**One listing rule:** the uploads panel shows a row when the bytes are still in R2, or when the moved file is not reachable in the folder tree. A targeted file therefore leaves the panel and appears in its folder; an untargeted file stays in the panel and downloads through the device. No file is ever listed twice, and no file becomes unreachable.
|
|
20
|
-
|
|
21
|
-
`devicePath` rather than a boolean is deliberate: a boolean would say a move happened without saying where, and the audit's `unpublishedRouted` check needs the path to compare against the published index.
|
|
22
|
-
|
|
23
|
-
**Withdrawal ends in row deletion, not a tombstone.** Once the device file is confirmed absent the row is removed. A tombstone would grow the manifest without bound and force every listing read to filter it, and the R2 object is already gone, so the row describes nothing.
|
|
24
|
-
|
|
25
|
-
### Serving a moved untargeted file
|
|
26
|
-
|
|
27
|
-
A moved untargeted file sits at `uploads/<ownerId>/<filename>`, and `uploads` is deliberately never exposed (`schema-exposed-dirs.mjs:9-14`). `resolveFetchTarget` refuses any path whose top segment is outside the exposed set (`portal-fetch.ts:182`), and the signed message carries `accountId`, `relPath` and an expiry but no owner (`portal-fetch.ts:56`), so as it stands the device would both refuse the path and be unable to tell whose uploads folder was asked for.
|
|
28
|
-
|
|
29
|
-
Two link types with two distinct signed messages resolve this:
|
|
30
|
-
|
|
31
|
-
| Link type | Signed message | Device admission rule |
|
|
32
|
-
|---|---|---|
|
|
33
|
-
| Exposed-folder file | `accountId \n relPath \n exp` (unchanged) | top segment ∈ exposed set |
|
|
34
|
-
| Own-upload file | `accountId \n uploads \n ownerId \n relPath \n exp` | path is exactly `uploads/<signed ownerId>/…` |
|
|
35
|
-
|
|
36
|
-
The owner is inside the HMAC, so a portal cannot mint a link into another person's upload folder, and the device still decides admission itself rather than trusting the path. `uploads/` gains no exposure to the folder tree; only a link naming its own owner reaches it.
|
|
37
|
-
|
|
38
|
-
Two message shapes rather than one extended shape, because the exposed-folder message is unchanged and an already-deployed portal never mints the second type. Device and portal redeploy at different times (installer publish versus `site-deploy`), so a single extended message would break every existing download in the window between them.
|
|
39
|
-
|
|
40
|
-
## Upload key
|
|
41
|
-
|
|
42
|
-
`ownerId/targetPath/filename`, collapsing to `ownerId/filename` when untargeted.
|
|
43
|
-
|
|
44
|
-
The target must be in the key. With a flat `ownerId/filename` key, the same filename dropped into two different folders collides on `objectKey UNIQUE`: the upsert would overwrite the first object, reset `ingested`, and the first file would never land. `authorizeKey` (`_lib/authorize.ts:22-32`) already admits deeper keys, rejecting only `..` and a leading separator, so the owner-isolation gate is unchanged by this.
|
|
45
|
-
|
|
46
|
-
Same owner, same folder, same filename still upserts, which is success criterion 7's replace case.
|
|
47
|
-
|
|
48
|
-
## Target validation, and where the boundary actually is
|
|
49
|
-
|
|
50
|
-
A target is legal when it passes the person's grant (`grantAllows`, `_lib/session.ts:87`) **and** carries a folder row (`isDir=1`) in the published `directory` index for that account.
|
|
51
|
-
|
|
52
|
-
This means you can only drop where you can see, which introduces no new permission concept: the set of legal upload targets is exactly the set of folders already rendered to that person. An empty target is always legal.
|
|
53
|
-
|
|
54
|
-
**The portal check is convenience; the device check is the boundary.** The pull re-derives the exposed set itself through `resolveExposedDirs`, refusing anything outside it, the same two-ended pattern `portal-fetch.ts` uses for signed downloads. A compromised portal cannot route a file into a folder the device does not expose. The two ends are pinned to agree by a parity test over one shared fixture set, mirroring the existing HMAC parity test.
|
|
55
|
-
|
|
56
|
-
Refusal fails closed: the file stays pending, the refusal is logged with the exact failing segment, and it is never silently rerouted to the default. A silent reroute would put a client's file somewhere they did not choose and could not find.
|
|
57
|
-
|
|
58
|
-
## Large uploads
|
|
59
|
-
|
|
60
|
-
The client slices anything above one part into 25 MiB pieces against an R2 multipart session; files at or below one part keep today's single request. The cap is 1 GiB, checked at session creation against the declared size and again at completion against the summed parts.
|
|
61
|
-
|
|
62
|
-
Slicing is on the client because the ceiling is the isolate: an upload is read wholly into Function memory (`upload.ts:137`), which is why 25 MiB was chosen in the first place (`upload.ts:8-15`). Parts preserve that bound exactly while removing the total-size limit. Cloudflare's proxied-body cap is not the constraint here; the portal is on Pages, not behind the tunnel that bounds the admin `/data` page (Task 1174).
|
|
63
|
-
|
|
64
|
-
## Atomic index replace
|
|
65
|
-
|
|
66
|
-
`directory` gains a `generation` column and a per-account pointer table. The push writes rows at the next generation, flips the pointer in one statement, then deletes superseded rows.
|
|
67
|
-
|
|
68
|
-
The flip is the atomic point, so a listing racing a push sees the old tree or the new one, never a half-built one. Cleanup afterwards need not be atomic because nothing reads a superseded generation. This supersedes backlog Task 1842, which recorded the delete-then-insert window as an open defect; at 60 second cadence that window would otherwise be hit sixty times more often than at hourly.
|
|
69
|
-
|
|
70
|
-
## Cadence
|
|
71
|
-
|
|
72
|
-
The index push becomes a 60 second loop in the admin server's loop registry, beside the pull that already runs there. The scheduling heartbeat's hourly gate and its `auditPortalIndexPush` are retired.
|
|
73
|
-
|
|
74
|
-
That heartbeat spawn is the path whose own header records it has "never been observed running on a device" (`portal-index-push.mjs:11-13`). Moving the push into the loop registry replaces an unobserved spawn with the mechanism the pull demonstrably uses, and removes the hourly gate that made the client's tree stale by up to two hours. Importing across the tree boundary is precedented: `portal-fetch.ts:22` already imports `schema-exposed-dirs.mjs` from the plugins tree. The script stays runnable standalone.
|
|
75
|
-
|
|
76
|
-
## Audit
|
|
77
|
-
|
|
78
|
-
The five-minute `[data-portal-audit]` gains four fields, each answering a failure that emits no event of its own:
|
|
79
|
-
|
|
80
|
-
- `lastPushAgeSec` — the push stopped running. Healthy under 180.
|
|
81
|
-
- `unmovedObjects` — an R2 object survived its own pull, so the move leaked.
|
|
82
|
-
- `deletePendingRows` — a withdrawal never reached the device.
|
|
83
|
-
- `unpublishedRouted` — a routed file landed somewhere the index never published, so the client cannot see what they uploaded.
|
|
84
|
-
|
|
85
|
-
Enumeration becomes the union of accounts presenting a config and accounts with a prior push record, reporting `config-absent` rather than falling silent. That is Task 1901's fix, folded in here because this task retires the audit 1901 would have patched.
|
|
86
|
-
|
|
87
|
-
## Download routing
|
|
88
|
-
|
|
89
|
-
`/api/download?key=` keeps serving R2 bytes while the row is in transit. Once the row has moved the object is gone, so the same request mints an own-upload signed link and 302s to the device, which is what keeps criterion 5 true without the client learning that the bytes changed home.
|
|
90
|
-
|
|
91
|
-
## Client
|
|
92
|
-
|
|
93
|
-
Folder rows and the open listing become drop targets with a drop state. The picker takes multiple files and a multi-file drop uploads each in turn. Upload rows gain `DownloadURL` drag-out, matching what device rows already have (`portal.js:226-233`). Uploads show per-file progress and distinct messages per failure class, replacing today's single "Upload failed." for every status. The page re-reads every 60 seconds while its tab is visible.
|
|
94
|
-
|
|
95
|
-
Polling was rejected in Tasks 1885 and 1900 on the explicit reasoning that "a poll against an hourly tree is waste". The premise dies with the hourly push: against a 60 second tree the poll is the thing that makes a device-side change visible without the client clicking anything, which is success criterion 4.
|
|
96
|
-
|
|
97
|
-
## Out of scope
|
|
98
|
-
|
|
99
|
-
Client deletion or rename of device-authored files the client did not upload; the trust boundary is that a client may withdraw their own file, not destroy a business record. Folder upload and zip download, both directions being files only. Portal-side folder creation, rename and move. The third D1 client (Task 1843). The admin `/data` page and Task 1174. Graph ingestion of pulled files, unchanged from Task 1906.
|
|
100
|
-
|
|
101
|
-
## Dependency
|
|
102
|
-
|
|
103
|
-
Live acceptance is blocked until Task 1909 restores the quarantined `data-portal.json` on both sitedesk accounts; the portal has enumerated zero accounts since 2026-07-21T19:16:05Z. Code and unit verification do not depend on it.
|
package/payload/platform/docs/superpowers/specs/2026-07-23-task-1930-adherence-enforcement-design.md
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# Task 1930 — Schema-and-preference adherence enforcement (design)
|
|
2
|
-
|
|
3
|
-
Date: 2026-07-23
|
|
4
|
-
Task: `.tasks/backlog/1930-enforce-schema-and-preference-adherence-scratch-deliverables-and-unconsulted-sends.md`
|
|
5
|
-
|
|
6
|
-
## Problem
|
|
7
|
-
|
|
8
|
-
Two adherence failures share one root: a rule the agent follows by judgement, with no gate forcing it.
|
|
9
|
-
|
|
10
|
-
- **Preferences.** Layer-2 `Preference` nodes (`platform/plugins/memory/mcp/src/tools/profile-read.ts`) are consulted by per-turn judgement. A customer signature preference was missed on a delivered document because nothing forced a `profile-read` before the send.
|
|
11
|
-
- **Placement.** `output/` is tool-owned scratch rebuilt each write (`platform/templates/account-schema/SCHEMA.md:29-42`); finished deliverables strand there and graph refs point at those scratch paths. The `data-manager` reconcile audit skips tool-owned dirs, so both are invisible to it.
|
|
12
|
-
|
|
13
|
-
## Decisions (operator-confirmed)
|
|
14
|
-
|
|
15
|
-
1. The preference gate fires only on **customer-facing document deliverables**, not every send. Casual channel replies and plain-text emails pass.
|
|
16
|
-
2. This sprint ships the gate, the wrapper reminder, and the spec changes. The **periodic scheduling** of the reconcile audit is deferred to Task 1931.
|
|
17
|
-
3. The gate hard-blocks (exit 2), matching the user's instruction.
|
|
18
|
-
|
|
19
|
-
## Architecture
|
|
20
|
-
|
|
21
|
-
Five units, each single-purpose and independently testable.
|
|
22
|
-
|
|
23
|
-
### 1. `platform/plugins/admin/hooks/preference-consult-gate.sh` (new, PreToolUse)
|
|
24
|
-
|
|
25
|
-
Contract mirrors `quote-render-gate.sh`: read the JSON envelope on stdin, exit 0 allow / exit 2 block, fail-open on any uninspectable call (tty, empty stdin, missing `python3`, missing/unreadable transcript).
|
|
26
|
-
|
|
27
|
-
**Fires only on a customer-facing document deliverable**, decided from `tool_name` + `tool_input`:
|
|
28
|
-
- `mcp__plugin_browser_browser__browser-pdf-save` whose output path is under `memory/users/<phone>/documents/`.
|
|
29
|
-
- `SendUserFile` whose file path is under `memory/users/<phone>/documents/` or carries a document extension (`.pdf`, `.html`, `.docx`).
|
|
30
|
-
- `mcp__plugin_email_email__email-send` / `email-reply` / `email-draft-send` with a non-empty `attachments`.
|
|
31
|
-
- `mcp__plugin_outlook_outlook__outlook-mail-send` / `outlook-mail-reply` / `outlook-draft-send` with a non-empty `attachments`.
|
|
32
|
-
|
|
33
|
-
Any tool or shape not matching the above exits 0 (not a document deliverable).
|
|
34
|
-
|
|
35
|
-
**Consultation check.** Read the transcript at `transcript_path`. Scan the messages from the last `role:user` entry to the end for an assistant `tool_use` whose name ends in `profile-read`. Found → exit 0. Absent → exit 2.
|
|
36
|
-
|
|
37
|
-
**Block message** (operator-visible, no task numbers): names that a customer document is about to go out without the account's saved preferences being checked this turn, and directs the agent to run `profile-read` for this account and retry the send. A false block (preferences read in an earlier turn) is a one-step recovery, not a dead end.
|
|
38
|
-
|
|
39
|
-
**Log line** (stderr): `[preference-gate] op=bypass tool=<name> detail=no profile-read this turn` on a block; `op=allow tool=<name> consulted=true` on the allow path for a document deliverable. `browser-pdf-save` already carries `quote-render-gate` on the same matcher; both hooks run and are independent (one on receipt, one on consultation).
|
|
40
|
-
|
|
41
|
-
### 2. `platform/plugins/admin/hooks/preference-consult-directive.sh` (new, UserPromptSubmit)
|
|
42
|
-
|
|
43
|
-
Mirrors `datetime-inject.sh`: drain stdin first, fail-open exit 0 on every path, emit `hookSpecificOutput.additionalContext` via python3. The block is standing and prompt-independent. Content: the two-layer preference architecture (layer 1 fixed/injected, layer 2 the account's `Preference` records read via `profile-read`), and two directives: consult the relevant layer-2 preferences before any customer-facing document deliverable, and promote finished deliverables out of `output/` into `documents/` or `projects/`. Breadcrumb line: `[pref-wrapper] op=inject`.
|
|
44
|
-
|
|
45
|
-
### 3. `platform/templates/account-schema/SCHEMA.md`
|
|
46
|
-
|
|
47
|
-
In the tool-owned section (`:29-42`), add one sentence: finished deliverables must be promoted out of `output/` into `documents/` or `projects/`; `output/` is scratch of last resort, rebuilt on the next write, and a graph reference must never point into it.
|
|
48
|
-
|
|
49
|
-
### 4. `platform/templates/specialists/agents/data-manager.md`
|
|
50
|
-
|
|
51
|
-
Extend "The reconcile audit brief": in addition to (a) files unreachable from any node and (b) refs that resolve to no real path, count (c) graph refs resolving into a scratch dir (`output/`, `generated/`, `extracted/`, `url-get/`) and (d) deliverables referenced by a node but resolving only under scratch. Output contract becomes `unreachable=N broken-refs=M scratch-refs=P stranded=Q`. The audit stays read-only; it moves nothing.
|
|
52
|
-
|
|
53
|
-
### 5. Registration + docs
|
|
54
|
-
|
|
55
|
-
- `platform/scripts/lib/provision-account-dir.sh`: add PreToolUse matcher entries routing the document-send tools to `preference-consult-gate.sh`, and add `preference-consult-directive.sh` to the UserPromptSubmit hooks list.
|
|
56
|
-
- `platform/plugins/admin/PLUGIN.md`: hook entries for both new hooks (matcher, block condition, block message, log line, fail-open).
|
|
57
|
-
- `.docs/` and `platform/plugins/docs/references/`: the sprint-gate documentation targets.
|
|
58
|
-
|
|
59
|
-
## Registration matcher note
|
|
60
|
-
|
|
61
|
-
Settings matchers are per-tool. The gate registers against the browser, email, outlook, and SendUserFile matchers; the script itself does the document-deliverable discrimination, so over-matching a non-document send is safe (it exits 0). WhatsApp document delivery routes through the file path the same discrimination covers when a document goes out as a `SendUserFile`; a plain `whatsapp-reply` text is not a document deliverable and is out of scope by decision 1.
|
|
62
|
-
|
|
63
|
-
## Testing (ephemeral)
|
|
64
|
-
|
|
65
|
-
- **Gate.** For each document-send tool: envelope whose transcript has a `profile-read` since the last user turn → exit 0; envelope with none → exit 2 with the bypass line. A non-document send (email with no attachments, plain browser-pdf-save outside the customer documents scope) → exit 0. Empty/tty stdin → exit 0.
|
|
66
|
-
- **Wrapper.** Emits `hookSpecificOutput.additionalContext` containing both directives; fails open (exit 0) when python3 is absent.
|
|
67
|
-
- **Regression.** The 22 `fs-schema-guard` tests and the `quote-render-gate` tests stay green.
|
|
68
|
-
|
|
69
|
-
## Observability
|
|
70
|
-
|
|
71
|
-
- **Gate (per send):** `[preference-gate] op=allow|bypass tool=<name>`. Success = `op=allow consulted=true`. Failure = any `op=bypass`.
|
|
72
|
-
- **Wrapper:** `[pref-wrapper] op=inject` per turn; absence across turns means the directive is not reaching the agent.
|
|
73
|
-
- **Reconcile:** the extended brief emits `unreachable=N broken-refs=M scratch-refs=P stranded=Q`; a rising `scratch-refs`/`stranded` is the placement-drift signal. The standing periodic run that would make this a no-event backstop is Task 1931.
|
|
74
|
-
|
|
75
|
-
## Out of scope
|
|
76
|
-
|
|
77
|
-
- Hard write-block on `output/` writes (render tools legitimately use it as scratch).
|
|
78
|
-
- Auto-promotion or auto-move of stranded files (stays the `data-manager` paired-move job).
|
|
79
|
-
- The periodic scheduler for the reconcile audit (Task 1931).
|
|
80
|
-
- Layer-1 fixed-block content, the voice file, and which preferences are stored.
|
|
81
|
-
- Quote-specific conformance (1922/1928/1929).
|