@rubytech/create-sitedesk-code 0.1.524 → 0.1.525

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/superpowers-sprint/SKILL.md +26 -0
  3. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  4. package/payload/platform/plugins/memory/PLUGIN.md +10 -0
  5. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +5 -1
  6. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
  7. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts +2 -0
  8. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts.map +1 -0
  9. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js +141 -0
  10. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js.map +1 -0
  11. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts +1 -1
  12. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts.map +1 -1
  13. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +6 -0
  14. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
  15. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts +45 -0
  16. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts.map +1 -0
  17. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js +122 -0
  18. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js +26 -0
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js.map +1 -1
  21. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts +2 -0
  22. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts.map +1 -0
  23. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js +201 -0
  24. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js.map +1 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +89 -33
  27. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
  29. package/payload/platform/plugins/scheduling/PLUGIN.md +6 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts +2 -0
  31. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts.map +1 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js +148 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js.map +1 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +83 -6
  35. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
  36. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +90 -5
  37. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
  38. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js +111 -2
  39. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js.map +1 -1
  40. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +38 -0
  41. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  42. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +116 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  44. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +13 -1
  45. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  46. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +81 -8
  47. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js +39 -0
  49. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js.map +1 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +9 -1
  52. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  53. package/payload/platform/scripts/__tests__/check-canonical-tool-names.test.sh +162 -0
  54. package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +53 -0
  55. package/payload/platform/scripts/__tests__/task-id-citation.test.sh +72 -0
  56. package/payload/platform/scripts/check-canonical-tool-names.mjs +36 -4
  57. package/payload/platform/scripts/check-no-task-id-leaks.mjs +80 -62
  58. package/payload/platform/scripts/lib/canonical-tool-names.mjs +130 -26
  59. package/payload/platform/scripts/lib/task-id-citation.mjs +67 -0
  60. package/payload/platform/.docs/search-surface-contract.md +0 -58
  61. package/payload/platform/docs/superpowers/plans/2026-06-02-task-610-follower-202-retry.md +0 -372
  62. package/payload/platform/docs/superpowers/plans/2026-06-04-public-agent-knowledge-delivery.md +0 -230
  63. package/payload/platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +0 -544
  64. package/payload/platform/docs/superpowers/plans/2026-07-11-task-1557-operator-on-behalf-preference-attribution.md +0 -542
  65. package/payload/platform/docs/superpowers/plans/2026-07-13-account-schema-ontology-projection.md +0 -547
  66. package/payload/platform/docs/superpowers/plans/2026-07-14-graph-top-level-labels-ontology-single-source.md +0 -458
  67. package/payload/platform/docs/superpowers/plans/2026-07-17-task-1736-retire-mcp-stderr-tee.md +0 -397
  68. package/payload/platform/docs/superpowers/plans/2026-07-18-graph-native-ledger.md +0 -807
  69. package/payload/platform/docs/superpowers/plans/2026-07-18-ledger-write-path-fix.md +0 -109
  70. package/payload/platform/docs/superpowers/plans/2026-07-20-storage-pages-create.md +0 -82
  71. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +0 -673
  72. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +0 -1244
  73. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +0 -462
  74. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +0 -321
  75. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1829-platform-ui-typecheck.md +0 -830
  76. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +0 -2177
  77. package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +0 -124
  78. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +0 -107
  79. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +0 -205
  80. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +0 -748
  81. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +0 -1716
  82. package/payload/platform/docs/superpowers/plans/2026-07-23-task-1930-adherence-enforcement.md +0 -422
  83. package/payload/platform/docs/superpowers/plans/2026-07-24-task-1942-subaccount-switcher-brand-head.md +0 -327
  84. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1974-uploads-intake-inbox.md +0 -235
  85. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1976-intra-folder-hygiene.md +0 -215
  86. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +0 -555
  87. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1926-portal-push-concurrency-guard.md +0 -482
  88. package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +0 -394
  89. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +0 -624
  90. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md +0 -303
  91. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2052-account-machinery-write-fence.md +0 -250
  92. package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +0 -65
  93. package/payload/platform/docs/superpowers/specs/2026-06-02-task-610-follower-202-retry-design.md +0 -116
  94. package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +0 -170
  95. package/payload/platform/docs/superpowers/specs/2026-07-11-task-1557-operator-on-behalf-preference-attribution-design.md +0 -90
  96. package/payload/platform/docs/superpowers/specs/2026-07-13-account-schema-ontology-projection-design.md +0 -178
  97. package/payload/platform/docs/superpowers/specs/2026-07-14-graph-top-level-labels-ontology-single-source-design.md +0 -119
  98. package/payload/platform/docs/superpowers/specs/2026-07-17-task-1736-retire-mcp-stderr-tee-design.md +0 -128
  99. package/payload/platform/docs/superpowers/specs/2026-07-18-graph-native-ledger-design.md +0 -241
  100. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +0 -177
  101. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +0 -201
  102. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +0 -217
  103. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +0 -80
  104. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +0 -97
  105. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md +0 -163
  106. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +0 -163
  107. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +0 -121
  108. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +0 -103
  109. package/payload/platform/docs/superpowers/specs/2026-07-23-task-1930-adherence-enforcement-design.md +0 -81
  110. package/payload/platform/docs/superpowers/specs/2026-07-24-task-1942-subaccount-switcher-brand-head-design.md +0 -35
  111. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +0 -232
  112. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1926-portal-push-concurrency-guard-design.md +0 -200
  113. package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +0 -169
  114. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +0 -139
  115. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +0 -127
  116. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2052-account-machinery-write-fence-design.md +0 -150
  117. package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +0 -155
