@mulmoclaude/core 0.2.11 → 0.2.14

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 (33) hide show
  1. package/assets/helps/billing-invoice.md +77 -51
  2. package/assets/helps/collection-skills.md +16 -8
  3. package/assets/helps/error-recovery.md +179 -0
  4. package/assets/helps/index.md +1 -0
  5. package/dist/collection/core/schema.d.ts +19 -2
  6. package/dist/collection/index.cjs +2 -1
  7. package/dist/collection/index.js +2 -2
  8. package/dist/collection/server/discovery.d.ts +1 -0
  9. package/dist/collection/server/index.cjs +1 -1
  10. package/dist/collection/server/index.js +1 -1
  11. package/dist/collection-watchers/index.cjs +1 -1
  12. package/dist/collection-watchers/index.js +1 -1
  13. package/dist/{deriveAll-VRWrs3SF.cjs → deriveAll-Cb9rWjan.cjs} +17 -1
  14. package/dist/deriveAll-Cb9rWjan.cjs.map +1 -0
  15. package/dist/{deriveAll-vzIhhKBK.js → deriveAll-D3wFH4Tw.js} +12 -2
  16. package/dist/deriveAll-D3wFH4Tw.js.map +1 -0
  17. package/dist/feeds/index.cjs +2 -2
  18. package/dist/feeds/index.js +2 -2
  19. package/dist/feeds/server/index.cjs +3 -3
  20. package/dist/feeds/server/index.js +3 -3
  21. package/dist/{ingestTypes-B8DaQOTy.js → ingestTypes-BtMZogMX.js} +2 -2
  22. package/dist/{ingestTypes-B8DaQOTy.js.map → ingestTypes-BtMZogMX.js.map} +1 -1
  23. package/dist/{ingestTypes-DDOVhOYC.cjs → ingestTypes-DhJ63Ogd.cjs} +2 -2
  24. package/dist/{ingestTypes-DDOVhOYC.cjs.map → ingestTypes-DhJ63Ogd.cjs.map} +1 -1
  25. package/dist/{server-DAyIQ5rT.cjs → server-BjXvqGrE.cjs} +18 -7
  26. package/dist/server-BjXvqGrE.cjs.map +1 -0
  27. package/dist/{server-D2QZuMS2.js → server-CRlmOBVQ.js} +18 -7
  28. package/dist/server-CRlmOBVQ.js.map +1 -0
  29. package/package.json +1 -1
  30. package/dist/deriveAll-VRWrs3SF.cjs.map +0 -1
  31. package/dist/deriveAll-vzIhhKBK.js.map +0 -1
  32. package/dist/server-D2QZuMS2.js.map +0 -1
  33. package/dist/server-DAyIQ5rT.cjs.map +0 -1
@@ -3,11 +3,12 @@
3
3
  Read this when the user asks to **set up invoicing / billing** (sample query:
4
4
  *"Set up invoicing for my business"*). It scaffolds **two** collection skills:
5
5
 
6
- - **`profile`** — the user's **own** business identity (the "bill-from" block:
7
- company name, tax ID, payment details). A singleton (one record, id `me`).
8
- - **`invoice`** an invoice ledger; each invoice references a client, embeds the
9
- profile as the issuer, and carries a table of line items with host-computed
10
- subtotal / tax / total.
6
+ - **`profile`** — the user's **own** business identit(ies) (the "bill-from" block:
7
+ company name, tax ID, payment details). One or more records, one per issuer; the
8
+ primary one is id `me`.
9
+ - **`invoice`** an invoice ledger; each invoice references a client, picks an
10
+ issuer profile (`issuerId` `profile`) which it embeds as the bill-from block,
11
+ and carries a table of line items with host-computed subtotal / tax / total.
11
12
 
12
13
  This is **Bundle B** of the billing suite. It **builds on Bundle A**
13
14
  (`clients` + `worklog`, see `config/helps/billing-clients-worklog.md`):
@@ -37,7 +38,10 @@ use the `mc-` prefix.**
37
38
  > other collections in the workspace. The whole point is a reproducible billing
38
39
  > suite. (For a *custom* collection, use `config/helps/collection-skills.md`
39
40
  > instead.) Existing records under `data/invoice/items` / `data/profile/items`
40
- > already match these schemas and will render as-is no data edits needed.
41
+ > render as-is, with one exception: because `invoice.issuerId` is new and
42
+ > required, **backfill `issuerId: "me"` on any pre-existing invoice records** (a
43
+ > `manageCollection` `putItems` with `mode: "merge"`) so their issuer block
44
+ > resolves — otherwise they show a "missing issuer" prompt until set.
41
45
 
42
46
  ## Slug contract (do not change these)
43
47
 
@@ -45,7 +49,7 @@ use the `mc-` prefix.**
45
49
 
46
50
  | Collection | slug | `dataPath` | referenced by |
47
51
  |---|---|---|---|
48
- | Profile | `profile` | `data/profile/items` | `invoice.issuer` (embed `profile/me`) |
52
+ | Profile | `profile` | `data/profile/items` | `invoice.issuerId` (ref) + `invoice.issuer` (embed via `idField`) |
49
53
  | Invoice | `invoice` | `data/invoice/items` | — |
50
54
  | Clients | `clients` | `data/clients/items` | `invoice.clientId` (ref, from Bundle A) |
51
55
 
@@ -56,13 +60,15 @@ use the `mc-` prefix.**
56
60
 
57
61
  ## Order: `profile` before `invoice`
58
62
 
