@plinth-music/cli 0.17.0 → 0.19.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,77 @@
2
2
 
3
3
  Notable changes to `@plinth-music/cli`. Grouped by what a user notices, not by PR.
4
4
 
5
+ ## 0.19.0 - 2026-09-06
6
+
7
+ One code PR since `0.18.0` (`v0.18.0..1062fb2`, #177; #175 and #176 docs). A minor because
8
+ every member session is told something different, and one of the things it used to be told
9
+ was a call the server now refuses.
10
+
11
+ ⚠️ **Your sessions stop being taught a memory-fact write that Plinth rejects.** `source:
12
+ unknown` was the instruction in four places — the injected grounding, `/plinth:close`'s
13
+ routing step, the artist scaffold that command writes, and both `create_memory_fact` /
14
+ `create_user_memory_fact` call descriptions. Plinth's MCP server now requires a real `source`
15
+ on every memory-fact write and refuses one without it, so an agent following Plinth's own
16
+ instruction got an error back and the fact was not banked. All four say the same thing now:
17
+ `source` is required, and if you cannot name where a fact came from, ask the member or do not
18
+ bank it — never invent one.
19
+
20
+ ⚠️ **The instruction for banking a fact no longer contradicts itself.** The grounding block
21
+ told every workspace to bank a fact by adding a line to the right index. On a workspace whose
22
+ memory store is generated, a hand-written line is set aside rather than saved — so that
23
+ workspace was shipped "add a line" and "do not add a line to either file" in the same
24
+ paragraph. The line-writing route and the tool route are now stated separately, each naming
25
+ only the stores it actually applies to, and the two stores can be in different modes without
26
+ either sentence going wrong.
27
+
28
+ - **`/plinth:close`'s guidance for an artist's memory file now renders on every workspace.**
29
+ It was gated on whether the workspace or member store was generated, which says nothing
30
+ about an artist's — those flip per artist. On a workspace where an artist store is generated
31
+ and the other two are not, the guidance did not render at all and an agent hand-edited a
32
+ file whose edits are set aside.
33
+ - The close skill's dated-recall wording no longer claims a memory line carries a date when
34
+ the file does not carry one.
35
+ - The injected grounding block grows 175 bytes (12,914 to 13,089). On a real workspace at the
36
+ ceiling the cost is smaller than that, because part of the growth is a clause only a
37
+ hand-written store renders: measured on This Fiction against the live files, the injection
38
+ goes **19,967 to 19,979 bytes of the 20,000 budget, and no memory fact is displaced** — 15
39
+ of 26 workspace facts and 7 of 10 member facts ride into the session head on both builds.
40
+
41
+ The desktop app pins `@plinth-music/cli` exact (0.17.0 as of v1.10.0), so neither this release
42
+ nor 0.18.0 reaches the app until its own release repins.
43
+
44
+ ## 0.18.0 - 2026-09-04
45
+
46
+ Three code PRs since `0.17.0` (`v0.17.0..3ece28b`, #170, #171, #173; #169 and #172 docs).
47
+ A minor because every member session is now told something different, and the generated
48
+ deny list refuses 32 tool names it did not refuse before.
49
+
50
+ ⚠️ **Your sessions are no longer told that writing a calendar date is impossible.** Plinth's
51
+ MCP server gained `preview_` / `create_` / `update_calendar_entry` on 3 September, and 0.17.0
52
+ shipped injected grounding and command files that denied any such route existed — a false
53
+ denial an agent acts on by giving up and leaving the date wrong. The grounding block and both
54
+ `/plinth:start` and `/plinth:close` now name the route and the order it runs in: propose with
55
+ `preview_calendar_entry`, put the proposal to a person, and only once they agree pass the
56
+ token it minted to the writer. Agreement comes before the token in every one of those
57
+ sentences, deliberately — nothing outside the desktop cockpit asks a human anything, so on a
58
+ plain install that instruction is the whole gate.
59
+
60
+ ⚠️ **Calendar deletion and cancellation are refused again, on every MCP server: the deny list
61
+ goes 91 to 123 patterns.** The rename that opened the sanctioned write path also retired a
62
+ guarantee, because the old patterns were anchored on the noun `event` — so
63
+ `delete_calendar_entry` on anyone's server was refused by nothing. Thirty-two patterns
64
+ anchored on `calendar` and `entry` restore it. Two consequences worth knowing: this refuses a
65
+ Plinth `delete_calendar_entry` too (none exists and none is planned, so if one ever ships it
66
+ arrives denied rather than surprising somebody), and `delete` matches inside `deleted`, so a
67
+ `get_deleted_calendar_entry` would be refused as well.
68
+
69
+ - The injected grounding block grows 215 bytes (12,699 to 12,914). The injection budget is
70
+ fixed, so on a workspace already at it those bytes come out of the memory facts that ride
71
+ into the session head. The facts themselves are untouched on disk and still readable.
72
+
73
+ The desktop app pins `@plinth-music/cli` exact (0.17.0 as of v1.10.0) and picks none of this
74
+ up until its own release repins.
75
+
5
76
  ## 0.17.0 - 2026-09-03
6
77
 
7
78
  One code PR since `0.16.0` (`v0.16.0..a6209c8`, #165; #167 is tests and comments,
package/README.md CHANGED
@@ -29,7 +29,7 @@ plinth start # run the daemon: continuous pull + watch + pu
29
29
  - `plinth confirm` — review and release destructive batches the daemon has quarantined (apply, or `--discard`).
30
30
  - `plinth refresh-context [--workspace <slug>]` — force-regenerate the workspace-root `CLAUDE.md` **and the two generated commands** from the current Plinth schema + workspace. The user-customisable block between the `<!-- BEGIN: user-customisable -->` / `<!-- END -->` markers is always preserved verbatim in every generated file; `CLAUDE.local.md` is never touched.
31
31
  - `plinth grounding` — print the session-start grounding block (current date, **who the member is**, entity resolution, write confirmation, workspace rules, workspace memory, user memory). It opens by naming the member: display name, email, role, workspace and machine timezone, and that tasks the agent creates are assigned to them unless told otherwise. That renders from a principal **persisted at login**, not a call at render time — this command runs on every session start, so a round trip would tax all of them and fail offline. An existing login self-heals on the next sync, so no re-login is needed, and a server that does not yet carry `role` renders a shorter true line rather than a dangling clause. Invoked by the generated Claude Code SessionStart hook. **Bounded at 20,000 bytes total.** Only the two memory indexes give: a generated safety primitive is never cut, and if the generated block alone exceeded the budget the indexes collapse to a pointer and the block goes over rather than losing a rule. An index that does not fit whole keeps the **most recently banked** facts, by the date each line carries rather than by position — `workspace-memory.md` supersedes in place, so an updated line keeps its original slot and position does not track age — and renders them in the file's own order, with a note saying how many of how many are shown. **A fact that does not fit whole is named by its title** — its own first sentence, a boundary the member already wrote, with its `[[pointer]]` kept — so the agent knows the fact exists and where to read it. ⚠️ **Naming is preferred over carrying, and that trades depth for breadth:** the naming pass runs first and packs, and whole facts compete for what is left, so on a full index no fact may ride whole at all. A title pays in proportion to how well the hook was written; a fact whose first sentence runs long compresses barely at all, which is why *shorten each fact's first sentence* buys room where *trim the file* does not. `voice.md` is a **pointer, not a payload**: it is read on demand when drafting, never injected, which is what took a live workspace's block from 43,578 bytes to 19,911.
32
- - `plinth voice-gate` — gate agent-originated copy against the workspace voice rubric. **No longer mounted by the mirror generator** (20 Aug 2026): a generated mirror carries no PreToolUse hook, voice is carried as an instruction by `workspace-rules.md`, and the outbound boundary is a `permissions.deny` list in the generated `.claude/settings.json` that refuses the **send, reply, forward, calendar-write and mail-deletion verbs on any mounted MCP**, not seven tool names — each verb anchored at a word boundary in four positions (start, after `_`, after `-`, and at a camel hump), which is what catches `outlook_send_message` and `send-mail` while leaving `resend_verification` and `list_sent` alone. Spam is the one family that is *not* anchored, and deliberately: no English word contains `spam` as a substring, so the unanchored form has nothing to over-reach into and catches `reportSpam` and `move_to_spam` as well as `mark_message_spam`. Deletion joined on 30 Aug 2026, taking the list from 61 patterns to 91: `trash` bare (nothing reads by *starting* with it, and `empty_trash` falls out for free), `delete` against the `message` / `thread` / `mail` nouns (bare, it would refuse every `mcp__plinth__delete_*` tool), and spam-marking, which Gmail purges after thirty days and is therefore deletion on a fuse. **Drafting, filing, labelling and reading are untouched** — `outlook_send_draft` is denied while `outlook_create_draft` is not, because the discriminator is the verb, never the noun, and archiving a message is a move rather than a deletion. **Two costs are disclosed rather than discovered:** a few reads are caught by name (`getForwardingSettings`, a `list_trash`), and the bare `trash` family also refuses Google Drive's `trash_file` and the only route the live Gmail mount has for discarding a staged draft — that connector exposes no `delete_draft`, so a wrong draft is repaired with `update_draft` instead. Ten holes are recorded as assertions in `tests/context-session-hook.test.ts` rather than as prose — deletion-by-move and Gmail's own label route among them, the latter being the widest, since Gmail expresses TRASH and SPAM as labels and labelling stays allowed. The list is an enumeration and the tests say so out loud. The command still ships and still works, for a hand-wired mount: `--hook` runs it as a PreToolUse hook that blocks non-compliant Gmail drafts; direct mode takes `--file <path>` or stdin and prints the verdict (`--json` for the raw response).
32
+ - `plinth voice-gate` — gate agent-originated copy against the workspace voice rubric. **No longer mounted by the mirror generator** (20 Aug 2026): a generated mirror carries no PreToolUse hook, voice is carried as an instruction by `workspace-rules.md`, and the outbound boundary is a `permissions.deny` list in the generated `.claude/settings.json` that refuses the **send, reply, forward, calendar and mail-deletion verbs on any mounted MCP**, not seven tool names — each verb anchored at a word boundary in four positions (start, after `_`, after `-`, and at a camel hump), which is what catches `outlook_send_message` and `send-mail` while leaving `resend_verification` and `list_sent` alone. Spam is the one family that is *not* anchored, and deliberately: no English word contains `spam` as a substring, so the unanchored form has nothing to over-reach into and catches `reportSpam` and `move_to_spam` as well as `mark_message_spam`. Deletion joined on 30 Aug 2026, taking the list from 61 patterns to 91: `trash` bare (nothing reads by *starting* with it, and `empty_trash` falls out for free), `delete` against the `message` / `thread` / `mail` nouns (bare, it would refuse every `mcp__plinth__delete_*` tool), and spam-marking, which Gmail purges after thirty days and is therefore deletion on a fuse. **Plinth's own calendar-write route is deliberately outside the list** (4 Sept 2026). The server renamed those tools to `preview_` / `create_` / `update_calendar_entry` on 3 September, and because the deny dialect has no negation an exception cannot be written as a rule — the carve-out *is* the name. The same rename retired a guarantee, since the calendar families were anchored on the noun `event` and `delete_calendar_entry` on anyone's server was then refused by nothing, so the list went from 91 patterns to **123**: `delete` and `cancel` anchored on `calendar` and `entry`, which refuses calendar deletion on every server again, Plinth's own included. **Drafting, filing, labelling and reading are untouched** — `outlook_send_draft` is denied while `outlook_create_draft` is not, because the discriminator is the verb, never the noun, and archiving a message is a move rather than a deletion. **Two costs are disclosed rather than discovered:** a few reads are caught by name (`getForwardingSettings`, a `list_trash`), and the bare `trash` family also refuses Google Drive's `trash_file` and the only route the live Gmail mount has for discarding a staged draft — that connector exposes no `delete_draft`, so a wrong draft is repaired with `update_draft` instead. Ten holes are recorded as assertions in `tests/context-session-hook.test.ts` rather than as prose — deletion-by-move and Gmail's own label route among them, the latter being the widest, since Gmail expresses TRASH and SPAM as labels and labelling stays allowed. The list is an enumeration and the tests say so out loud. The command still ships and still works, for a hand-wired mount: `--hook` runs it as a PreToolUse hook that blocks non-compliant Gmail drafts; direct mode takes `--file <path>` or stdin and prints the verdict (`--json` for the raw response).
33
33
  - `plinth backlinks <target>` — report what points at a target, by reading the local mirror rather than making a network call. `<target>` can be a mirror path (`projects/breadcrumb-trail.md` — including a mirror-root file like `CLAUDE.md`), a bare name (`breadcrumb-trail`), or a wikilink target as written (`Aligned Timeline - 4 June 2026`). Output is file paths with line numbers and the link as written; `--paths-only` emits deduped paths for piping, and stdout carries the answer ALONE — withheld rows never reach the pipe, whatever the display flags say. **Every narrowing is disclosed, and the counts print even when they are zero**, because a filter the caller cannot see is indistinguishable from an empty corpus, and a disclosure that only appears when it bites gives a reader no baseline to judge it against. Two footers carry them: links withheld from the answer (daemon-rendered marker blocks, code spans / HTML comments, and links to a bare name several files answer to), and the corpus line — which workspace was searched, how much of the mirror was read against its full size, and what went unread: `this-fiction · 859 of 880 files scanned (21 non-markdown, 0 unreadable) in 34ms`. `--unfiltered` shows the withheld rows, each tagged with the narrowing that hid it. Non-markdown files are not scanned for links but **do** count as existing, so asking about a PDF in `files/` answers rather than denying it. Exit codes: `0` results found or the target file exists, `1` no such target and nothing links to that name, `2` a bare name several files answer to (it lists them rather than guessing — and every candidate it prints resolves when pasted back), `3` no active workspace, an unknown or malformed workspace slug, or a mirror with nothing to search.
34
34
  - `plinth declare <path> --scratchpad <dir> [--label=<text>]` — declare a file in the session scratchpad as a work product, so it appears in the cockpit's work-products panel instead of being lost with the session. Appends one line to an append-only `.plinth-deliverables.jsonl` manifest inside `<dir>`; the manifest records the **path, never the content**, so the panel always points at the live file rather than a snapshot that can go stale. Grounding primitive (j) instructs the agent to run this the moment it writes a deliverable whose only home is the scratchpad. **The scratchpad directory is required and never inferred** (`--scratchpad`, or `PLINTH_SCRATCHPAD_DIR`): this repo learns it from neither the app that spawns the terminal nor the harness that told the agent, so a guessed root would silently widen the containment check every refusal rests on. Refuses, with exit 1, anything outside that directory — including a symlink whose target is outside it — plus a directory, a file that does not exist yet, and a missing root.
35
35
  - `plinth review-tier [--repo <path>] [--base <ref>]` — print the code-review tier the current diff earns, `low` or `high`, for use as `/code-review $(plinth review-tier)`. Reads the diff against the merge base — committed, uncommitted **and untracked**, since a brand-new never-added file is invisible to `git diff` and would otherwise be classified as absent — and routes `high` on the risk classes the review rubric names: auth/RLS, optimistic concurrency/CAS, financial math, data-moving migrations, and large **and** multi-subsystem together. **It is a router, not a ceiling**: size alone never escalates, because a blanket cap would kill the concurrency reviews that earn their keep. **The output contract is the safety story.** It is invoked inside `$( )`, where an empty stdout or a non-zero exit would collapse the caller to an unqualified `/code-review` that silently inherits whatever global effort is configured — safe by accident, and indistinguishable from this working. So stdout carries **exactly one token and nothing else**, the exit code is **always 0**, and the reasoning goes **unconditionally to stderr** on both tiers. Anything it cannot classify — not a git repo, no merge base, no default branch, an empty diff — prints `high` and says why on stderr.
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "bc49180c1d2e3f217f3eedda18bd075870cc36aa",
2
+ "sha": "3f94e18ba675aa091a366eb8e57bcb3b8bd04b30",
3
3
  "dirty": false,
4
- "built_at": "2026-09-03T21:05:22.806Z"
4
+ "built_at": "2026-09-06T19:15:56.134Z"
5
5
  }
package/dist/cli.js CHANGED
@@ -7584,7 +7584,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
7584
7584
  // package.json
7585
7585
  var package_default = {
7586
7586
  name: "@plinth-music/cli",
7587
- version: "0.17.0",
7587
+ version: "0.19.0",
7588
7588
  description: "Plinth capstone sync daemon and CLI",
7589
7589
  type: "module",
7590
7590
  private: false,
@@ -27072,7 +27072,7 @@ function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemor
27072
27072
  add("grounding", `Plinth session grounding:`, ``, `Today's date: ${today}. You have no inherent sense of the current date and ` + `your training has a knowledge cutoff, so treat this injected date as ` + `authoritative for anything time-relative: deadlines, what counts as ` + `"recent" or "upcoming", and the age of any data or of your own knowledge.`);
27073
27073
  if (identity !== null)
27074
27074
  add("identity", ``, identity);
27075
- add("grounding", ``, `Resolve Plinth entities before answering about them. For any answer grounded ` + `in a specific artist, project, task, or other workspace entity, first look ` + `the name up in THIS workspace with the Plinth MCP tools ` + `(\`mcp__plinth__search_entities\`, \`mcp__plinth__list_artists\`, or ` + `\`mcp__plinth__list_projects\`) and ground your answer in what they return. ` + `Do not answer from name recognition alone: a globally famous name may not ` + `exist in this workspace, and an entity here may differ from the public ` + `figure of the same name. If the lookup returns nothing, say so instead of ` + `inventing details.`, ``, `Plan before you write. The first time you call any mutating Plinth MCP tool ` + `in a session (create_*, update_*, delete_*, mark_*: anything that changes ` + `workspace data), do not commit silently: state exactly what you would write, ` + `naming the tool, the entity, and the fields and values, and proceed only after ` + `the user confirms or amends.`, ``, `Write through the canonical surface. In this cockpit the file tree is your edit ` + `surface, so reach for it before the most direct tool. Change prose (document ` + `bodies, free-form notes, anything textual) by editing the mirror markdown ` + `file; the daemon syncs it up. An artist's master context is the clearest case: ` + `it lives in the body of that artist's \`artists/<slug>/_artist.md\` mirror file, ` + `the file the web context pane renders, and one of only two files under ` + `\`artists/<slug>/\` the daemon syncs both ways (\`memory.md\` is the other), ` + `so change it by editing that body, never by creating ` + `a separate document or context entity (e.g. \`create_document\`) the pane won't ` + `render. One carve-out inside that file: a block labelled as rendered from ` + `Plinth is regenerated on every sync and your edits to it are discarded, so ` + `write those through the tool the block names. Change structured fields (task ` + `status, priority, assignees, due dates, a project's category) through the ` + `Plinth MCP (\`mcp__plinth__*\`) or the task file's frontmatter, which is ` + `row-canonical but still written through the validated API. Make a bulk or ` + `destructive change (a mass delete, a gutting overwrite) by editing the file ` + `path, so the daemon's confirm-gate can quarantine it for \`plinth confirm\`. ` + `Never write the database directly, which rules out raw SQL and ` + `\`execute_sql\`, \`apply_migration\` and \`psql\`: those bypass the daemon, the ` + `mirror, the confirm-gate, canonicality, and API validation.`, ``, `Dates belong on the calendar, not in \`key_dates\`. The date surface for an artist ` + `is their mapped Google Calendar; the \`key_dates\` table is retired. Reading ` + `dates is yours, and this mirror already holds them: the daemon projects every ` + `mapped calendar into \`views/calendar/upcoming.md\` and ` + `\`views/calendar/<slug>.md\`, so read those before reaching for a tool ` + `(\`list_artist_calendars\`). They are derived and overwritten, ` + `so they are current only as of the \`generated_at\` in their header - quote that ` + `stamp, never edit them, and check the status line, because a calendar Plinth ` + `could not reach is unknown rather than empty. When a tool is the only way, ` + `resolve the calendar through \`mcp__plinth__list_artist_calendars\` and use its ` + `\`calendar_id\` field, not the mapping row's \`id\`, because the default ` + `\`primary\` is almost never theirs.`, ``, `Writing a date is not yours, by any route: not a calendar tool, and not a ` + `shell command line either, the Google Workspace one included. The calendar ` + `tools are refused for you where Plinth has configured this workspace. ` + `Nothing refuses a shell, anywhere, and where Plinth has not configured ` + `this workspace nothing is refused at all: you are the only thing holding ` + `those. The way ` + `through is a person, not a workaround, so when a date needs adding or ` + `correcting say plainly what the change is, say that it goes in through ` + `Plinth's calendar view, and say that a person makes it. Say why, too: a date ` + `left looking handled is worse than one openly outstanding, and a calendar ` + `write goes out to everyone invited the moment it lands and cannot be recalled.`, ``, `Draft email. Never send it. Writing the draft is yours: use the draft tools ` + `(\`create_draft\`, \`update_draft\`), then say where the draft is and what it ` + `says, so the person can read it and send it in their own name. Sending, ` + `replying, forwarding and deleting are not yours, by any route: not a mail tool, and ` + `not a shell command line either. The mail tools are refused for you where ` + `Plinth has configured this workspace. Nothing refuses a shell, anywhere, ` + `and where Plinth has not configured this workspace nothing is refused at ` + `all: you are the only thing holding those. This is not a comment on your ` + `writing. It is that a message which has left the building cannot be recalled, ` + `and one deleted out of a mailbox is not yours to lose; the person whose name ` + `is on it is the one who decides.`);
27075
+ add("grounding", ``, `Resolve Plinth entities before answering about them. For any answer grounded ` + `in a specific artist, project, task, or other workspace entity, first look ` + `the name up in THIS workspace with the Plinth MCP tools ` + `(\`mcp__plinth__search_entities\`, \`mcp__plinth__list_artists\`, or ` + `\`mcp__plinth__list_projects\`) and ground your answer in what they return. ` + `Do not answer from name recognition alone: a globally famous name may not ` + `exist in this workspace, and an entity here may differ from the public ` + `figure of the same name. If the lookup returns nothing, say so instead of ` + `inventing details.`, ``, `Plan before you write. The first time you call any mutating Plinth MCP tool ` + `in a session (create_*, update_*, delete_*, mark_*: anything that changes ` + `workspace data), do not commit silently: state exactly what you would write, ` + `naming the tool, the entity, and the fields and values, and proceed only after ` + `the user confirms or amends.`, ``, `Write through the canonical surface. In this cockpit the file tree is your edit ` + `surface, so reach for it before the most direct tool. Change prose (document ` + `bodies, free-form notes, anything textual) by editing the mirror markdown ` + `file; the daemon syncs it up. An artist's master context is the clearest case: ` + `it lives in the body of that artist's \`artists/<slug>/_artist.md\` mirror file, ` + `the file the web context pane renders, and one of only two files under ` + `\`artists/<slug>/\` the daemon syncs both ways (\`memory.md\` is the other), ` + `so change it by editing that body, never by creating ` + `a separate document or context entity (e.g. \`create_document\`) the pane won't ` + `render. One carve-out inside that file: a block labelled as rendered from ` + `Plinth is regenerated on every sync and your edits to it are discarded, so ` + `write those through the tool the block names. Change structured fields (task ` + `status, priority, assignees, due dates, a project's category) through the ` + `Plinth MCP (\`mcp__plinth__*\`) or the task file's frontmatter, which is ` + `row-canonical but still written through the validated API. Make a bulk or ` + `destructive change (a mass delete, a gutting overwrite) by editing the file ` + `path, so the daemon's confirm-gate can quarantine it for \`plinth confirm\`. ` + `Never write the database directly, which rules out raw SQL and ` + `\`execute_sql\`, \`apply_migration\` and \`psql\`: those bypass the daemon, the ` + `mirror, the confirm-gate, canonicality, and API validation.`, ``, `Dates belong on the calendar, not in \`key_dates\`. The date surface for an artist ` + `is their mapped Google Calendar; the \`key_dates\` table is retired. Reading ` + `dates is yours, and this mirror already holds them: the daemon projects every ` + `mapped calendar into \`views/calendar/upcoming.md\` and ` + `\`views/calendar/<slug>.md\`, so read those before reaching for a tool ` + `(\`list_artist_calendars\`). They are derived and overwritten, ` + `so they are current only as of the \`generated_at\` in their header - quote that ` + `stamp, never edit them, and check the status line, because a calendar Plinth ` + `could not reach is unknown rather than empty. When a tool is the only way, ` + `resolve the calendar through \`mcp__plinth__list_artist_calendars\` and use its ` + `\`calendar_id\` field, not the mapping row's \`id\`, because the default ` + `\`primary\` is almost never theirs.`, ``, `Writing a date is not yours to do unasked, and the route is Plinth's own: ` + `propose it with \`preview_calendar_entry\`, put that proposal to the person, ` + `and only once they agree pass the token it minted for exactly that proposal ` + `to \`create_calendar_entry\` or \`update_calendar_entry\`. Nothing else writes ` + `one: ` + `no other calendar tool, and not a shell command line either, the ` + `Google Workspace one included. ` + `Nothing refuses a shell, anywhere, and where Plinth has not configured ` + `this workspace nothing is refused at all: you are the only thing holding ` + `those. Where no calendar is mapped, or a token is refused, that is not a ` + `wall. The way ` + `through is a person, not a workaround, so ` + `say plainly what the change is, say that it goes in through ` + `Plinth's calendar view, and say that a person makes it. Say why, too: a date ` + `left looking handled is worse than one openly outstanding, and a calendar ` + `write goes out to everyone invited the moment it lands and cannot be recalled.`, ``, `Draft email. Never send it. Writing the draft is yours: use the draft tools ` + `(\`create_draft\`, \`update_draft\`), then say where the draft is and what it ` + `says, so the person can read it and send it in their own name. Sending, ` + `replying, forwarding and deleting are not yours, by any route: not a mail tool, and ` + `not a shell command line either. The mail tools are refused for you where ` + `Plinth has configured this workspace. Nothing refuses a shell, anywhere, ` + `and where Plinth has not configured this workspace nothing is refused at ` + `all: you are the only thing holding those. This is not a comment on your ` + `writing. It is that a message which has left the building cannot be recalled, ` + `and one deleted out of a mailbox is not yours to lose; the person whose name ` + `is on it is the one who decides.`);
27076
27076
  if (workspaceRules === null && voiceProfile === null) {
27077
27077
  add("voice", ``, `Check the voice yourself, because nothing else checks it now. Two files carry ` + `it: \`workspace-rules.md\` at the mirror root holds this workspace's house ` + `style, and \`${VOICE_FILENAME}\` beside it holds your own voice profile, the ` + `sign-off and the habits that belong to one member rather than the team. Read ` + `them before you write and follow them as written, rather than drafting in ` + `your own register and correcting after. Nothing inspects a draft between you ` + `and the person reading it, so the rules in those files are the whole of it.`);
27078
27078
  }
@@ -27087,7 +27087,7 @@ function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemor
27087
27087
  add("voice", ``, `That voice profile does not currently parse. Plinth read it and found ` + `its structure broken, most often a code fence that was opened and ` + `never closed, so any machine-read setting inside it is being ignored ` + `rather than applied. The prose in it is still the member's own ` + `writing and is still worth following. Treat anything that looks like ` + `a configured setting in it as NOT in force, say so if it comes up, ` + `and tell them their profile needs fixing rather than fixing it ` + `yourself.`);
27088
27088
  }
27089
27089
  }
27090
- add("grounding", ``, `Keep your memory on the Plinth surface. Your memory here IS the Plinth ` + `surface (\`workspace-memory.md\`, your own \`user-memory.md\`, and the ` + `per-artist warm stores the daemon syncs), and it is the ONLY memory you ` + `keep. Do not use any other memory store: no private notes-to-self, no ` + `separate scratch file, no tool-native memory of your own. What you learn ` + `here is recorded on the Plinth surface, or nowhere. When you learn a ` + `durable fact worth keeping, bank it by adding a line to the right index, ` + `which the daemon syncs up, following the write-back conventions in that ` + `file's header: one line, at least one \`[[pointer]]\` to where the full ` + `fact lives, an absolute date, and where you learned it. Only ever record ` + `what you have actually been taught, never public knowledge and never your ` + `own priors. The scope rule decides WHICH index, and there are ` + `THREE. A fact true of ONE ARTIST (their rate card, how their kit ` + `works, what a contact of theirs reliably does) goes in that artist's ` + `own warm store, \`artists/<slug>/memory.md\`, and not in the shared ` + `index: putting it there taxes every teammate who never touches that ` + `artist and hides it from the one who does. A fact that holds for the ` + `whole workspace (a project, a working convention, a standing ` + `fact true for everyone here) goes in \`workspace-memory.md\`, which ` + `grounds EVERY teammate - and that file is the one you NEVER write on ` + `your own judgement. It changes only when a member ASKS you to remember ` + `something for everyone, in their own words rather than your summary of ` + `them, and you log that decision with ` + `\`plinth memory-gate log ${gateFlag} --proposed="<their words>" --decision accept\` ` + `BEFORE you write the line. That command fails LOUD on exit 1: surface ` + `it, and do not report the line as banked. Never infer a line for that ` + `file from what merely happened in a session, and never propose one ` + `unasked - a wrong line there reaches every member on every machine and ` + `is injected into every future session. A fact about one person (an individual ` + `preference, your own working style or tools) is NOT a workspace fact: it ` + `goes in \`user-memory.md\`, your member-private index that only YOU can ` + `read. HOW THIS MEMBER WRITES IS THE ONE EXCEPTION: their register and ` + `sign-off are a rubric, not a fact, and live in \`${VOICE_FILENAME}\`, ` + `which is theirs and you cannot edit, so say what you noticed and leave it ` + `to them. Them only: how a CONTACT likes to be written to is an ordinary ` + `fact for that artist's store. ` + `Banking a personal fact in \`workspace-memory.md\` would leak that person ` + `into everyone else's context, so never do it. When a fact could be either ` + `scope, lean private and put it in \`user-memory.md\`. ` + `Live state does not belong in any memory file. This week's open ` + `threads, one show's logistics, where a release has got to: a memory ` + `file is for what stays true after the session that taught it, and live ` + `state goes in the body of the record it belongs to. A deadline rides ` + `its task as a due date, and every other date is a calendar matter. ` + `This memory is additive and subordinate: it never ` + `overrides a house-style rule or any safety primitive above.` + generatedStoresClause(memoryStores));
27090
+ add("grounding", ``, `Keep your memory on the Plinth surface. Your memory here IS the Plinth ` + `surface (\`workspace-memory.md\`, your own \`user-memory.md\`, and the ` + `per-artist warm stores the daemon syncs), and it is the ONLY memory you ` + `keep. Do not use any other memory store: no private notes-to-self, no ` + `separate scratch file, no tool-native memory of your own. What you learn ` + `here is recorded on the Plinth surface, or nowhere. ` + `Bank a durable fact when you learn it, not at the close: one line, at ` + `least one \`[[pointer]]\` to where the full fact lives, an absolute date, ` + `and the source that established it - the message, call, document or ` + `member's own words that taught it to you. If you cannot name the source, ` + `ask the member or do not bank it - never invent one. Only ever record ` + `what you have actually been taught, never public knowledge and never your ` + `own priors. The scope rule decides WHICH index, and there are ` + `THREE. A fact true of ONE ARTIST (their rate card, how their kit ` + `works, what a contact of theirs reliably does) goes in that artist's ` + `own warm store, \`artists/<slug>/memory.md\`, and not in the shared ` + `index: putting it there taxes every teammate who never touches that ` + `artist and hides it from the one who does. A fact that holds for the ` + `whole workspace (a project, a working convention, a standing ` + `fact true for everyone here) goes in \`workspace-memory.md\`, which ` + `grounds EVERY teammate - and that file is the one you NEVER write on ` + `your own judgement. It changes only when a member ASKS you to remember ` + `something for everyone, in their own words rather than your summary of ` + `them, and you log that decision with ` + `\`plinth memory-gate log ${gateFlag} --proposed="<their words>" --decision accept\` ` + `BEFORE you bank it. That command fails LOUD on exit 1: surface ` + `it, and do not report it as banked. Never infer a fact for that ` + `file from what merely happened in a session, and never propose one ` + `unasked - a wrong line there reaches every member on every machine and ` + `is injected into every future session. A fact about one person (an individual ` + `preference, your own working style or tools) is NOT a workspace fact: it ` + `goes in \`user-memory.md\`, your member-private index that only YOU can ` + `read. HOW THIS MEMBER WRITES IS THE ONE EXCEPTION: their register and ` + `sign-off are a rubric, not a fact, and live in \`${VOICE_FILENAME}\`, ` + `which is theirs and you cannot edit, so say what you noticed and leave it ` + `to them. Them only: how a CONTACT likes to be written to is an ordinary ` + `fact for that artist's store. ` + `Banking a personal fact in \`workspace-memory.md\` would leak that person ` + `into everyone else's context, so never do it. When a fact could be either ` + `scope, lean private and put it in \`user-memory.md\`. ` + `Live state does not belong in any memory file. This week's open ` + `threads, one show's logistics, where a release has got to: a memory ` + `file is for what stays true after the session that taught it, and live ` + `state goes in the body of the record it belongs to. A deadline rides ` + `its task as a due date, and every other date is a calendar matter. ` + `This memory is additive and subordinate: it never ` + `overrides a house-style rule or any safety primitive above.` + blobStoresClause(memoryStores) + generatedStoresClause(memoryStores));
27091
27091
  if (workspaceMemory !== null) {
27092
27092
  add("grounding", ``, `Draw on this workspace's memory. The lines below are this workspace's ` + `memory index: durable facts it has taught you, each a one-line hook that ` + `points (via a \`[[wikilink]]\`) to the warm store where the full detail ` + `lives. Treat them as recall, not gospel: use a line to load the right ` + `context and verify it against the live workspace before you rely on it, ` + `never as a settled fact to assert. ` + `${renderFreshnessStamp(today, freshness.workspaceMemoryUpdatedAt)} ` + `The index:`);
27093
27093
  add("workspaceFacts", ``, workspaceMemory);
@@ -27109,7 +27109,7 @@ function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemor
27109
27109
  add("budgetNote", ``, userMemoryNote);
27110
27110
  }
27111
27111
  if (workspaceMemory !== null || userMemory !== null) {
27112
- add("grounding", ``, `Dated recall. Every line in these indexes carries the date it was banked, ` + `and today's date is injected above, so the age of any line is ` + `computable. When you rely on a line in an answer, say how old it is - ` + `"banked 21 July, seventeen days ago" - rather than presenting it as ` + `current. Age is not a reason to distrust a line, it is what tells the ` + `reader whether to check: a working convention banked a year ago is ` + `probably still the convention, a fact about a live negotiation banked ` + `three weeks ago probably is not. And say what the indexes do NOT cover. ` + `If you are asked about something no line here addresses, say so plainly ` + `rather than answering only from what happens to be present - an index ` + `that is silent on a subject reads exactly like one that has settled it.`);
27112
+ add("grounding", ``, `Dated recall. Each index above says what is known about its age, and a ` + `line ending \` (YYYY-MM-DD)\` carries the date that fact was banked. ` + `When you rely on a line, say how old it is - ` + `"banked 21 July, seventeen days ago" - rather than presenting it as ` + `current. Age is not a reason to distrust a line, it is what tells the ` + `reader whether to check: a working convention banked a year ago is ` + `probably still the convention, a fact about a live negotiation banked ` + `three weeks ago probably is not. And say what the indexes do NOT cover. ` + `If you are asked about something no line here addresses, say so plainly ` + `rather than answering only from what happens to be present - an index ` + `that is silent on a subject reads exactly like one that has settled it.`);
27113
27113
  }
27114
27114
  add("grounding", ``, `Declare a work product before you finish with it. Your scratchpad ` + `directory sits outside Plinth: temporary, scoped to this session, and ` + `invisible to the person who asked for the work. Prefer not to leave a ` + `deliverable there at all. If it belongs to this workspace, write it to ` + `a mirror file instead, where it syncs and is durable. When the ` + `scratchpad genuinely is a file's only home, meaning a document, ` + `profile, report or analysis that was the POINT of the work rather than ` + `a step toward it, declare it the moment you write it, with ` + `\`plinth declare "<path>" --scratchpad="<the scratchpad directory ` + `your harness told you to use>" --label='<short description>'\`. ` + `Declare only ` + `those. A file that already has another home is not a deliverable: not ` + `raw API dumps, thread JSON, helper scripts, or the body files you write ` + `for a tool that reads a path, where the durable deliverable is the ` + `draft the tool creates. Declaring everything you wrote buries the one ` + `that mattered, which is the failure this exists to prevent. ` + `Re-declaring a path replaces the earlier entry, so refine a label ` + `freely. This is about work products, not memory: durable facts still go ` + `to the index above. Like every rule here it is additive and ` + `subordinate, and never relaxes a safety primitive above.`);
27115
27115
  if (filesPastBar > 0) {
@@ -27128,12 +27128,23 @@ function composeGroundingBlock(today, workspaceRules, workspaceMemory, userMemor
27128
27128
  groundingRuleShaped: countRuleShapedLines(groundingText)
27129
27129
  };
27130
27130
  }
27131
+ function blobStoresClause(modes) {
27132
+ if (modes.workspace && modes.user)
27133
+ return "";
27134
+ if (!modes.workspace && !modes.user) {
27135
+ return ` You bank a fact by adding a line to the right index, which the daemon ` + `syncs up, following the write-back conventions in that file's header.`;
27136
+ }
27137
+ const which = modes.workspace ? `\`${USER_MEMORY_FILENAME}\`` : `\`${WORKSPACE_MEMORY_FILENAME}\``;
27138
+ return ` You bank a fact in ${which} by adding a line to it, which the daemon ` + `syncs up, following the write-back conventions in that file's header.`;
27139
+ }
27131
27140
  function generatedStoresClause(modes) {
27132
27141
  if (!anyGenerated(modes))
27133
27142
  return "";
27134
27143
  const which = modes.workspace && modes.user ? `\`${WORKSPACE_MEMORY_FILENAME}\` and \`${USER_MEMORY_FILENAME}\` are` : modes.workspace ? `\`${WORKSPACE_MEMORY_FILENAME}\` is` : `\`${USER_MEMORY_FILENAME}\` is`;
27135
- const tools = modes.user ? modes.workspace ? `\`create_user_memory_fact\` for a personal fact, which writes on one ` + `call, and \`create_memory_fact\` for a shared one, which shows you the ` + `fact first and needs a second call to save it` : `\`create_user_memory_fact\`, which writes on one call` : `\`create_memory_fact\`, which shows you the fact first and needs a ` + `second call to save it`;
27136
- return ` HOW you bank a fact has changed for this workspace: ${which} now ` + `GENERATED from memory facts, so do not add a line to ${modes.workspace && modes.user ? "either file" : "that file"} - it is rewritten from the ` + `facts on every sync. Use ${tools}. Correct a fact with ` + `\`supersede_memory_fact\` or \`delete_memory_fact\` (\`_user_\` for your own), ` + `taking the id from the fact file's frontmatter. Each fact's full text lives ` + `in \`memory/<slug>.md\`, or \`memory/private/<slug>.md\` for your own, and ` + `the index lines above link to them - read the one you need rather than the ` + `whole folder. A hand edit to a generated file does not reach Plinth: it is ` + `set aside and \`plinth status\` says where.`;
27144
+ const tools = modes.user ? modes.workspace ? `\`create_user_memory_fact\` for a personal fact, which writes on one ` + `call, and \`create_memory_fact\` for a shared one, which shows the ` + `fact to the member and stops - their yes saves it` : `\`create_user_memory_fact\`, which writes on one call` : `\`create_memory_fact\`, which shows the fact to the member and stops - ` + `their yes saves it`;
27145
+ const corrections = modes.workspace && modes.user ? `\`supersede_memory_fact\` or \`delete_memory_fact\` (\`_user_\` for your own)` : modes.workspace ? `\`supersede_memory_fact\` or \`delete_memory_fact\`` : `\`supersede_user_memory_fact\` or \`delete_user_memory_fact\``;
27146
+ const bodies = modes.workspace && modes.user ? `\`memory/<slug>.md\`, or \`memory/private/<slug>.md\` for your own` : modes.workspace ? `\`memory/<slug>.md\`` : `\`memory/private/<slug>.md\``;
27147
+ return ` HOW you bank a fact has changed${modes.workspace && modes.user ? " for this workspace" : ""}: ${which} now ` + `GENERATED from memory facts, so do not add a line to ${modes.workspace && modes.user ? "either file" : "that file"} - it is rewritten from the ` + `facts on every sync. Use ${tools}. Correct a fact with ${corrections}, ` + `taking the id from the fact file's frontmatter. Each fact's full text lives ` + `in ${bodies}, and ` + `${modes.workspace && modes.user ? "the index lines below link" : "that index's lines below link"} to them - read the one you need rather than the ` + `whole folder. A hand edit to a generated file does not reach Plinth: it is ` + `set aside and \`plinth status\` says where.`;
27137
27148
  }
27138
27149
  function buildGroundingReport(today, workspaceRules = null, workspaceMemory = null, userMemory = null, freshness = {}, rosterIsEmpty = false, voiceProfile = null, voiceMalformed = false, workspaceSlug = null, member = null, filesPastBar = 0, memoryStores = BLOB_MEMORY_STORES) {
27139
27150
  const compose = (wmBody, umBody, wmNote, umNote, wmTitles = null, umTitles = null) => composeGroundingBlock(today, workspaceRules, wmBody, umBody, freshness, rosterIsEmpty, voiceProfile, voiceMalformed, workspaceSlug, wmNote, umNote, member, wmTitles, umTitles, filesPastBar, memoryStores);
@@ -29346,8 +29357,10 @@ var MANAGED_COMMENT = [
29346
29357
  "or in .claude/settings.local.json.",
29347
29358
  "The permissions.deny entries below are managed the same way. They stop an agent",
29348
29359
  "in this workspace sending, replying to or forwarding mail, deleting or trashing",
29349
- "it, marking it spam, and writing a calendar; drafting, filing, labelling and",
29350
- "reading stay allowed. The refusal covers a draft the agent staged itself: on a",
29360
+ "it, marking it spam, and writing a calendar - create, update, delete and cancel",
29361
+ "wherever the name also carries event, plus delete and cancel wherever it carries",
29362
+ "calendar entry; drafting, filing, labelling and reading stay allowed.",
29363
+ "The refusal covers a draft the agent staged itself: on a",
29351
29364
  "Gmail mount there is no delete-draft tool, so trashing was the only way to",
29352
29365
  "discard one and it is now refused. Fix a wrong draft with update_draft, which",
29353
29366
  "keeps its thread. They match by VERB FAMILY rather than by",
@@ -29355,6 +29368,8 @@ var MANAGED_COMMENT = [
29355
29368
  "rule set, not four - and the matcher cannot express an alternation, so each",
29356
29369
  "spelling needs its own line. That width also catches a few reads whose name",
29357
29370
  "merely begins with a send verb - getForwardingSettings is the live example -",
29371
+ "and a few whose name merely CONTAINS one - a calendar read called",
29372
+ "get_deleted_calendar_entry is refused, because delete sits inside deleted -",
29358
29373
  "and it stops an agent trashing any file, not only mail: nothing here can tell",
29359
29374
  "a mailbox's trash from a drive's.",
29360
29375
  "You cannot re-open one of these here: a deny beats an allow, including yours,",
@@ -29363,6 +29378,10 @@ var MANAGED_COMMENT = [
29363
29378
  "message that has gone cannot be recalled. Nor is a deleted one yours to lose:",
29364
29379
  "it is somebody else's mailbox, spam-marking is deletion on a thirty-day fuse,",
29365
29380
  "and neither is undone by noticing afterwards.",
29381
+ "One thing is deliberately NOT on this list, and it is the only exception:",
29382
+ "Plinth's own create_calendar_entry and update_calendar_entry, the sanctioned",
29383
+ "calendar write path, which take a two-step preview-and-confirm of their own",
29384
+ "before anything reaches a calendar.",
29366
29385
  "One more entry stops an agent EDITING voice.md, the",
29367
29386
  "voice profile it is judged against: that file is yours to write and theirs to read.",
29368
29387
  "Your own allow list is untouched, and your own",
@@ -29441,6 +29460,38 @@ var MANAGED_DENY = [
29441
29460
  "mcp__*__*_cancel*Event*",
29442
29461
  "mcp__*__*-cancel*Event*",
29443
29462
  "mcp__*__*Cancel*Event*",
29463
+ "mcp__*__delete*calendar*entry*",
29464
+ "mcp__*__*_delete*calendar*entry*",
29465
+ "mcp__*__*-delete*calendar*entry*",
29466
+ "mcp__*__*Delete*calendar*entry*",
29467
+ "mcp__*__delete*calendar*Entry*",
29468
+ "mcp__*__*_delete*calendar*Entry*",
29469
+ "mcp__*__*-delete*calendar*Entry*",
29470
+ "mcp__*__*Delete*calendar*Entry*",
29471
+ "mcp__*__delete*Calendar*entry*",
29472
+ "mcp__*__*_delete*Calendar*entry*",
29473
+ "mcp__*__*-delete*Calendar*entry*",
29474
+ "mcp__*__*Delete*Calendar*entry*",
29475
+ "mcp__*__delete*Calendar*Entry*",
29476
+ "mcp__*__*_delete*Calendar*Entry*",
29477
+ "mcp__*__*-delete*Calendar*Entry*",
29478
+ "mcp__*__*Delete*Calendar*Entry*",
29479
+ "mcp__*__cancel*calendar*entry*",
29480
+ "mcp__*__*_cancel*calendar*entry*",
29481
+ "mcp__*__*-cancel*calendar*entry*",
29482
+ "mcp__*__*Cancel*calendar*entry*",
29483
+ "mcp__*__cancel*calendar*Entry*",
29484
+ "mcp__*__*_cancel*calendar*Entry*",
29485
+ "mcp__*__*-cancel*calendar*Entry*",
29486
+ "mcp__*__*Cancel*calendar*Entry*",
29487
+ "mcp__*__cancel*Calendar*entry*",
29488
+ "mcp__*__*_cancel*Calendar*entry*",
29489
+ "mcp__*__*-cancel*Calendar*entry*",
29490
+ "mcp__*__*Cancel*Calendar*entry*",
29491
+ "mcp__*__cancel*Calendar*Entry*",
29492
+ "mcp__*__*_cancel*Calendar*Entry*",
29493
+ "mcp__*__*-cancel*Calendar*Entry*",
29494
+ "mcp__*__*Cancel*Calendar*Entry*",
29444
29495
  "mcp__*__trash*",
29445
29496
  "mcp__*__*_trash*",
29446
29497
  "mcp__*__*-trash*",
@@ -29922,7 +29973,7 @@ function buildStartCommandMd(rosterIsEmpty2, offerTrim = false, stores = BLOB_ME
29922
29973
  "The SessionStart hook injects **both memory indexes**, above: the workspace index (durable facts and conventions that hold for anyone here) and your personal index (facts true of **this member**: how they work, what they prefer). They may arrive trimmed to their newest lines, and the injection says so where it happens. **Do not re-read `workspace-memory.md` or `user-memory.md` wholesale** - they are already in this context window and re-reading them is the single most expensive thing this command could do for nothing - but if the injection says an index was trimmed and you need what is missing, read that file.",
29923
29974
  "",
29924
29975
  ...anyGenerated(stores) ? [
29925
- "**Each index line links to the fact's full text; open only the one you need.** A line reads `- [[memory/<slug>]] - <description>`, and the description is a hook rather than the fact. The body is at `memory/<slug>.md`, or `memory/private/<slug>.md` for one of your own. **Do not read the folder** - that is the whole corpus, which is what the index exists to spare you.",
29976
+ `**Each index line links to the fact's full text; open only the one you need.** A line reads \`- [[memory/<slug>]] - <description>\`, and the description is a hook rather than the fact. The body is at ${stores.workspace && stores.user ? "`memory/<slug>.md`, or `memory/private/<slug>.md` for one of your own" : stores.workspace ? "`memory/<slug>.md`" : "`memory/private/<slug>.md`"}. **Do not read the folder** - that is the whole corpus, which is what the index exists to spare you.`,
29926
29977
  ""
29927
29978
  ] : [],
29928
29979
  "**Who you are working for is in the grounding above, not in an index.** The hook renders the member, their email and the workspace from the credentials this machine logged in with, so do not go looking for identity in either memory file and do not ask the user for it. **Use the personal index for what it is actually for**, which is the half that gets skipped: how this member works and what they prefer.",
@@ -29945,7 +29996,7 @@ function buildStartCommandMd(rosterIsEmpty2, offerTrim = false, stores = BLOB_ME
29945
29996
  "",
29946
29997
  "Then the calendar. It is Plinth's projection of every artist's calendar.",
29947
29998
  "",
29948
- "**This is a FILE READ, not a calendar call, and the rule in Step 2 stands unchanged: you still never open a calendar tool.** The file is here so that you do not have to.",
29999
+ "**This is a FILE READ, not a calendar call, and Step 2's budget stands unchanged: no calendar call in this brief.** The file is here so that you do not have to make one. **That is a cost rule about the opener, not a closed door** - Plinth has a calendar-write route of its own, named below. Opening a session is not when you take it.",
29949
30000
  "",
29950
30001
  '**Brief it as of its own `generated_at` stamp, and say the stamp.** A projection is a cache. It is exactly as current as the header says it is and no more, so "nothing on this week" is only ever true as of that moment.',
29951
30002
  "",
@@ -29953,7 +30004,7 @@ function buildStartCommandMd(rosterIsEmpty2, offerTrim = false, stores = BLOB_ME
29953
30004
  "",
29954
30005
  "If the file carries `fetch_state: stale`, the last refresh failed: say how old the dates are rather than reporting them as current. If the file is absent altogether, say the projection has not run yet and carry on without it.",
29955
30006
  "",
29956
- "**Dates are read here and changed by a person, in Plinth's calendar view.** If something needs moving, say what the change is and hand it over.",
30007
+ "**Dates are read here, and changing one is not part of this brief.** If something needs moving, say plainly what the change is. After this session's brief it goes one of two ways: propose it with `preview_calendar_entry`, put that proposal to the person, and only once they agree pass the token it minted to `create_calendar_entry` or `update_calendar_entry` - or hand the change to a person to make in Plinth's calendar view.",
29957
30008
  "",
29958
30009
  "## Step 4: brief, in three lines",
29959
30010
  "",
@@ -30062,7 +30113,7 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30062
30113
  "- **A note, brief or bio that is a piece of writing in its own right** is a document under `documents/`. That means standalone material somebody would open and read on its own, not a fact you are recording because you learned it - a fact is a memory line, and Step 4b routes it.",
30063
30114
  "- **A stable attribute of an artist** - their website, a handle, their bio - is that artist's own field, or the body of `artists/<slug>/_artist.md`.",
30064
30115
  "- **A contract, statement or attachment** is a file under `files/`.",
30065
- "- **A date is live state and belongs in no memory file.** A deadline rides its task as a due date. Every other date is a calendar matter: say plainly what the date is, say that a person adds it in Plinth's calendar view, and name it in the closing summary so it is not quietly dropped.",
30116
+ "- **A date is live state and belongs in no memory file.** A deadline rides its task as a due date. Every other date is a calendar matter: say plainly what the date is, and either propose it with `preview_calendar_entry`, put that proposal to the member, and only once they agree pass the token it minted to `create_calendar_entry` or `update_calendar_entry` - or hand it to a person to add in Plinth's calendar view. Name it in the closing summary either way, so it is not quietly dropped.",
30066
30117
  "",
30067
30118
  '**A typed fact with no home today is named in the closing summary as "no home yet", and written nowhere.** The case that actually occurs is a contact who belongs to several artists or to none - a publisher who routes writers across the whole roster. Do not pick one artist arbitrarily and do not flatten it into a memory line. **You cannot write it with no artist either: `create_artist_contact` requires `artist_id` or `artist_name` and errors with `Either artist_id or artist_name is required`.** So this is a real gap with no surface behind it today, not a shortcut you are declining to take. Name it in the summary and leave it unwritten; a named gap is useful, and a contact filed under an artist it does not belong to is worse than nothing.',
30068
30119
  "",
@@ -30070,7 +30121,7 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30070
30121
  "",
30071
30122
  "### Step 4b: two piles, and the artist pile comes first",
30072
30123
  "",
30073
- "**Before either pile, the gate. These four are never banked:** *raw transcript* - what it taught you may be a fact, what was said is not · *a guess, or anything only true today* - live state, which Step 3 routes · *a duplicate of a line already in the index* · *anything the generated `CLAUDE.md` or this session's grounding already states*. **Every line you do bank names its source** - who told you, or which tool read returned it - with an absolute date. **If you cannot name the source, write `source: unknown` - never infer one.**",
30124
+ "**Before either pile, the gate. These four are never banked:** *raw transcript* - what it taught you may be a fact, what was said is not · *a guess, or anything only true today* - live state, which Step 3 routes · *a duplicate of a line already in the index* · *anything the generated `CLAUDE.md` or this session's grounding already states*. **Every line you do bank carries an absolute date and names its source** - who told you, which tool read returned it, or the message, call, document or member's own words that taught it to you. **If you cannot name the source, ask the member or do not bank it - never invent one.**",
30074
30125
  "",
30075
30126
  "**Read the index before you add to it, and reconcile what disagrees.** If two lines on your subject contradict each other, **which pair it is decides what you may do**. *One is the line you are adding now*: do not add a third - edit the older line until it reads as superseded, then write yours. **It may well be a teammate's, so mark it superseded, never delete it, and name whose line you changed in the closing summary.** *Both were there before this session*: **propose the supersede and let the member confirm it**, as Step 6 does. Neither is yours and this step has no confirmation gate, so **resolving somebody else's is not yours to do unasked**.",
30076
30127
  "",
@@ -30086,11 +30137,11 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30086
30137
  "",
30087
30138
  "That artist's rate card, a contact of theirs whose behaviour you had to learn, how their kit actually works, what a venue reliably does. It syncs to that artist's Plinth record, so a teammate opening that artist gets it. **List what you wrote here in the closing summary.**",
30088
30139
  "",
30089
- ...anyGenerated(stores) ? [
30090
- "**Some artist stores are generated and some are not, so check before you write.** If `artists/<slug>/memory.md` opens with a `- [[memory/<slug>]] - <description>` list, it is generated from that artist's memory facts and a hand edit will not reach Plinth. Use `create_memory_fact` naming the artist; it shows you the fact first and needs a second call to save it, which is the same ask-first gate as the shared pile because an artist fact is a shared fact. If it reads as ordinary prose, write it directly as above.",
30140
+ ...[
30141
+ "**Some artist stores are generated and some are not, so check before you write.** If `artists/<slug>/memory.md` opens with a `- [[memory/<slug>]] - <description>` list, it is generated from that artist's memory facts and a hand edit will not reach Plinth. Use `create_memory_fact`, naming the artist and passing `source` - it is a **required** argument and a fact without a nameable one is refused, so settle the source before you call rather than after. It shows the fact to the member and stops; their yes is what saves it, because an artist fact is one a teammate reads and so is not yours to bank alone. If it reads as ordinary prose, write it directly as above.",
30091
30142
  ""
30092
- ] : [],
30093
- "**Follow the conventions in the file's own header comment**, which states them and is the version that stays current. Do not work from a copy of them here. **The source convention and the conflict order above are the exception**: they reach a header only on a file this command creates, so they hold wherever an existing header is silent.",
30143
+ ],
30144
+ "**Follow the conventions in the file's own header comment**, which states them and is the version that stays current. Do not work from a copy of them here. **The source convention and the conflict order above are the exception**: they reach a header only on a file this command creates, so they hold wherever an existing header is silent. **And where an existing header states the older convention, the source rule above wins**: an older header names a placeholder Plinth now refuses. Follow the rule above and leave the header alone.",
30094
30145
  "",
30095
30146
  "**Re-read the file at the moment you write it, not when you were planning.** The daemon can pull a `memory.md` down from Plinth at any point in a session, so a file that was missing minutes ago may exist now - and if it does, add your line to it rather than creating it over the top. This is not hypothetical: a close on 26 Aug 2026 checked, found nothing, and clobbered a copy the daemon had pulled in ninety seconds earlier.",
30096
30147
  "",
@@ -30102,7 +30153,7 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30102
30153
  "<!-- Durable facts about <Artist> that hold across sessions. One line per fact, with",
30103
30154
  " provenance and an absolute date. SUPERSEDE, do not accumulate - when a fact here",
30104
30155
  " becomes wrong, edit that line in place rather than adding a contradicting one.",
30105
- " If you cannot name the source, write `source: unknown` - never infer one.",
30156
+ " If you cannot name the source, ask the member or do not bank it - never invent one.",
30106
30157
  " When two lines disagree, this order decides which is current: a line the member",
30107
30158
  " confirmed outranks a newer one the agent wrote; then the newer line; then one",
30108
30159
  " verified against a tool read; then one repeated across sessions; mark the loser",
@@ -30115,7 +30166,7 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30115
30166
  ...stores.user ? [
30116
30167
  "#### True of this member: `create_user_memory_fact`. The file is output.",
30117
30168
  "",
30118
- "Facts true of this member: what they prefer, how they work, their own identifiers. **`user-memory.md` is GENERATED from this member's memory facts, so do not write it** - a hand edit does not reach Plinth, is set aside, and `plinth status` says where. Create the fact with `create_user_memory_fact`, which writes on one call because this pile is theirs alone; the index and the body file are rewritten from the facts on the next sync. Correct one with `supersede_user_memory_fact` or `delete_user_memory_fact`, taking the id from the fact file's frontmatter. **List what you wrote in the closing summary**, so it is visible without opening the file.",
30169
+ "Facts true of this member: what they prefer, how they work, their own identifiers. **`user-memory.md` is GENERATED from this member's memory facts, so do not write it** - a hand edit does not reach Plinth, is set aside, and `plinth status` says where. Create the fact with `create_user_memory_fact`, passing `source` - it is a **required** argument here too, and a fact without a nameable one is refused. It writes on one call because this pile is theirs alone; the index and the body file are rewritten from the facts on the next sync. Correct one with `supersede_user_memory_fact` or `delete_user_memory_fact`, taking the id from the fact file's frontmatter. **List what you wrote in the closing summary**, so it is visible without opening the file.",
30119
30170
  ""
30120
30171
  ] : [
30121
30172
  "#### True of this member: `user-memory.md` at the mirror root. Write it directly.",
@@ -30170,7 +30221,7 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30170
30221
  "Then the rules the proposal itself obeys:",
30171
30222
  "",
30172
30223
  "- **Every surviving fact appears either unchanged or beside its rewrite**, so the member is reading a diff rather than trusting a summary of one.",
30173
- "- **A fact that is now wrong is edited where it stands, not deleted** - the correction and the date it changed are the record. The file's own header comment states its conventions and is the version that stays current; follow it rather than a copy.",
30224
+ "- **A fact that is now wrong is edited where it stands, not deleted** - the correction and the date it changed are the record. The file's own header comment states its conventions and is the version that stays current; follow it rather than a copy - with the one exception named in Step 4b, where the source rule wins over an older header.",
30174
30225
  "- **Provenance and the absolute date survive on every line you keep.** A fact whose source you drop while shortening it has been damaged, not trimmed.",
30175
30226
  "- **Nothing banked in the last 30 days is dropped**, measured against the date the session hook injected. Recent facts have not had time to prove whether they were durable, and a trim is not the place to decide.",
30176
30227
  "- **Merging two facts into one is a rewrite and shows as one**, with both original lines visible in the proposal.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plinth-music/cli",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Plinth capstone sync daemon and CLI",
5
5
  "type": "module",
6
6
  "private": false,