@@ -1,542 +0,0 @@
1
- # Task 1557 — Operator-on-behalf Preference Attribution Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** A preference an operator states while working inside a client's sub-account attaches to that client's account owner, not to the operator.
6
-
7
- **Architecture:** The decision lives in one place — `resolveProfileIdentity` in the memory MCP's `index.ts`. It gains an operator-on-behalf case: when the caller's `AdminUser` home account differs from the account the session is scoped to (a house operator inside a client's sub-account), the profile op attributes to the account's seeded owner. The decision is extracted into a pure classifier (`classifyProfileIdentity`) and a small DB helper (`resolveAdminHomeAccount`), both unit-tested; `index.ts` orchestrates the two and maps the result to the existing MCP reject envelope and bootstrap flags.
8
-
9
- **Tech Stack:** TypeScript, Node ESM, Neo4j driver, Vitest. All work is inside `platform/plugins/memory/mcp/`.
10
-
11
- ## Global Constraints
12
-
13
- - **Working directory for all builds/tests:** `platform/plugins/memory/mcp/`. Build with `npm run build`, test with `npm test` (vitest). The sibling `lib/*` packages must already be built (`dist/` present) — they are in this worktree.
14
- - **Precision, no speculation:** implement exactly the four attribution cases in the spec (caller-is-admin-here, foreign-operator-with-owner, foreign-operator-no-owner, no-caller). No extra fallback paths.
15
- - **Shipped skill markdown carries zero em-dashes** (`conversational-memory/SKILL.md`). Use commas or full stops.
16
- - **The cross-account `[xacct] op=identity` log line stays byte-identical** to its current form; the operator-on-behalf case emits a distinct new line. PLUGIN.md documents both.
17
- - **Committed tests:** the memory MCP has a committed vitest suite (`src/tools/__tests__/`). The new behaviour belongs in it, so the test files in this plan are committed, not discarded.
18
- - **Session trailer on every commit:** append `Session: e53535e2-8c90-4e75-9936-6132ef8a86a0` and the `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>` line.
19
-
20
- ---
21
-
22
- ### Task 1: Pure identity classifier
23
-
24
- **Files:**
25
- - Create: `platform/plugins/memory/mcp/src/lib/profile-identity.ts`
26
- - Test: `platform/plugins/memory/mcp/src/tools/__tests__/profile-identity.test.ts`
27
-
28
- **Interfaces:**
29
- - Produces: `classifyProfileIdentity(input: { crossAccount: boolean; resolvedAccountId: string; callerUserId: string | undefined; callerHomeAccountId: string | null; ownerUserId: string | null }): ProfileAttribution` where `type ProfileAttribution = { kind: "caller"; userId: string } | { kind: "owner"; userId: string } | { kind: "reject"; reason: "no-caller" | "no-owner" }`.
30
-
31
- - [ ] **Step 1: Write the failing test**
32
-
33
- Create `platform/plugins/memory/mcp/src/tools/__tests__/profile-identity.test.ts`:
34
-
35
- ```ts
36
- import { describe, it, expect } from "vitest";
37
- import { classifyProfileIdentity } from "../../lib/profile-identity.js";
38
-
39
- const SESSION_ACCT = "acct-session";
40
- const HOME_ACCT = "acct-home";
41
- const CALLER = "user-caller";
42
- const OWNER = "user-owner";
43
-
44
- describe("classifyProfileIdentity (Task 1557)", () => {
45
- it("cross-account with an owner attributes to the owner", () => {
46
- expect(
47
- classifyProfileIdentity({
48
- crossAccount: true,
49
- resolvedAccountId: SESSION_ACCT,
50
- callerUserId: CALLER,
51
- callerHomeAccountId: null,
52
- ownerUserId: OWNER,
53
- }),
54
- ).toEqual({ kind: "owner", userId: OWNER });
55
- });
56
-
57
- it("cross-account with no owner rejects no-owner", () => {
58
- expect(
59
- classifyProfileIdentity({
60
- crossAccount: true,
61
- resolvedAccountId: SESSION_ACCT,
62
- callerUserId: CALLER,
63
- callerHomeAccountId: null,
64
- ownerUserId: null,
65
- }),
66
- ).toEqual({ kind: "reject", reason: "no-owner" });
67
- });
68
-
69
- it("own-account with no caller userId rejects no-caller", () => {
70
- expect(
71
- classifyProfileIdentity({
72
- crossAccount: false,
73
- resolvedAccountId: SESSION_ACCT,
74
- callerUserId: undefined,
75
- callerHomeAccountId: null,
76
- ownerUserId: OWNER,
77
- }),
78
- ).toEqual({ kind: "reject", reason: "no-caller" });
79
- });
80
-
81
- it("own-account caller who is an admin of this account attributes to the caller", () => {
82
- expect(
83
- classifyProfileIdentity({
84
- crossAccount: false,
85
- resolvedAccountId: SESSION_ACCT,
86
- callerUserId: CALLER,
87
- callerHomeAccountId: SESSION_ACCT,
88
- ownerUserId: OWNER,
89
- }),
90
- ).toEqual({ kind: "caller", userId: CALLER });
91
- });
92
-
93
- it("own-account foreign operator (home differs) with an owner attributes to the owner", () => {
94
- expect(
95
- classifyProfileIdentity({
96
- crossAccount: false,
97
- resolvedAccountId: SESSION_ACCT,
98
- callerUserId: CALLER,
99
- callerHomeAccountId: HOME_ACCT,
100
- ownerUserId: OWNER,
101
- }),
102
- ).toEqual({ kind: "owner", userId: OWNER });
103
- });
104
-
105
- it("own-account foreign operator with no owner rejects no-owner", () => {
106
- expect(
107
- classifyProfileIdentity({
108
- crossAccount: false,
109
- resolvedAccountId: SESSION_ACCT,
110
- callerUserId: CALLER,
111
- callerHomeAccountId: HOME_ACCT,
112
- ownerUserId: null,
113
- }),
114
- ).toEqual({ kind: "reject", reason: "no-owner" });
115
- });
116
-
117
- it("own-account caller with no AdminUser node (home null) is treated as foreign", () => {
118
- expect(
119
- classifyProfileIdentity({
120
- crossAccount: false,
121
- resolvedAccountId: SESSION_ACCT,
122
- callerUserId: CALLER,
123
- callerHomeAccountId: null,
124
- ownerUserId: OWNER,
125
- }),
126
- ).toEqual({ kind: "owner", userId: OWNER });
127
- });
128
- });
129
- ```
130
-
131
- - [ ] **Step 2: Run test to verify it fails**
132
-
133
- Run: `cd platform/plugins/memory/mcp && npx vitest run src/tools/__tests__/profile-identity.test.ts`
134
- Expected: FAIL — cannot resolve module `../../lib/profile-identity.js`.
135
-
136
- - [ ] **Step 3: Write minimal implementation**
137
-
138
- Create `platform/plugins/memory/mcp/src/lib/profile-identity.ts`:
139
-
140
- ```ts
141
- /**
142
- * Task 1557 — decide which identity a profile op (profile-read/update/delete)
143
- * attributes to, given the already-resolved facts. Pure: no DB, no env.
144
- *
145
- * - Cross-account (a house session that set targetAccountId): attribute to the
146
- * target account's owner; reject if the target has no owner on record.
147
- * - Own-account, caller is a registered admin of THIS account (their AdminUser
148
- * home account equals the session account): attribute to the caller. This is
149
- * the owner-is-speaker path and the genuine second-admin path.
150
- * - Own-account, caller is a FOREIGN operator (home account differs, or the
151
- * caller has no AdminUser node in this graph — a house operator working inside
152
- * a client's sub-account): attribute to the session account's owner; reject
153
- * loudly if that account has no owner on record.
154
- */
155
- export type ProfileAttribution =
156
- | { kind: "caller"; userId: string }
157
- | { kind: "owner"; userId: string }
158
- | { kind: "reject"; reason: "no-caller" | "no-owner" };
159
-
160
- export function classifyProfileIdentity(input: {
161
- crossAccount: boolean;
162
- resolvedAccountId: string;
163
- callerUserId: string | undefined;
164
- callerHomeAccountId: string | null;
165
- ownerUserId: string | null;
166
- }): ProfileAttribution {
167
- const { crossAccount, resolvedAccountId, callerUserId, callerHomeAccountId, ownerUserId } = input;
168
- if (crossAccount) {
169
- return ownerUserId
170
- ? { kind: "owner", userId: ownerUserId }
171
- : { kind: "reject", reason: "no-owner" };
172
- }
173
- if (!callerUserId) return { kind: "reject", reason: "no-caller" };
174
- if (callerHomeAccountId === resolvedAccountId) {
175
- return { kind: "caller", userId: callerUserId };
176
- }
177
- return ownerUserId
178
- ? { kind: "owner", userId: ownerUserId }
179
- : { kind: "reject", reason: "no-owner" };
180
- }
181
- ```
182
-
183
- - [ ] **Step 4: Run test to verify it passes**
184
-
185
- Run: `cd platform/plugins/memory/mcp && npx vitest run src/tools/__tests__/profile-identity.test.ts`
186
- Expected: PASS (7 tests).
187
-
188
- - [ ] **Step 5: Commit**
189
-
190
- ```bash
191
- git add platform/plugins/memory/mcp/src/lib/profile-identity.ts platform/plugins/memory/mcp/src/tools/__tests__/profile-identity.test.ts
192
- git commit -m "feat(memory): pure profile-identity classifier for operator-on-behalf attribution (Task 1557)
193
-
194
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
195
- Session: e53535e2-8c90-4e75-9936-6132ef8a86a0"
196
- ```
197
-
198
- ---
199
-
200
- ### Task 2: Caller home-account resolver
201
-
202
- **Files:**
203
- - Create: `platform/plugins/memory/mcp/src/lib/resolve-admin-home-account.ts`
204
- - Test: `platform/plugins/memory/mcp/src/tools/__tests__/resolve-admin-home-account.test.ts`
205
-
206
- **Interfaces:**
207
- - Consumes: `getSession` from `../lib/neo4j.js` (existing).
208
- - Produces: `resolveAdminHomeAccount(userId: string): Promise<string | null>` — the `accountId` stamped on the caller's single `:AdminUser {userId}` node, or null.
209
-
210
- - [ ] **Step 1: Write the failing test**
211
-
212
- Create `platform/plugins/memory/mcp/src/tools/__tests__/resolve-admin-home-account.test.ts`:
213
-
214
- ```ts
215
- /**
216
- * Task 1557 — resolveAdminHomeAccount returns the accountId stamped on the
217
- * caller's AdminUser node (their home account), used to tell an operator working
218
- * inside a client's sub-account from an admin of the account being operated.
219
- */
220
- import { describe, it, expect, vi, beforeEach } from "vitest";
221
- import { makeMockSession } from "./_helpers/emit-capture.js";
222
-
223
- const CALLER = "11111111-2222-3333-4444-555555555555";
224
- const HOUSE = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
225
-
226
- let mockSession: ReturnType<typeof makeMockSession>;
227
- vi.mock("../../lib/neo4j.js", () => ({
228
- getSession: () => mockSession,
229
- }));
230
-
231
- import { resolveAdminHomeAccount } from "../../lib/resolve-admin-home-account.js";
232
-
233
- describe("resolveAdminHomeAccount (Task 1557)", () => {
234
- beforeEach(() => {
235
- mockSession = makeMockSession();
236
- });
237
-
238
- it("returns the accountId stamped on the caller's AdminUser node", async () => {
239
- mockSession = makeMockSession([[{ accountId: HOUSE }]]);
240
- const result = await resolveAdminHomeAccount(CALLER);
241
- expect(result).toBe(HOUSE);
242
- const cypher = String(mockSession.run.mock.calls[0][0]);
243
- expect(cypher).toMatch(/MATCH \(au:AdminUser \{userId: \$userId\}\)/);
244
- expect(mockSession.run.mock.calls[0][1]).toMatchObject({ userId: CALLER });
245
- expect(mockSession.close).toHaveBeenCalled();
246
- });
247
-
248
- it("returns null when the caller has no AdminUser node", async () => {
249
- mockSession = makeMockSession([[]]);
250
- expect(await resolveAdminHomeAccount(CALLER)).toBeNull();
251
- expect(mockSession.close).toHaveBeenCalled();
252
- });
253
-
254
- it("returns null when the AdminUser node carries no accountId", async () => {
255
- mockSession = makeMockSession([[{ accountId: "" }]]);
256
- expect(await resolveAdminHomeAccount(CALLER)).toBeNull();
257
- });
258
- });
259
- ```
260
-
261
- - [ ] **Step 2: Run test to verify it fails**
262
-
263
- Run: `cd platform/plugins/memory/mcp && npx vitest run src/tools/__tests__/resolve-admin-home-account.test.ts`
264
- Expected: FAIL — cannot resolve module `../../lib/resolve-admin-home-account.js`.
265
-
266
- - [ ] **Step 3: Write minimal implementation**
267
-
268
- Create `platform/plugins/memory/mcp/src/lib/resolve-admin-home-account.ts`:
269
-
270
- ```ts
271
- import { getSession } from "./neo4j.js";
272
-
273
- /**
274
- * Task 1557 — resolve the "home" account of an admin: the `accountId` stamped
275
- * on their `:AdminUser` node. AdminUser is MERGE-keyed on `userId` alone
276
- * (neo4j-store.ts), with `accountId` set ON CREATE, so each admin has exactly
277
- * one node whose `accountId` is the account they were first provisioned under.
278
- *
279
- * A profile op whose caller's home account differs from the account the session
280
- * is scoped to is a house operator working inside a client's sub-account — the
281
- * preference belongs to that client's owner, not the operator. Returns null when
282
- * the caller has no `:AdminUser` node or it carries no `accountId`.
283
- */
284
- export async function resolveAdminHomeAccount(userId: string): Promise<string | null> {
285
- const session = getSession();
286
- try {
287
- const res = await session.run(
288
- `MATCH (au:AdminUser {userId: $userId})
289
- RETURN au.accountId AS accountId
290
- LIMIT 1`,
291
- { userId },
292
- );
293
- const accountId = res.records[0]?.get("accountId");
294
- return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
295
- } finally {
296
- await session.close();
297
- }
298
- }
299
- ```
300
-
301
- - [ ] **Step 4: Run test to verify it passes**
302
-
303
- Run: `cd platform/plugins/memory/mcp && npx vitest run src/tools/__tests__/resolve-admin-home-account.test.ts`
304
- Expected: PASS (3 tests).
305
-
306
- - [ ] **Step 5: Commit**
307
-
308
- ```bash
309
- git add platform/plugins/memory/mcp/src/lib/resolve-admin-home-account.ts platform/plugins/memory/mcp/src/tools/__tests__/resolve-admin-home-account.test.ts
310
- git commit -m "feat(memory): resolveAdminHomeAccount helper (Task 1557)
311
-
312
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
313
- Session: e53535e2-8c90-4e75-9936-6132ef8a86a0"
314
- ```
315
-
316
- ---
317
-
318
- ### Task 3: Wire the classifier into resolveProfileIdentity and the handler bootstrap flags
319
-
320
- **Files:**
321
- - Modify: `platform/plugins/memory/mcp/src/index.ts` (imports; `resolveProfileIdentity` at lines 150-186; `profile-update` handler bootstrap flags at lines 2584-2585)
322
- - Modify: `platform/plugins/memory/mcp/src/tools/__tests__/profile-update-personfields-bootstrap.test.ts:112-116` (structural assertion tracks the new wiring)
323
-
324
- **Interfaces:**
325
- - Consumes: `classifyProfileIdentity` (Task 1), `resolveAdminHomeAccount` (Task 2), `resolveOwnerUserId` (existing, `./lib/resolve-owner-userid.js`).
326
- - Produces: `resolveProfileIdentity(...)` now returns `{ userId: string; bootstrap: boolean } | { reject: … }`. The `bootstrap` field is true exactly when the write attributes to a resolved owner (cross-account or operator-on-behalf); handlers pass it as `bootstrapProfile`/`bootstrapPerson`.
327
-
328
- - [ ] **Step 1: Add the two imports**
329
-
330
- In `platform/plugins/memory/mcp/src/index.ts`, immediately after the existing line 53 `import { resolveOwnerUserId } from "./lib/resolve-owner-userid.js";`, add:
331
-
332
- ```ts
333
- import { resolveAdminHomeAccount } from "./lib/resolve-admin-home-account.js";
334
- import { classifyProfileIdentity } from "./lib/profile-identity.js";
335
- ```
336
-
337
- - [ ] **Step 2: Replace the IdentityResult type and resolveProfileIdentity body**
338
-
339
- Replace the block at `platform/plugins/memory/mcp/src/index.ts:150-186` (from `type IdentityResult =` through the end of the `resolveProfileIdentity` function) with:
340
-
341
- ```ts
342
- type IdentityResult =
343
- | { userId: string; bootstrap: boolean }
344
- | { reject: { content: { type: "text"; text: string }[]; isError: true } };
345
- async function resolveProfileIdentity(
346
- toolName: string,
347
- crossAccount: boolean,
348
- resolvedAccountId: string,
349
- callerUserId: string | undefined,
350
- ): Promise<IdentityResult> {
351
- // Resolve the facts the pure classifier needs. On the own-account path the
352
- // account's owner is resolved only when the caller is NOT operating their
353
- // home account (a foreign house operator inside a client's sub-account), so
354
- // the common owner-is-speaker path costs one AdminUser lookup, not two.
355
- const callerHomeAccountId =
356
- !crossAccount && callerUserId ? await resolveAdminHomeAccount(callerUserId) : null;
357
- const needOwner =
358
- crossAccount || (!!callerUserId && callerHomeAccountId !== resolvedAccountId);
359
- const ownerUserId = needOwner ? await resolveOwnerUserId(resolvedAccountId) : null;
360
-
361
- const decision = classifyProfileIdentity({
362
- crossAccount,
363
- resolvedAccountId,
364
- callerUserId,
365
- callerHomeAccountId,
366
- ownerUserId,
367
- });
368
-
369
- if (decision.kind === "reject") {
370
- const text =
371
- decision.reason === "no-owner"
372
- ? `${toolName} failed: target account ${resolvedAccountId.slice(0, 8)}… has no owner AdminUser to attribute the profile to — its graph root is unprovisioned (see Task 1359 seedAccountGraphRoot).`
373
- : `${toolName} requires an authenticated admin session with userId`;
374
- return {
375
- reject: {
376
- content: [{ type: "text" as const, text }],
377
- isError: true as const,
378
- },
379
- };
380
- }
381
-
382
- if (decision.kind === "owner") {
383
- if (crossAccount) {
384
- process.stderr.write(
385
- `[xacct] op=identity tool=${toolName} target=${resolvedAccountId.slice(0, 8)} resolved-userId=${decision.userId.slice(0, 8)}\n`,
386
- );
387
- } else {
388
- process.stderr.write(
389
- `[xacct] op=identity tool=${toolName} mode=operator-on-behalf target=${resolvedAccountId.slice(0, 8)} caller=${(callerUserId ?? "").slice(0, 8)} resolved-userId=${decision.userId.slice(0, 8)}\n`,
390
- );
391
- }
392
- return { userId: decision.userId, bootstrap: true };
393
- }
394
-
395
- // decision.kind === "caller" — own-account owner-is-speaker / second admin.
396
- return { userId: decision.userId, bootstrap: false };
397
- }
398
- ```
399
-
400
- - [ ] **Step 3: Point the profile-update bootstrap flags at the resolved identity**
401
-
402
- In `platform/plugins/memory/mcp/src/index.ts`, replace lines 2584-2585:
403
-
404
- ```ts
405
- bootstrapProfile: crossAccount,
406
- bootstrapPerson: crossAccount,
407
- ```
408
-
409
- with:
410
-
411
- ```ts
412
- bootstrapProfile: ident.bootstrap,
413
- bootstrapPerson: ident.bootstrap,
414
- ```
415
-
416
- (No change is needed in the `profile-read` or `profile-delete` handlers: they read only `ident.userId`, which the success shape still carries. The `if ("reject" in ident) return ident.reject;` guard is unchanged in all three.)
417
-
418
- - [ ] **Step 4: Update the structural test to track the new wiring**
419
-
420
- In `platform/plugins/memory/mcp/src/tools/__tests__/profile-update-personfields-bootstrap.test.ts`, replace lines 112-116:
421
-
422
- ```ts
423
- it("index.ts wires bootstrapPerson: crossAccount at the profile-update call site", () => {
424
- const SRC = resolve(dirname(fileURLToPath(import.meta.url)), "../../index.ts");
425
- const src = readFileSync(SRC, "utf8");
426
- expect(src).toContain("bootstrapPerson: crossAccount");
427
- });
428
- ```
429
-
430
- with:
431
-
432
- ```ts
433
- it("index.ts wires bootstrapPerson from the resolved identity at the profile-update call site", () => {
434
- const SRC = resolve(dirname(fileURLToPath(import.meta.url)), "../../index.ts");
435
- const src = readFileSync(SRC, "utf8");
436
- expect(src).toContain("bootstrapPerson: ident.bootstrap");
437
- });
438
- ```
439
-
440
- - [ ] **Step 5: Build and run the full suite**
441
-
442
- Run: `cd platform/plugins/memory/mcp && npm run build && npm test`
443
- Expected: build clean (no TypeScript errors); all tests PASS, including the new `profile-identity` and `resolve-admin-home-account` files and the updated `profile-update-personfields-bootstrap` structural test. Baseline before this task was 67 files / 477 tests passing; after Tasks 1-3 expect 69 files and 487 tests (477 + 7 classifier + 3 resolver), all green.
444
-
445
- - [ ] **Step 6: Commit**
446
-
447
- ```bash
448
- git add platform/plugins/memory/mcp/src/index.ts platform/plugins/memory/mcp/src/tools/__tests__/profile-update-personfields-bootstrap.test.ts
449
- git commit -m "fix(memory): attribute preferences to the account owner when an operator works inside a client's sub-account (Task 1557)
450
-
451
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
452
- Session: e53535e2-8c90-4e75-9936-6132ef8a86a0"
453
- ```
454
-
455
- ---
456
-
457
- ### Task 4: Teach the skill and document the model
458
-
459
- **Files:**
460
- - Modify: `platform/plugins/memory/skills/conversational-memory/SKILL.md` (add an operator-on-behalf section)
461
- - Modify: `platform/plugins/memory/PLUGIN.md` (Conversational Memory section, after line 223)
462
- - Modify: `platform/plugins/memory/mcp/src/tools/profile-update.ts:312-314` (refresh the now-stale bootstrap comment)
463
-
464
- **Interfaces:** none (docs and comment only).
465
-
466
- - [ ] **Step 1: Add the operator-on-behalf section to the skill**
467
-
468
- In `platform/plugins/memory/skills/conversational-memory/SKILL.md`, insert this section immediately before the `## How to Store` heading (currently line 75). Zero em-dashes:
469
-
470
- ```markdown
471
- ## Operating a client's account on the owner's behalf
472
-
473
- When you are a house operator working inside a client's sub-account, the preferences you capture there belong to that client, the account's owner on record, not to you. Attribution is automatic. A preference stored while you operate a client's account is written against that owner, and "what does this owner prefer?" reads the owner's list. You pass no target and no flag; the tool decides from who is driving the session.
474
-
475
- Your own working preferences belong to your own session, not a client's. Store them when you are operating your own account.
476
-
477
- If a client's account has no owner on record, a preference write is refused out loud rather than saved to your profile. That means the account's graph root was never seeded, so it cannot hold owner preferences until it is.
478
- ```
479
-
480
- - [ ] **Step 2: Document the model in PLUGIN.md**
481
-
482
- In `platform/plugins/memory/PLUGIN.md`, insert this paragraph immediately after line 223 (the end of the `## Conversational Memory` intro paragraph, before the `## UserProfile + Person Field Management` heading):
483
-
484
- ```markdown
485
- When a house operator operates a client's sub-account, the session is scoped into that account (`ACCOUNT_ID` is the sub-account, `USER_ID` is the operator, no `HOUSE_ADMIN_SCOPE`). In that session `profile-read`/`profile-update`/`profile-delete` resolve to the account's **owner** `AdminUser`, not the operator: the operator has no `AdminUser` node in the sub-account subgraph, so `resolveProfileIdentity` sees the caller is not an admin of the account it is scoped to and attributes to the seeded owner — the same owner identity the cross-account `targetAccountId` path resolves (above), reached without `targetAccountId` because the re-scoped session cannot set it. An account with no seeded owner rejects the write loudly (`… has no owner AdminUser …`) rather than attributing it to the operator, and emits `[xacct] op=identity tool=profile-* mode=operator-on-behalf target=<sub8> caller=<op8> resolved-userId=<owner8>` on the owner-attributed path. The operator's own preferences live on their own (house) session.
486
- ```
487
-
488
- - [ ] **Step 3: Refresh the stale bootstrap comment**
489
-
490
- In `platform/plugins/memory/mcp/src/tools/profile-update.ts`, replace lines 312-314:
491
-
492
- ```ts
493
- // preference lands. Own-account writes never set bootstrapProfile (their
494
- // UserProfile is provisioned at PIN setup), so this is byte-identical for
495
- // them. Idempotent: a MERGE that matches an existing UserProfile is a no-op.
496
- ```
497
-
498
- with:
499
-
500
- ```ts
501
- // preference lands. Own-account owner-is-speaker writes never set
502
- // bootstrapProfile (their UserProfile is provisioned at PIN setup); an
503
- // operator-on-behalf write (Task 1557) does set it, and this MERGE
504
- // bootstraps the client owner's UserProfile exactly as the cross-account
505
- // path does. Idempotent: a MERGE that matches an existing UserProfile is a no-op.
506
- ```
507
-
508
- - [ ] **Step 4: Verify the build still compiles (comment-only source change)**
509
-
510
- Run: `cd platform/plugins/memory/mcp && npm run build`
511
- Expected: clean, no TypeScript errors.
512
-
513
- - [ ] **Step 5: Commit**
514
-
515
- ```bash
516
- git add platform/plugins/memory/skills/conversational-memory/SKILL.md platform/plugins/memory/PLUGIN.md platform/plugins/memory/mcp/src/tools/profile-update.ts
517
- git commit -m "docs(memory): teach conversational-memory the operator-on-behalf owner attribution (Task 1557)
518
-
519
- Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
520
- Session: e53535e2-8c90-4e75-9936-6132ef8a86a0"
521
- ```
522
-
523
- ---
524
-
525
- ## Self-Review
526
-
527
- **Spec coverage:**
528
- - "Attribute a foreign operator's preference to the account owner" → Task 1 (classifier `owner` branch) + Task 3 (wiring). ✓
529
- - "Discriminator = caller not an admin of the scoped account, robust to multi-admin" → Task 2 (`resolveAdminHomeAccount`) + Task 1 (`callerHomeAccountId === resolvedAccountId` → caller). ✓
530
- - "No owner → loud reject, never silent operator attribution" → Task 1 (`no-owner` reject) + Task 3 (reject envelope). ✓
531
- - "Bootstrap the owner's profile on the operator-on-behalf path" → Task 3 (`bootstrap` from resolved identity) + Task 4 Step 3 (comment). ✓
532
- - "Read side returns owner's preferences" → covered for free: `profile-read` uses `ident.userId` which now resolves to the owner (Task 3 note in Step 3). ✓
533
- - "Own-account owner-is-speaker and second-admin unchanged" → Task 1 (`caller` branch) + tests in Task 1. ✓
534
- - "Cross-account path unchanged, log line byte-identical" → Task 3 Step 2 (crossAccount branch keeps the exact existing line). ✓
535
- - "Skill teaching" → Task 4 Step 1. "PLUGIN.md doc" → Task 4 Step 2. ✓
536
- - "Tests: operator-on-behalf, own-account, second-admin, no-owner, cross-account" → Task 1 test (all classifier branches) + Task 2 test (home resolution) + existing `resolve-owner-userid.test.ts` and `cross-account.test.ts` remain green (verified in Task 3 Step 5). ✓
537
-
538
- **Placeholder scan:** No TBD/TODO/"handle edge cases"; every code step carries complete code. ✓
539
-
540
- **Type consistency:** `ProfileAttribution` kinds (`caller`/`owner`/`reject`) and the `classifyProfileIdentity` input keys are identical between Task 1's definition, Task 1's tests, and Task 3's call site. `IdentityResult` success shape `{ userId, bootstrap }` matches the handler reads (`ident.userId`, `ident.bootstrap`). `resolveAdminHomeAccount(userId): Promise<string | null>` matches its Task 3 call. ✓
541
-
542
- **Out-of-scope confirmed untouched:** no change to `targetAccountId`/`resolveEffectiveAccountCore`, the sub-account picker/spawn path, owner seeding, or arbitrary-person attribution. A no-owner backfill remains a separate concern; if the on-device reproduce surfaces a rootless client account, file a `.tasks/NNN-*.md` rather than relaxing the reject.