59
- `invoice.issuer` embeds the `profile/me` record, so create `profile` first.
63
+ `invoice.issuerId` references a `profile` record (and `issuer` embeds it), so
64
+ create `profile` first.
60
65
 
61
66
  ---
62
67
 
63
68
  ## 1. `profile`
64
69
 
65
- `data/skills/profile/schema.json` (a **singleton** exactly one record, id `me`):
70
+ `data/skills/profile/schema.json` (one record per issuer; the primary one is id
71
+ `me` — **not** a singleton, so an invoice can pick among several issuers):
66
72
 
67
73
  ```json
68
74
  {
@@ -70,7 +76,6 @@ use the `mc-` prefix.**
70
76
  "icon": "badge",
71
77
  "dataPath": "data/profile/items",
72
78
  "primaryKey": "id",
73
- "singleton": "me",
74
79
  "fields": {
75
80
  "id": { "type": "string", "label": "ID", "primary": true, "required": true },
76
81
  "companyName": { "type": "string", "label": "Company / legal name", "required": true },
@@ -90,25 +95,27 @@ use the `mc-` prefix.**
90
95
  ```markdown
91
96
  ---
92
97
  name: profile
93
- description: The user's own business profile — the issuer ("bill-from") identity
94
- used on invoices. A singleton collection with exactly one record, id `me`. The
95
- record lives at `data/profile/items/me.json`; the user views and edits it at
96
- `/collections/profile`. Record I/O via the `manageCollection` tool (raw
97
- Read / Write / Edit on the JSON file is the escape hatch).
98
+ description: The user's own business profile(s) — the issuer ("bill-from") identity
99
+ used on invoices. One or more records, each an issuer; the primary one is `me`.
100
+ Records live at `data/profile/items/<id>.json`; the user views and edits them at
101
+ `/collections/profile`. Each invoice picks one via its `issuerId` ref. Record I/O
102
+ via the `manageCollection` tool (raw Read / Write / Edit on the JSON is the escape hatch).
98
103
  ---
99
104
 
100
105
  # Business Profile (schema-driven collection)
101
106
 
102
- Holds the user's **own** business identity — the "bill-from" side of an invoice.
103
- Counterpart to `clients`, which holds the "bill-to" parties.
107
+ Holds the user's **own** business identit(ies) — the "bill-from" side of an
108
+ invoice. Counterpart to `clients`, which holds the "bill-to" parties.
104
109
 
105
- ## Singletonexactly one record, id `me`
106
- This collection has **one** record. Its primary key is always the literal string
107
- `me`, stored at `data/profile/items/me.json`. Never create a second record and
108
- never invent another id read, create, and update `me.json` only.
110
+ ## Multiple issuers `me` is the primary one
111
+ This collection can hold **several** records, one per issuer identity (a personal
112
+ name, a company, a second LLC). The primary profile keeps the id `me`, stored at
113
+ `data/profile/items/me.json`; add more with their own kebab-case ids (e.g.
114
+ `pervasive`, `personal`). An invoice points at one via its `issuerId`, so a
115
+ profile must exist before an invoice can reference it.
109
116
 
110
117
  ## Record shape
111
- - `id` — string, **primary key**, always `me`
118
+ - `id` — string, **primary key** (kebab-case slug; the primary profile is `me`)
112
119
  - `companyName` — string, **required** (the legal/company name shown on invoices)
113
120
  - `taxRegistrationId` — string (VAT / EIN / JP T-number — region-dependent)
114
121
  - `email` — email
@@ -123,18 +130,20 @@ never invent another id — read, create, and update `me.json` only.
123
130
  Leave optional fields the user hasn't given you out of the JSON entirely.
124
131
 
125
132
  ## What to do
126
- **Set up / update**: Read `data/profile/items/me.json` (may not exist yet), merge
127
- changes, Write back. Preserve fields you weren't asked to change. If missing and
128
- the user wants to set their profile, create it with `id: "me"` plus the fields
129
- they provided.
133
+ **Set up / update**: Read the relevant `data/profile/items/<id>.json` (the primary
134
+ one is `me.json`, may not exist yet), merge changes, Write back. Preserve fields
135
+ you weren't asked to change. When creating the primary profile use `id: "me"`; for
136
+ an additional issuer, list `data/profile/items/` first and pick a fresh kebab-case
137
+ id.
130
138
 
131
- **Look up**: Read `me.json` and answer from it. If missing, tell the user their
132
- profile isn't set up yet and offer to collect it (`presentForm` only if several
133
- fields are needed at once).
139
+ **Look up**: Read the relevant profile JSON and answer from it. If the primary
140
+ `me.json` is missing, tell the user their profile isn't set up yet and offer to
141
+ collect it (`presentForm` only if several fields are needed at once).
134
142
 
135
- **Never delete** the profile unless the user explicitly asks to reset it.
143
+ **Never delete** a profile unless the user explicitly asks to an invoice's
144
+ `issuerId` ref breaks if you remove a profile it points at.
136
145
 
137
- ## Linking to the profile in chat
146
+ ## Linking to a profile in chat
138
147
  - Do: `[your business profile](/collections/profile?selected=me)`
139
148
  - Don't: link the raw JSON path.
140
149
 
@@ -147,8 +156,9 @@ only when you genuinely need several fields they haven't provided.
147
156
 
148
157
  ## 2. `invoice`
149
158
 
150
- `data/skills/invoice/schema.json` (`issuer` embeds `profile/me`; `clientId` is a
151
- `ref` **to `clients`**; subtotal / tax / total are `derived`):
159
+ `data/skills/invoice/schema.json` (`issuerId` is a `ref` **to `profile`** and
160
+ `issuer` embeds whichever profile it names via `idField`; `clientId` is a `ref`
161
+ **to `clients`**; subtotal / tax / total are `derived`):
152
162
 
