@plinth-music/cli 0.18.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,45 @@
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
+
5
44
  ## 0.18.0 - 2026-09-04
6
45
 
7
46
  Three code PRs since `0.17.0` (`v0.17.0..3ece28b`, #170, #171, #173; #169 and #172 docs).
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "d641e2f41a54942541eeddfa55776d42e226e011",
2
+ "sha": "3f94e18ba675aa091a366eb8e57bcb3b8bd04b30",
3
3
  "dirty": false,
4
- "built_at": "2026-09-04T17:57:36.767Z"
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.18.0",
7587
+ version: "0.19.0",
7588
7588
  description: "Plinth capstone sync daemon and CLI",
7589
7589
  type: "module",
7590
7590
  private: false,
@@ -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);
@@ -29962,7 +29973,7 @@ function buildStartCommandMd(rosterIsEmpty2, offerTrim = false, stores = BLOB_ME
29962
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.",
29963
29974
  "",
29964
29975
  ...anyGenerated(stores) ? [
29965
- "**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.`,
29966
29977
  ""
29967
29978
  ] : [],
29968
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.",
@@ -30110,7 +30121,7 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30110
30121
  "",
30111
30122
  "### Step 4b: two piles, and the artist pile comes first",
30112
30123
  "",
30113
- "**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.**",
30114
30125
  "",
30115
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**.",
30116
30127
  "",
@@ -30126,11 +30137,11 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30126
30137
  "",
30127
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.**",
30128
30139
  "",
30129
- ...anyGenerated(stores) ? [
30130
- "**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.",
30131
30142
  ""
30132
- ] : [],
30133
- "**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.",
30134
30145
  "",
30135
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.",
30136
30147
  "",
@@ -30142,7 +30153,7 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30142
30153
  "<!-- Durable facts about <Artist> that hold across sessions. One line per fact, with",
30143
30154
  " provenance and an absolute date. SUPERSEDE, do not accumulate - when a fact here",
30144
30155
  " becomes wrong, edit that line in place rather than adding a contradicting one.",
30145
- " 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.",
30146
30157
  " When two lines disagree, this order decides which is current: a line the member",
30147
30158
  " confirmed outranks a newer one the agent wrote; then the newer line; then one",
30148
30159
  " verified against a tool read; then one repeated across sessions; mark the loser",
@@ -30155,7 +30166,7 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30155
30166
  ...stores.user ? [
30156
30167
  "#### True of this member: `create_user_memory_fact`. The file is output.",
30157
30168
  "",
30158
- "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.",
30159
30170
  ""
30160
30171
  ] : [
30161
30172
  "#### True of this member: `user-memory.md` at the mirror root. Write it directly.",
@@ -30210,7 +30221,7 @@ function buildCloseCommandMd(workspaceSlug, stores = BLOB_MEMORY_STORES) {
30210
30221
  "Then the rules the proposal itself obeys:",
30211
30222
  "",
30212
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.",
30213
- "- **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.",
30214
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.",
30215
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.",
30216
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.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Plinth capstone sync daemon and CLI",
5
5
  "type": "module",
6
6
  "private": false,