153
163
  ```json
154
164
  {
@@ -158,7 +168,8 @@ only when you genuinely need several fields they haven't provided.
158
168
  "primaryKey": "id",
159
169
  "fields": {
160
170
  "id": { "type": "string", "label": "ID", "primary": true, "required": true },
161
- "issuer": { "type": "embed", "to": "profile", "id": "me", "label": "From (issuer)" },
171
+ "issuerId": { "type": "ref", "to": "profile", "label": "From (issuer)", "required": true },
172
+ "issuer": { "type": "embed", "to": "profile", "idField": "issuerId", "label": "From (issuer)" },
162
173
  "clientId": { "type": "ref", "to": "clients", "label": "Client", "required": true },
163
174
  "issueDate": { "type": "date", "label": "Issued", "required": true },
164
175
  "dueDate": { "type": "date", "label": "Due" },
@@ -196,7 +207,8 @@ name: invoice
196
207
  description: A simple invoice ledger — create, list, edit, and remove invoices.
197
208
  Records live at `data/invoice/items/<id>.json`; the user views them at
198
209
  `/collections/invoice`. Each invoice references a client (`clientId` → the
199
- `clients` collection) and embeds the user's `profile/me` as the issuer.
210
+ `clients` collection) and an issuer profile (`issuerId` the `profile`
211
+ collection), which it embeds as the bill-from block.
200
212
  Subtotal / tax / total are host-computed — you don't write them.
201
213
  ---
202
214
 
@@ -207,9 +219,13 @@ description: A simple invoice ledger — create, list, edit, and remove invoices
207
219
  counter): `INV-2026-0001`. List `data/invoice/items/` first to find the highest
208
220
  number for the year, then increment; pick the next free number rather than
209
221
  overwriting.
210
- - `issuer` — **display-only** embed of `profile/me` (the bill-from block). You do
211
- **not** write this it carries no stored value. If the profile isn't set up,
212
- the view shows a "set it up" prompt; point the user at `/collections/profile`.
222
+ - `issuerId` — ref `profile`, **required**. The bill-from identity. Write the
223
+ raw profile slug (the primary profile is `me`); the host renders it as a dropdown
224
+ picker. List `data/profile/items/` to find the right slug; no profile set up yet
225
+ → point the user at `/collections/profile`.
226
+ - `issuer` — **display-only** embed of the profile named by `issuerId` (the
227
+ bill-from block). You do **not** write this — it carries no stored value; it
228
+ follows whatever `issuerId` points at.
213
229
  - `clientId` — ref → `clients`, **required**
214
230
  - `issueDate` — ISO date `YYYY-MM-DD`, **required** (default today)
215
231
  - `dueDate` — ISO date (optional)
@@ -232,8 +248,10 @@ or supply a literal slug. Never invent a clientId — it renders as a broken lin
232
248
 
233
249
  ## What to do
234
250
  **Create**: derive an `id`, build the record, Write `data/invoice/items/<id>.json`.
235
- Defaults: `status: "draft"`, `issueDate: <today>`. Set `currency` from context —
236
- don't silently default to USD. Don't write `subtotal` / `tax` / `total`.
251
+ Defaults: `status: "draft"`, `issueDate: <today>`, `issuerId: "me"` (the primary
252
+ profile) unless the user names a different issuer then resolve it to a real
253
+ `profile` slug. Set `currency` from context — don't silently default to USD. Don't
254
+ write `subtotal` / `tax` / `total`.
237
255
 
238
256
  **Create from worklog hours** (common): when the user says "invoice Acme for the
239
257
  work I did this month":
@@ -304,11 +322,12 @@ and present it in the canvas with the `presentDocument` tool. Steps:
304
322
 
305
323
  1. **Resolve the recipient (Bill To).** Read `data/clients/items/<clientId>.json`
306
324
  for the client's `name`, `address`, `email`.
307
- 2. **Resolve the issuer (From).** Read `data/profile/items/me.json` for
308
- `companyName`, `taxRegistrationId`, `address`, `email`, `phone`,
309
- `paymentDetails`. **If that file does not exist**, stop and tell the user their
310
- business profile isn't set up point them at `/collections/profile` and do
311
- not write a half-blank invoice.
325
+ 2. **Resolve the issuer (From).** Read `data/profile/items/<issuerId>.json` — the
326
+ invoice's own `issuerId` (fall back to `me` for a legacy invoice without one) —
327
+ for `companyName`, `taxRegistrationId`, `address`, `email`, `phone`,
328
+ `paymentDetails`. **If that file does not exist**, stop and tell the user that
329
+ issuer's business profile isn't set up — point them at `/collections/profile` —
330
+ and do not write a half-blank invoice.
312
331
  3. **Compute totals** from line items: `subtotal` = Σ(`quantity` × `rate`), `tax`
313
332
  = `subtotal` × `taxRate` (missing `taxRate` = 0), `total` = `subtotal` + `tax`.
314
333
  Format money with the invoice's `currency`.
@@ -332,8 +351,11 @@ The invoice record is in the `<record_data_json>` block above. You are the
332
351
  `accounting` role and own `manageAccounting`. Post the double-entry sale journal.
333
352
 
334
353
  ### 1. Resolve the book
335
- 1. Read `data/profile/items/me.json`. If it has a non-empty `defaultBookId`, use
336
- it as `bookId` and skip the rest of this step.
354
+ 1. Read `data/profile/items/<issuerId>.json` the invoice's own `issuerId` (fall
355
+ back to `me` for a legacy invoice without one). If it has a non-empty
356
+ `defaultBookId`, use it as `bookId` and skip the rest of this step. If that
357
+ profile file doesn't exist, tell the user that issuer's profile isn't set up
358
+ (point them at `/collections/profile`) and stop.
337
359
  2. Otherwise call `manageAccounting` `getBooks`. One book → use it. Several →
338
360
  keep books whose currency/country fit the invoice; if one remains use it; else
339
361
  prefer the book whose name matches the issuer's `companyName`; only if still
@@ -378,11 +400,14 @@ The invoice record is in the `<record_data_json>` block above. You are the
378
400
  `accounting` role and own `manageAccounting`. The invoice is paid; post the
379
401
  cash-receipt journal that clears the receivable. Payment is always direct deposit
380
402
  to the issuer's bank account (the issuer's `paymentDetails` on
381
- `data/profile/items/me.json`), so the debit is the book's Cash / Checking account.
403
+ `data/profile/items/<issuerId>.json`), so the debit is the book's Cash / Checking
404
+ account.
382
405
 
383
406
  ### 1. Resolve the book
384
- Read `data/profile/items/me.json` for a non-empty `defaultBookId`; else `getBooks`
385
- and pick as in the sale flow. No book tell the user to set one up and stop.
407
+ Read `data/profile/items/<issuerId>.json` the invoice's own `issuerId` (fall
408
+ back to `me` for a legacy invoice) for a non-empty `defaultBookId`; else
409
+ `getBooks` and pick as in the sale flow. No book → tell the user to set one up and
410
+ stop.
386
411
 
387
412
  ### 2. Resolve real account codes
388
413
  `getAccounts` — never invent: **Cash / Checking / Bank** (asset, the debit; if
@@ -418,7 +443,8 @@ The invoice record is in the `<record_data_json>` block above. You are the
418
443
  journals posted for it.
419
444
 
420
445
  ### 1. Resolve the book
421
- Read `data/profile/items/me.json` for a non-empty `defaultBookId`; else resolve
446
+ Read `data/profile/items/<issuerId>.json` the invoice's own `issuerId` (fall
447
+ back to `me` for a legacy invoice) — for a non-empty `defaultBookId`; else resolve
422
448
  via `getBooks` (as in the sale flow). No book → nothing to void; say so and stop.
423
449
 
424
450
  ### 2. Resolve the Accounts Receivable code
@@ -168,10 +168,17 @@ Every field spec needs a `type` and a `label`. Extra keys by type:
168
168
  A `derived` field on the same record can also **dereference** a `ref` to read
169
169
  a numeric column off the record it points at — see the cross-collection
170
170
  formula syntax below.
171
- - **`embed`** — `to: "<target-slug>"`, `id: "<record-id>"`. Pulls a _fixed_
171
+ - **`embed`** — `to: "<target-slug>"`, plus **exactly one** of `id` (a _fixed_
172
+ record id, same for every row) or `idField` (a _per-record_ target). Pulls a
172
173
  record from another collection into the read-only detail view (display-only,
173
- **nothing is stored** on this record). Example: an invoice embedding the
174
- user's own profile: `{ "type": "embed", "to": "profile", "id": "me" }`.
174
+ **nothing is stored** on this record). Fixed every row embeds the same
175
+ singleton record (e.g. a one-record `settings` collection):
176
+ `{ "type": "embed", "to": "settings", "id": "app" }`.
177
+ Per-record — `idField` names a sibling field (typically a `ref`) holding the
178
+ target id, so each row embeds a different record; the host renders that
179
+ sibling as a dropdown picker in the editor and hides its own cell (the embed
180
+ owns it). E.g. a multi-issuer invoice: a `ref` field `issuerId → profile`
181
+ plus `{ "type": "embed", "to": "profile", "idField": "issuerId" }`.
175
182
  - **`table`** — `of: { <col>: <sub-field-spec>, ... }`. An array of rows. Each
176
183
  sub-field is a flat spec; sub-fields **cannot** be `table` or `derived`
177
184
  (no nested tables, no computed columns).
@@ -851,8 +858,9 @@ recipe when the user wants any of them, and copy the schema verbatim:
851
858
  - **`worklog`** — adds a `ref` (`clientId → clients`), a `date`, a `number`, a
852
859
  `boolean`. A companion data source.
853
860
  - **`config/helps/billing-invoice.md`** (Bundle B):
854
- - **`profile`** — a `singleton` (one record, id `me`): the issuer identity.
855
- - **`invoice`** — the full toolkit in one schema: an `embed` issuer
856
- (`profile/me`), a `ref` client (`clients`), a `table` of line items, three
857
- `derived` money fields, an `enum` status, and four `actions` (PDF always-on;
858
- sale / payment / void gated by `status` via `when`).
861
+ - **`profile`** — one record per issuer identity (primary id `me`); each invoice
862
+ picks one.
863
+ - **`invoice`** the full toolkit in one schema: a `ref` issuer (`issuerId
864
+ profile`) embedded via an `idField` `embed`, a `ref` client (`clients`), a
865
+ `table` of line items, three `derived` money fields, an `enum` status, and four
866
+ `actions` (PDF always-on; sale / payment / void gated by `status` via `when`).
@@ -0,0 +1,179 @@
1
+ # Error recovery — when a tool call fails
2
+
3
+ This is the lookup the agent reads BEFORE asking the user a clarifying
4
+ question or giving up on a failing tool call. Each section is keyed by
5
+ the error message you'd see in tool output, with the cause and the
6
+ documented fix.
7
+
8
+ Cite the section you used in your reply so the user can follow up
9
+ (e.g. "Per `config/helps/error-recovery.md` § gh-auth / SSH …").
10
+
11
+ If no section here matches, list the workspace's other help files
12
+ (`ls config/helps/`) and Read whichever name best matches the failing
13
+ area (`sandbox.md`, `github.md`, `collection-skills.md`, etc.) before
14
+ falling back to asking the user.
15
+
16
+ ## gh / git / SSH errors inside the sandbox
17
+
18
+ ### Symptoms
19
+
20
+ - `gh: To authenticate, please run gh auth login`
21
+ - `git@github.com: Permission denied (publickey)`
22
+ - `Could not resolve host: github.com`
23
+ - `Permission denied (publickey)` on `git push` / `git clone <ssh-url>`
24
+ - `fatal: Could not read from remote repository`
25
+
26
+ ### Cause
27
+
28
+ The Claude Code agent runs inside a credential-free Docker sandbox by
29
+ default. The host's SSH agent and `gh` config aren't exposed unless the
30
+ user opts them in.
31
+
32
+ ### Fix
33
+
34
+ Tell the user to enable the two opt-in mounts on the next agent spawn
35
+ (see also `config/helps/sandbox.md` for the full contract):
36
+
37
+ ```bash
38
+ # Forward the host's SSH agent into the container.
39
+ # Private keys stay on the host; only the signing oracle is exposed.
40
+ SANDBOX_FORWARD_SSH_AGENT=1 \
41
+ # Mount allowlisted config files/dirs read-only — including ~/.config/gh.
42
+ SANDBOX_MOUNT_CONFIGS=gh \
43
+ yarn dev # or: npx mulmoclaude
44
+ ```
45
+
46
+ Equivalent CLI flags: `--sandbox-forward-ssh-agent --sandbox-mount-configs=gh`.
47
+
48
+ After restart, inside the agent's first tool turn verify:
49
+
50
+ ```bash
51
+ ssh-add -l # should list at least one key
52
+ gh auth status # should report logged in
53
+ ```
54
+
55
+ If `ssh-add -l` fails, the host's SSH agent isn't running — tell the
56
+ user to start it (`ssh-add ~/.ssh/id_*` on macOS / Linux). If
57
+ `gh auth status` fails, the user needs to run `gh auth login` on the
58
+ host first (host config is mounted read-only into the sandbox).
59
+
60
+ ### When neither helps
61
+
62
+ The sandbox itself can be turned off for the session with
63
+ `DISABLE_SANDBOX=1 yarn dev` / `--disable-sandbox`. The agent then
64
+ inherits the user's full environment. Recommend this only when the
65
+ credential mount approach didn't resolve the issue — the sandbox is
66
+ the safer default.
67
+
68
+ ## Collection registry — Contribute / Discover failures
69
+
70
+ ### Symptoms
71
+
72
+ - Contribute flow: `gh pr create` fails, `git push` rejected, or the
73
+ registry clone fails inside `github/`.
74
+ - Discover tab loads no entries, or one registry's cards are missing.
75
+
76
+ ### Cause + fix
77
+
78
+ For the Contribute side, the underlying issue is almost always
79
+ sandbox credentials — see the gh/git/SSH section above before anything
80
+ else.
81
+
82
+ For Discover, multi-registry config lives at
83
+ `config/collections-registries.json`. A malformed entry there is
84
+ silently dropped; check the server log for
85
+ `[collections-registry] registry config entry rejected`. The file
86
+ format and validation rules are documented in
87
+ `config/helps/collection-skills.md` (Contribute bundle layout) and the
88
+ shipped registry repo's README. Common rejections:
89
+
90
+ - URL not HTTPS, or includes embedded credentials.
91
+ - `rawBaseUrl` contains a `?` query or `#` fragment.
92
+ - `name` reuses the reserved value `official`.
93
+ - `name` doesn't match `[A-Za-z0-9][A-Za-z0-9_-]{0,31}`.
94
+
95
+ ## Marp slide PDF — empty / image / font issues
96
+
97
+ ### Symptoms
98
+
99
+ - PDF export of a Marp deck produces tofu (□□□) for Japanese / CJK text.
100
+ - Inline images in a slide are missing from the PDF.
101
+ - A custom Marp `theme: <name>` is ignored.
102
+
103
+ ### Fix
104
+
105
+ CJK fonts (Hiragino on macOS, Yu Gothic / Meiryo on Windows, Noto Sans
106
+ CJK on Linux) need to exist on the **host running the server**, not
107
+ in the sandbox (PDF render happens server-side via puppeteer). On
108
+ Linux: `sudo apt-get install fonts-noto-cjk`. Docker host:
109
+ `apt-get install -y fonts-noto-cjk` in the production Dockerfile.
110
+
111
+ Inline images missing from the PDF: paths must be relative to the
112
+ `.md` file, NOT absolute or workspace-rooted. See the "Image
113
+ references in markdown / HTML" section of the system prompt for the
114
+ rule.
115
+
116
+ Custom theme ignored: themes live in `~/mulmoclaude/config/marp-themes/<name>.css`.
117
+ The filename (sans `.css`) is the theme slug; only `[A-Za-z0-9_-]` is
118
+ allowed. Reload the browser tab after adding a theme — preview caches
119
+ per session.
120
+
121
+ ## Build / yarn workspace ordering
122
+
123
+ ### Symptoms
124
+
125
+ - `yarn dev` fails on a fresh clone with
126
+ `Cannot find module '@mulmoclaude/<x>-plugin/server'` or similar.
127
+ - A workspace package's `dist/` is missing on first run.
128
+
129
+ ### Fix
130
+
131
+ ```bash
132
+ yarn build:packages # builds every shared workspace package in tier order
133
+ yarn dev # then the dev server picks them up
134
+ ```
135
+
136
+ If a specific package keeps failing, build just it:
137
+ `yarn workspace @mulmoclaude/<name> run build`. The build pipeline
138
+ runs plugins before services so cross-package imports resolve cold.
139
+
140
+ ## Plugin runtime — install / drift
141
+
142
+ ### Symptoms
143
+
144
+ - A runtime plugin shown in `/skills` doesn't load, or its routes 404.
145
+ - After upgrading the plugin host, a previously-installed plugin
146
+ reports a peer-dependency mismatch (e.g. `gui-chat-protocol`
147
+ version skew).
148
+
149
+ ### Fix
150
+
151
+ Runtime plugins are installed via tgz under `~/mulmoclaude/plugins/`
152
+ with a ledger at `plugins/plugins.json`. Reinstall the failing
153
+ plugin via the `/skills` UI to refresh both the tgz and the ledger.
154
+ A version skew on a peer dep means the plugin was built against an
155
+ older host — bump the plugin via the Discover tab's update flow.
156
+
157
+ ## Fallback
158
+
159
+ If none of the above matches the failing tool output:
160
+
161
+ 1. `ls config/helps/` to see every shipped help file.
162
+ 2. Pick the file whose name most closely matches the failing area
163
+ (`sandbox.md`, `github.md`, `feeds.md`, `presentation-deck.md`,
164
+ `mulmoscript.md`, `spreadsheet.md`, etc.) and Read it.
165
+ 3. If you find a fix there, apply it and cite the help by path in
166
+ your reply.
167
+ 4. If nothing fits, surface the raw error to the user and say
168
+ "no documented fix found in `config/helps/` — could you share more
169
+ context so we can resolve this together?" rather than silently
170
+ guessing or retrying the same command.
171
+
172
+ ## When you discover a new common error
173
+
174
+ If you resolve a new class of error that other users are likely to
175
+ hit, suggest to the user that we extend this file. Don't edit it
176
+ yourself — additions to `config/helps/error-recovery.md` are managed
177
+ by the project maintainers so the canonical copy in
178
+ `packages/core/assets/helps/error-recovery.md` and the installed copy
179
+ stay in sync.
@@ -63,6 +63,7 @@ See [Wiki](config/helps/wiki.md) for details on how it works.
63
63
  - [Spreadsheet](config/helps/spreadsheet.md) — cell format, formulas, date handling, and format codes for the presentSpreadsheet plugin
64
64
  - [presentHtml](config/helps/presenthtml.md) — self-contained HTML rules and the three-`../` relative-path convention used by the presentHtml plugin to keep generated files portable under `file://`
65
65
  - [Sandbox](config/helps/sandbox.md) — how the Docker sandbox isolates the agent, what it can access, and how to disable it
66
+ - [Error recovery](config/helps/error-recovery.md) — the lookup the agent reads on tool failures (gh/git/SSH inside the sandbox, Marp PDF, registry import, build/workspace, plugin runtime) before asking the user
66
67
  - [Telegram Bridge](config/helps/telegram.md) — how to talk to MulmoClaude from the Telegram app: creating a bot, starting the bridge, allowlisting chat IDs, commands, and troubleshooting
67
68
  - [Feeds](config/helps/feeds.md) — register a self-refreshing data feed (RSS/Atom/JSON) by authoring `feeds/<slug>/schema.json`: schema shape, the `ingest` block, raw-item field mapping, and `maxItems` retention
68
69
  - [GitHub repositories in the workspace](config/helps/github.md) — clone-destination rules under `github/<name>/` and how to handle existing directories with matching or different remotes
@@ -220,9 +220,20 @@ export interface CollectionFieldSpec {
220
220
  * record to pull from the `to` collection (e.g. `me` for the
221
221
  * singleton mc-profile). Nothing is stored on this record — the
222
222
  * embed is a display-only directive resolved at render time, so
223
- * it never appears in the list table or the edit form. Required
224
- * when type is `embed`; ignored on every other type. */
223
+ * it never appears in the list table or the edit form. Supply
224
+ * either this (a fixed target, same for every record) or
225
+ * `idField` (a per-record target) — exactly one. Ignored on every
226
+ * other type. */
225
227
  id?: string;
228
+ /** When `type === "embed"`: the name of a sibling top-level field
229
+ * whose value names the target record's primary key — letting the
230
+ * embed point at a *different* record per row (e.g. an invoice's
231
+ * `issuerId` ref selects which `profile` to embed as the
232
+ * bill-from block). The renderer reads `record[idField]` at render
233
+ * time; an absent/empty value resolves to "no record" (the same
234
+ * fail-soft as a missing fixed `id`). Mutually exclusive with `id`
235
+ * — an embed must declare exactly one. Ignored on every other type. */
236
+ idField?: string;
226
237
  /** When `type === "money"` (or `type === "derived"` with
227
238
  * `display: "money"`): a literal ISO 4217 currency code passed to
228
239
  * `Intl.NumberFormat` for display — fixed for every record. The
@@ -401,3 +412,9 @@ export interface CollectionDetail extends CollectionSummary {
401
412
  schema: CollectionSchema;
402
413
  }
403
414
  export type CollectionItem = Record<string, unknown>;
415
+ /** Resolve an `embed` field's target record id: the fixed `id`, or the value
416
+ * of the sibling `idField` on this record (empty string when neither applies
417
+ * — the caller renders that as "no record"). Pure + isomorphic so the server
418
+ * projection (`derive.ts`) and the client preview (`useCollectionRendering`)
419
+ * resolve embeds identically. */
420
+ export declare function embedTargetId(field: CollectionFieldSpec, record: CollectionItem | null): string;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_deriveAll = require("../deriveAll-VRWrs3SF.cjs");
2
+ const require_deriveAll = require("../deriveAll-Cb9rWjan.cjs");
3
3
  //#region src/collection/core/presentCollection.ts
4
4
  var TOOL_NAME = "presentCollection";
5
5
  var TOOL_DEFINITION = {
@@ -760,6 +760,7 @@ exports.daySlice = daySlice;
760
760
  exports.defangForPrompt = defangForPrompt;
761
761
  exports.deriveAll = require_deriveAll.deriveAll;
762
762
  exports.draftToRecord = draftToRecord;
763
+ exports.embedTargetId = require_deriveAll.embedTargetId;
763
764
  exports.emptyRow = emptyRow;
764
765
  exports.enumColorClasses = enumColorClasses;
765
766
  exports.enumSortValue = enumSortValue;
@@ -1,4 +1,4 @@
1
- import { a as FEED_SCHEDULES, i as AGENT_INGEST_KIND, n as resolveRowRefs, o as INGEST_KINDS, r as evaluateDerived, s as isFieldDrivenEvery, t as deriveAll } from "../deriveAll-vzIhhKBK.js";
1
+ import { a as FEED_SCHEDULES, c as isFieldDrivenEvery, i as AGENT_INGEST_KIND, n as resolveRowRefs, o as INGEST_KINDS, r as evaluateDerived, s as embedTargetId, t as deriveAll } from "../deriveAll-D3wFH4Tw.js";
2
2
  //#region src/collection/core/presentCollection.ts
3
3
  var TOOL_NAME = "presentCollection";
4
4
  var TOOL_DEFINITION = {
@@ -735,6 +735,6 @@ function defangForPrompt(value) {
735
735
  return value.replace(/[<>]/g, "").replace(/`/g, "'").replace(/\$\{/g, "$ {").replace(/\s+/g, " ").slice(0, DEFANG_MAX_LEN);
736
736
  }
737
737
  //#endregion
738
- export { AGENT_INGEST_KIND, ENUM_ALERT, ENUM_NEUTRAL, ENUM_NUDGE, FEED_SCHEDULES, INGEST_KINDS, MINUTES_PER_DAY, TOOL_DEFINITION, TOOL_NAME, actionVisible, assignLanes, boolSortValue, bucketRecords, buildMonthGrid, buildUpdatedRecord, coerceInlineValue, compareSortValues, compareYmd, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, draftToRecord, emptyRow, enumColorClasses, enumSortValue, enumValueIndex, errorMessage, evaluateDerived, executePresentCollection, fieldVisible, firstMissingRequiredField, isFieldDrivenEvery, isSortableField, itemIdOf, itemLabelOf, labelFieldFor, monthAnchorDate, nextSortDirection, numericSortValue, parseIsoDate, parseIsoDateTime, parseTimeRange, recordSpan, resolveEnumColor, resolveRowRefs, rowFromItem, shortHexId, sortItems, spanCoversDay, stringSortValue, whenMatches, ymdKey };
738
+ export { AGENT_INGEST_KIND, ENUM_ALERT, ENUM_NEUTRAL, ENUM_NUDGE, FEED_SCHEDULES, INGEST_KINDS, MINUTES_PER_DAY, TOOL_DEFINITION, TOOL_NAME, actionVisible, assignLanes, boolSortValue, bucketRecords, buildMonthGrid, buildUpdatedRecord, coerceInlineValue, compareSortValues, compareYmd, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, draftToRecord, embedTargetId, emptyRow, enumColorClasses, enumSortValue, enumValueIndex, errorMessage, evaluateDerived, executePresentCollection, fieldVisible, firstMissingRequiredField, isFieldDrivenEvery, isSortableField, itemIdOf, itemLabelOf, labelFieldFor, monthAnchorDate, nextSortDirection, numericSortValue, parseIsoDate, parseIsoDateTime, parseTimeRange, recordSpan, resolveEnumColor, resolveRowRefs, rowFromItem, shortHexId, sortItems, spanCoversDay, stringSortValue, whenMatches, ymdKey };
739
739
 
740
740
  //# sourceMappingURL=index.js.map
@@ -32,6 +32,7 @@ export declare const CollectionSchemaZ: z.ZodObject<{
32
32
  required: z.ZodOptional<z.ZodBoolean>;
33
33
  to: z.ZodOptional<z.ZodString>;
34
34
  id: z.ZodOptional<z.ZodString>;
35
+ idField: z.ZodOptional<z.ZodString>;
35
36
  currency: z.ZodOptional<z.ZodString>;
36
37
  currencyField: z.ZodOptional<z.ZodString>;
37
38
  values: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_server = require("../../server-DAyIQ5rT.cjs");
2
+ const require_server = require("../../server-BjXvqGrE.cjs");
3
3
  const require_collection_paths = require("../paths.cjs");
4
4
  exports.COMPUTED_TYPES = require_server.COMPUTED_TYPES;
5
5
  exports.CollectionSchemaZ = require_server.CollectionSchemaZ;
@@ -1,3 +1,3 @@
1
- import { A as readItem, B as safeRecordId, C as buildActionSeedPrompt, D as listItems, E as generateItemId, F as isContainedInRoot, G as publishCollectionChange, H as configureCollectionHost, I as isContainedInWorkspace, K as setCollectionChangePublisher, L as itemFilePath, M as resolveCreateItemId, N as writeItem, O as readCustomViewHtml, P as SCHEMA_FILE, R as resolveDataDir, S as validateRecordObject, T as deleteItem, U as getWorkspaceRoot, V as safeSlugName, W as log, _ as loadCollection, a as computeSuccessor, b as COMPUTED_TYPES, c as isTriggerDue, d as resolveEvery, f as successorId, g as discoverCollections, h as acceptParsedSchema, i as advanceTriggerDate, j as readSkillTemplate, k as readCustomViewI18n, l as maybeSpawnSuccessor, m as CollectionSchemaZ, n as deleteCollection, o as daysInMonth, p as enrichItems, r as deleteCollectionRefusalMessage, s as formatCivil, t as deleteCustomView, u as parseCivil, v as toDetail, w as buildCollectionActionSeedPrompt, x as validateCollectionRecords, y as toSummary, z as resolveTemplatePath } from "../../server-D2QZuMS2.js";
1
+ import { A as readItem, B as safeRecordId, C as buildActionSeedPrompt, D as listItems, E as generateItemId, F as isContainedInRoot, G as publishCollectionChange, H as configureCollectionHost, I as isContainedInWorkspace, K as setCollectionChangePublisher, L as itemFilePath, M as resolveCreateItemId, N as writeItem, O as readCustomViewHtml, P as SCHEMA_FILE, R as resolveDataDir, S as validateRecordObject, T as deleteItem, U as getWorkspaceRoot, V as safeSlugName, W as log, _ as loadCollection, a as computeSuccessor, b as COMPUTED_TYPES, c as isTriggerDue, d as resolveEvery, f as successorId, g as discoverCollections, h as acceptParsedSchema, i as advanceTriggerDate, j as readSkillTemplate, k as readCustomViewI18n, l as maybeSpawnSuccessor, m as CollectionSchemaZ, n as deleteCollection, o as daysInMonth, p as enrichItems, r as deleteCollectionRefusalMessage, s as formatCivil, t as deleteCustomView, u as parseCivil, v as toDetail, w as buildCollectionActionSeedPrompt, x as validateCollectionRecords, y as toSummary, z as resolveTemplatePath } from "../../server-CRlmOBVQ.js";
2
2
  import { isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath } from "../paths.js";
3
3
  export { COMPUTED_TYPES, CollectionSchemaZ, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, buildActionSeedPrompt, buildCollectionActionSeedPrompt, computeSuccessor, configureCollectionHost, daysInMonth, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, enrichItems, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readSkillTemplate, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, setCollectionChangePublisher, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_collection_index = require("../collection/index.cjs");
3
- const require_server = require("../server-DAyIQ5rT.cjs");
3
+ const require_server = require("../server-BjXvqGrE.cjs");
4
4
  const require_notifier = require("../notifier-bS8IEeLA.cjs");
5
5
  let node_fs = require("node:fs");
6
6
  let node_fs_promises = require("node:fs/promises");
@@ -1,5 +1,5 @@
1
1
  import { whenMatches } from "../collection/index.js";
2
- import { A as readItem, D as listItems, _ as loadCollection, c as isTriggerDue, g as discoverCollections, l as maybeSpawnSuccessor } from "../server-D2QZuMS2.js";
2
+ import { A as readItem, D as listItems, _ as loadCollection, c as isTriggerDue, g as discoverCollections, l as maybeSpawnSuccessor } from "../server-CRlmOBVQ.js";
3
3
  import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-ChpY0XrY.js";
4
4
  import { watch } from "node:fs";
5
5
  import { mkdir } from "node:fs/promises";
@@ -27,6 +27,16 @@ var FEED_SCHEDULES = [
27
27
  function isFieldDrivenEvery(every) {
28
28
  return "fromField" in every;
29
29
  }
30
+ /** Resolve an `embed` field's target record id: the fixed `id`, or the value
31
+ * of the sibling `idField` on this record (empty string when neither applies
32
+ * — the caller renders that as "no record"). Pure + isomorphic so the server
33
+ * projection (`derive.ts`) and the client preview (`useCollectionRendering`)
34
+ * resolve embeds identically. */
35
+ function embedTargetId(field, record) {
36
+ if (field.id) return field.id;
37
+ if (field.idField && record) return String(record[field.idField] ?? "");
38
+ return "";
39
+ }
30
40
  //#endregion
31
41
  //#region src/collection/core/derivedFormula.ts
32
42
  function evaluateDerived(formula, ctx) {
@@ -391,6 +401,12 @@ Object.defineProperty(exports, "deriveAll", {
391
401
  return deriveAll;
392
402
  }
393
403
  });
404
+ Object.defineProperty(exports, "embedTargetId", {
405
+ enumerable: true,
406
+ get: function() {
407
+ return embedTargetId;
408
+ }
409
+ });
394
410
  Object.defineProperty(exports, "evaluateDerived", {
395
411
  enumerable: true,
396
412
  get: function() {
@@ -410,4 +426,4 @@ Object.defineProperty(exports, "resolveRowRefs", {
410
426
  }
411
427
  });
412
428
 
413
- //# sourceMappingURL=deriveAll-VRWrs3SF.cjs.map
429
+ //# sourceMappingURL=deriveAll-Cb9rWjan.cjs.map