@plinth-music/cli 0.9.1 → 0.10.1

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,164 @@
2
2
 
3
3
  Notable changes to `@plinth-music/cli`. Grouped by what a user notices, not by PR.
4
4
 
5
+ ## 0.10.1 - 2026-08-26
6
+
7
+ One PR since `0.10.0` (`v0.10.0..82441a1`, #121). Generated command text and its tests only, so a patch. Every mirror receives it on its next sync; there is no server prerequisite.
8
+
9
+ ### `/plinth:close` banks the session where a teammate can open it
10
+
11
+ - **The session record goes into the artist's own page.** Closing a session writes a dated section under `## Session log` at the bottom of `artists/<slug>/_artist.md` - the synced body the app renders in the Knowledge Base context pane - for each artist the session touched. A session that touched no artist writes one dated note under `documents/`. It used to go to `artists/<slug>/daily-log.md`, a local file nothing synced and no teammate could open; one artist's had reached 35 KB.
12
+ - **Five sections are kept per artist.** Before an older one drops off, anything still true is lifted into that artist's `memory.md`. The close re-reads the page immediately before writing and appends only its own section; another session's section may be dropped by the cap, never reworded. There is no server limit on an artist body, so this prose bound is the only bound.
13
+ - **The shared-memory step only proposes rules you stated** - "always", "never", "from now on", "our policy is", or a confirmation to that effect in the conversation. Session detail is in the record and is not proposed, and zero candidates is the normal outcome. Measured on one live workspace before this change: of 33 proposals, 23 were rejected, the last six in a row.
14
+ - `/plinth:start` briefs from the `## Session log`. `daily-log.md` is named as older local history on every generated surface and as a destination on none; a new artist's stub says so as well.
15
+
16
+ ## 0.10.0 — 2026-08-26
17
+
18
+ Eleven PRs since `0.9.1` (`v0.9.1..9e27349`, #106 and #108–#117; #107 was superseded
19
+ by #109). Three new surfaces — a `voice.md` in every mirror, a `/plinth:onboard`
20
+ command, and a `views/conflicts.md` listing every local edit the daemon set aside — and
21
+ three behaviour changes worth reading first: the CLI refuses a flag whose value the
22
+ parser lost, a workspace admits one daemon at a time, and a symlink inside the mirror
23
+ is refused rather than followed. The server side of `voice.md` (`/api/sync/voice`) is
24
+ already live and a 0.9.1 client talks to the same API. The desktop app pins `0.9.1`
25
+ exact and does not pick any of this up until a desktop release repins it.
26
+
27
+ ### ⚠️ A flag whose value the parser lost is refused, not acted on
28
+
29
+ Three ways a value used to go missing while the command ran anyway: a value beginning
30
+ with `-` (`--proposed "- a line"` arrived as an empty string and the bullet was parsed
31
+ as six boolean flags); a `--no-` token, which the argument parser deletes before the
32
+ command sees it, so the flag swallows the next word — usually the next flag's name; and
33
+ a value simply left off, which arrived as `""`. `plinth session end --summary "--no-x"`
34
+ exited 0 and lost the summary; `plinth memory-gate log --proposed "--no-longer needed"
35
+ --member <uuid>` could file a permanent row against the wrong member.
36
+
37
+ The parser is now pinned (citty `0.2.2` exact), and every string flag on every command — 27 flags across 13 commands —
38
+ refuses a value shaped like a flag and names the form that cannot mis-parse:
39
+ `--flag=value`. Four flags carry text a person wrote and may legitimately begin with
40
+ `-` (`--proposed`, `--final`, `--summary`, `--label`); the rest take slugs, uuids, paths
41
+ and counts and refuse any leading `-`. A flag added later gets the strict rule until
42
+ someone classifies it. One exception, by design: `plinth session end` still ends the
43
+ session at exit 0 and reports the dropped value on stderr, because it is the last step
44
+ of every `/plinth:close` and must not fail on a malformed flag.
45
+
46
+ Also: `plinth memory-gate log --dry-run` validates and prints without writing to the
47
+ append-only log, and a refusal names the value that arrived rather than describing the
48
+ caller's job.
49
+
50
+ Not fixed: `--help` / `-h` in a value slot still prints help and exits 0. That lives
51
+ inside the parser, ahead of any command.
52
+
53
+ ### ⚠️ One workspace, one daemon
54
+
55
+ A second `plinth start` on a workspace used to stomp the first daemon's pidfile and
56
+ both kept running — two writers on one mirror. The daemon now holds an exclusive kernel
57
+ lock on `~/.plinth/daemon-<workspace>.lock` for its whole life: a second
58
+ `plinth start`, or a standalone `plinth sync` while a daemon is up, exits 1 naming the
59
+ holder's pid. The kernel releases the lock when the holder dies, `kill -9` included, so
60
+ there is no stale state and no cleanup step. **Never delete the lock file**: the lock
61
+ lives on the inode, and removing the path is exactly what lets a second daemon in beside
62
+ a live one. The only file a refusal will ever tell you to delete is the legacy pidfile
63
+ left by a pre-0.10.0 daemon.
64
+
65
+ macOS only. Linux ignores the lock flag silently, so rather than appear to work,
66
+ `plinth start` says so on other platforms.
67
+
68
+ From the same change: a malformed `state.json` names the file and the recovery instead
69
+ of throwing a bare `SyntaxError`, and `state.json` / `config.json` writes use
70
+ per-process temporary names so two processes cannot rename each other's half-written
71
+ file into place.
72
+
73
+ ### ⚠️ A symlink inside the mirror is refused, not followed
74
+
75
+ `ln -s ~/Important ~/Plinth/<workspace>/files/docs` used to be enough for the daemon to
76
+ write cloud rows into `~/Important`, delete real files out there on a tombstone, and
77
+ ship a linked private file upstream as a workspace file. Every write, delete and watch
78
+ path now checks each path component below the workspace folder and refuses a link. A
79
+ refused row is skipped, recorded in `plinth status` (reason `symlink`), and the pull
80
+ cursor is held back so the row is delivered the moment the link is removed; the reject
81
+ clears itself on that first clean apply. A linked mirror ROOT
82
+ (`~/Plinth -> /Volumes/Drive/Plinth`) is still allowed — that redirects the whole mirror
83
+ consistently and is a legitimate setup.
84
+
85
+ ### `voice.md`: your voice profile, in the mirror and in every session
86
+
87
+ A new file at the mirror root beside `user-memory.md`. It round-trips to the server's
88
+ voice store — which the web app's 26 Aug data migration moved out of `user-memory.md`,
89
+ so until this release the profile was editable nowhere — and is injected into every
90
+ Claude Code session as a rubric the agent reads whole and cannot write
91
+ (`Edit(//**/voice.md)` is in the generated deny list at every depth, the per-artist
92
+ folders included). If the file's structure is broken the server reports `malformed`,
93
+ and that is shown as a `Voice` row in `plinth status`, in the daemon's output, and as a
94
+ caveat on the injected rubric — never written into the file itself, so the rubric's own
95
+ hash is undisturbed. The first pull writes a scaffold to fill in.
96
+
97
+ ⚠️ An older daemon strips the field this lane adds to `state.json` — the desktop app's
98
+ bundled `0.9.1` included. Until the desktop is repinned, do not alternate a 0.10.0 daemon
99
+ and the desktop's on the same workspace.
100
+
101
+ ### `/plinth:onboard`: a new workspace sets itself up by conversation
102
+
103
+ The third generated command. Connectors one at a time, each proven by a real read; a
104
+ roster proposed from what those reads saw, with a Spotify link per artist; the
105
+ professional team as contacts; then an inbox review that turns running threads into
106
+ projects with their tasks. Every step ends with something you can see and nothing is
107
+ created without a yes. A workspace with no artists finds it without being told the
108
+ name: the session-start grounding gains one line (only while the roster is empty, so a
109
+ workspace with artists pays nothing) and `/plinth:start` offers onboarding instead of
110
+ briefing a roster of nothing.
111
+
112
+ ### `views/conflicts.md`: the local edits the daemon set aside, listed where you work
113
+
114
+ When the cloud wins a conflict the daemon stashes your version in
115
+ `~/.plinth/_conflicts/` and writes the cloud copy over the file. On 26 Aug that happened
116
+ to a 32 KB edit an agent had just read back clean and reported as banked, and nothing
117
+ said so for an hour. The daemon now writes `views/conflicts.md` on every sync — before
118
+ the network call as well as after, so a run that fails still reports — listing each
119
+ stash with a `cp` command that restores it. The zero case is written too, so an absent
120
+ file means an old daemon, not a clean one. `views/` is never pushed. `/plinth:start` and
121
+ `/plinth:close` both read it. The first live sync surfaced a stash from 20 June that no
122
+ surface had shown.
123
+
124
+ ### `plinth status` no longer says "All local writes synced" over files it never pushed
125
+
126
+ With the daemon down nothing is tried, so the failure lane was empty and the line
127
+ printed a positive claim over edits sitting on disk — including a file a live run had
128
+ created hours earlier. `status` now scans the mirror against what was last acknowledged,
129
+ using each lane's own comparison, and lists every local write that has not reached
130
+ Plinth with the remedy that clears it: start the daemon, or — for an entity file created
131
+ while the daemon was down, which no push path reaches — open and save it once with the
132
+ daemon running. What cannot clear (rejected, held for confirmation, over cap) is
133
+ excluded, so the count can reach zero and stay believed. Adds 0.1–0.3 s.
134
+
135
+ Two more from the same pass: `plinth status` no longer crashes with a raw
136
+ `YAMLException` on a tracked file with malformed frontmatter, and an over-cap
137
+ `user-memory.md` / `workspace-rules.md` / `workspace-memory.md` is no longer counted
138
+ forever.
139
+
140
+ ### `/plinth:close` files each fact where it belongs
141
+
142
+ Step 4 sorted by scope only, so a contact, task or document with a typed home of its
143
+ own went to prose, and a fact true of one artist went to the shared gate by default. It
144
+ now sorts by type first — a contact, task, document, artist attribute or file goes to
145
+ that surface and is never offered as a memory line — then by scope, artist first, with
146
+ only the workspace pile gated. Every `plinth` command in the generated text is pinned
147
+ to `--workspace <slug>`, because the CLI resolves the ACTIVE workspace from
148
+ `~/.plinth/config.json` and not from the folder you are in: `memory-gate show` run
149
+ inside one mirror returned another workspace's log. The text also now says truthfully
150
+ that a contact with no artist is refused by the server
151
+ (`Either artist_id or artist_name is required`), where an earlier draft claimed the
152
+ write succeeded silently.
153
+
154
+ ### A live session is no longer forgotten when two terminals disagree on locale
155
+
156
+ `plinth session list` and `plinth status` compare a process's recorded start time to
157
+ decide whether a pid has been recycled, and the probe was locale-formatted: the same
158
+ live pid reads `Wed Aug 26 07:46:04 2026` in one terminal and
159
+ `mer. 26 août 07:46:04 2026` in another, the mismatch read as a recycled pid, and that
160
+ deleted a live session's record. The probe is pinned to the C locale, and a record
161
+ written by an older build is reported as "cannot compare" rather than reaped.
162
+
5
163
  ## 0.9.1 — 2026-08-25
6
164
 
7
165
  Seven PRs since `0.9.0` (`v0.9.0..1efa7d7`, #98–#104). Every one of them is the
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Plinth capstone sync daemon and CLI. Mirrors your Plinth workspace to `~/Plinth/<workspace>/` and keeps it bidirectionally synced. AI-native: pair with Claude Code or any LLM agent for context-aware artist management.
4
4
 
5
- **Status: early access.** Two-way sync runs as a daemon across documents, projects, tasks, artists, meetings, threads and a user-writable `files/` subtree. See `V1_SPEC` §5 M3 in the plinth monorepo for the full milestone plan, and [`CHANGELOG.md`](CHANGELOG.md) for what landed when.
5
+ **Status: early access.** Two-way sync runs as a daemon across documents, projects, tasks, artists, meetings, threads, a user-writable `files/` subtree, the three memory files and `voice.md` (your voice profile, round-tripped to the server and injected read-only into every session). See `V1_SPEC` §5 M3 in the plinth monorepo for the full milestone plan, and [`CHANGELOG.md`](CHANGELOG.md) for what landed when.
6
6
 
7
7
  ## Install
8
8
 
@@ -24,18 +24,20 @@ plinth start # run the daemon: continuous pull + watch + pu
24
24
 
25
25
  - `plinth login --workspace <slug> [--reset-sync]` — issue a personal access token in the browser, paste it back, store it in the OS keychain (macOS Keychain / Linux Secret Service / Windows Credential Manager). Logging in again to the same workspace is a token rotation and **keeps your sync cursors**, so the next sync picks up where the last one left off; it clears them only when the token belongs to a different workspace or user. `--reset-sync` discards them deliberately, making the next sync a full re-pull — use it after rebuilding a mirror from scratch, which is otherwise unreachable now that rotation preserves them.
26
26
  - `plinth sync [--workspace <slug>]` — one-shot pull into `~/Plinth/<workspace>/`. Also generates the workspace-root `CLAUDE.md` (a thin TOC + entity model + skill pointers), an empty `CLAUDE.local.md` stub on first sync, and the two session-boundary commands (below), and silently refreshes `CLAUDE.md` on a schema-version change. Defaults to the active workspace if `--workspace` is omitted.
27
- - `plinth start` — run the long-running sync daemon: pull the latest, then watch the mirror for local edits and push them. It also projects each mapped artist's Google calendar into `views/calendar/` in the mirror — a file per artist plus an `upcoming.md` — on its own 15-minute timer (`PLINTH_CALENDAR_INTERVAL_MS`), with one run at boot. That is deliberately not the 15-second pull cadence: one tick makes a Google call per mapped calendar, which would breach the server route's rate limit inside a minute. A refresh that fails keeps the last known bodies and rewrites only the header, so a stale view says it is stale rather than reading as an empty diary. Logs to `~/.plinth/daemon.log`.
28
- - `plinth status` — local sync state: daemon liveness, last sync and per-type counts, any destructive batches being held, writes awaiting retry or permanently rejected, and any conflict stashes in `~/.plinth/_conflicts/<workspace>/` — local copies that were set aside when the cloud version won. Warns when a source install's `dist/` is behind its checkout.
27
+ - `plinth start` — run the long-running sync daemon: pull the latest, then watch the mirror for local edits and push them. It also projects each mapped artist's Google calendar into `views/calendar/` in the mirror — a file per artist plus an `upcoming.md` — on its own 15-minute timer (`PLINTH_CALENDAR_INTERVAL_MS`), with one run at boot. That is deliberately not the 15-second pull cadence: one tick makes a Google call per mapped calendar, which would breach the server route's rate limit inside a minute. A refresh that fails keeps the last known bodies and rewrites only the header, so a stale view says it is stale rather than reading as an empty diary. Logs to `~/.plinth/daemon.log`. **One daemon per workspace:** it holds an exclusive kernel lock on `~/.plinth/daemon-<workspace>.lock` for its life (macOS only; other platforms are refused and told why), so a second `plinth start`, or a standalone `plinth sync` while it runs, exits 1 naming the holder's pid. **Never delete the lock file** — the lock lives on the inode, and removing the path is what lets a second daemon in beside a live one. It also writes `views/conflicts.md` on every sync: every local edit the cloud won and the daemon set aside in `~/.plinth/_conflicts/`, each with a `cp` command that restores it; the zero case is written too, so an absent file means an old daemon, not a clean one.
28
+ - `plinth status` — local sync state: daemon liveness, last sync and per-type counts, any destructive batches being held, writes awaiting retry or permanently rejected, **every local write that has not reached Plinth** (scanned from the mirror against the last acknowledged state, with the remedy that clears each — start the daemon, or open and save a file that was created while it was down), the state of `voice.md`, and any conflict stashes in `~/.plinth/_conflicts/<workspace>/` — local copies that were set aside when the cloud version won. Warns when a source install's `dist/` is behind its checkout.
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, entity resolution, write confirmation, workspace rules, workspace memory, user memory). Invoked by the generated Claude Code SessionStart hook.
32
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 Gmail send/reply/forward tools and the Calendar write tools while leaving drafts and reads alone. 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
- - `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.
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.
36
- - `plinth session register` / `plinth session list` / `plinth session end` — machine-local visibility for the Claude sessions writing this workspace, plus the session boundary's end verb. `register` runs as a generated SessionStart hook and is fail-quiet by construction; it records the session id, pid, cwd and — inside the Plinth desktop dock — the dock **tab id** (`PLINTH_DOCK_TAB_ID`), which is null in a plain terminal and is what lets the dock ask whether a given tab holds a live agent. `list` derives liveness at read time from pid **and** process start time (a recycled pid reads dead), reaps dead records as a side effect, and reports what it could not check so a short list is never mistaken for a full one; `--json` adds `tab_id` per session and an `ended` count. `end [--summary "<one line>"]` is the last step of a `/close` ritual: it marks the record ended — a field, not a delete, so the reap still owns deletion — and then, **only** when `PLINTH_DOCK_SIGNAL_PORT`, `PLINTH_DOCK_SIGNAL_TOKEN` and `PLINTH_DOCK_TAB_ID` are all present, POSTs `{"summary": …}` to the dock's loopback listener on `/closed` with the same `x-plinth-token` / `x-plinth-tab` header pair the desktop's edit hook already sends. **Marking happens before the signal**, so a dock that hears `/closed` and immediately asks `session list` already sees the session ended. Every failure path — no dock env, listener down, POST refused, no record — exits 0 and prints nothing: the close ritual runs identically in the dock, in another terminal harness, or in a bare shell, and the dock is optional by construction. Machine-local: a session on another machine is not counted.
36
+ - `plinth session register` / `plinth session list` / `plinth session end` — machine-local visibility for the Claude sessions writing this workspace, plus the session boundary's end verb. `register` runs as a generated SessionStart hook and is fail-quiet by construction; it records the session id, pid, cwd and — inside the Plinth desktop dock — the dock **tab id** (`PLINTH_DOCK_TAB_ID`), which is null in a plain terminal and is what lets the dock ask whether a given tab holds a live agent. `list` derives liveness at read time from pid **and** process start time (a recycled pid reads dead), reaps dead records as a side effect, and reports what it could not check so a short list is never mistaken for a full one; `--json` adds `tab_id` per session and an `ended` count. `end [--summary "<one line>"]` is the last step of a `/close` ritual: it marks the record ended — a field, not a delete, so the reap still owns deletion — and then, **only** when `PLINTH_DOCK_SIGNAL_PORT`, `PLINTH_DOCK_SIGNAL_TOKEN` and `PLINTH_DOCK_TAB_ID` are all present, POSTs `{"summary": …}` to the dock's loopback listener on `/closed` with the same `x-plinth-token` / `x-plinth-tab` header pair the desktop's edit hook already sends. **Marking happens before the signal**, so a dock that hears `/closed` and immediately asks `session list` already sees the session ended. Every failure path — no dock env, listener down, POST refused, no record — exits 0 and prints nothing: the close ritual runs identically in the dock, in another terminal harness, or in a bare shell, and the dock is optional by construction. **`end` is the one command the flag guard warns rather than refuses**, for that reason: a `--summary` the parser lost is dropped and reported on stderr, and the session still ends at exit 0. Ending a session is the safety action and must not fail because a flag was malformed. Machine-local: a session on another machine is not counted.
37
37
  - `plinth memory-gate log` / `plinth memory-gate show` — the decision record behind a `/close` ritual's shared-memory confirm gate. `log` appends one versioned JSON line per proposed `workspace-memory.md` line — timestamp, session id, member, the proposed text, `--decision accept|reject|edit`, and the final text — to `~/.plinth/_memory_gate/<workspace>/log.jsonl`; `--nothing-proposed` records a close that had nothing to bank, because *gated and proposed nothing* must stay distinguishable from *never gated*. **Accepts are logged, not only rejections**, for the same reason. The CLI owns the format rather than the agent writing it free-hand, so the log can still be read back in a year — and unlike the session verbs this one **fails loud** (a decision log that silently drops a decision produces the exact state it exists to make visible). A corrupt line is skipped and counted on read, never repaired and never allowed to block the next append. **Nothing under `~/.plinth` syncs; this log is machine-local, and that is intended for V1.**
38
38
 
39
+ ⚠️ **Every string flag refuses a value the argument parser lost, rather than acting on it.** Three ways a value goes missing and the command still runs: a `--no-` token, which citty deletes from argv before parsing so the flag swallows whatever follows; a bare `--` in a value slot, which node's `parseArgs` consumes AS the value instead of honouring as a terminator; and simply omitting the value. All three end with the flag holding something that is not a value — usually the next flag's name — and until 0.10.0 only `plinth memory-gate log` noticed. Now `src/lib/cli/flag-guard.ts` wraps every command, so `plinth session end --summary "--no-x"` and `plinth status --workspace` say so instead of quietly using a default. **`--flag=value` is the form that can never mis-parse and is what every refusal advises.** Four flags carry text a person wrote and may legitimately begin with `-` (`--proposed`, `--final`, `--summary`, `--label`); the rest take slugs, uuids, paths, refs and counts, which never do, and **an unclassified flag gets the strict rule** so a flag added later is guarded before anyone remembers it.
40
+
39
41
  ⚠️ `--no-color` is honoured on `plinth session *` and `plinth memory-gate *`. It is **still inert on every other command** (`confirm`, `status`, `declare`, `review-tier`, `backlinks`): citty folds `--no-color` into `{ color: false }`, so the `args["no-color"] === true` test those call sites use is always false. `src/lib/output.ts` exports `noColorFromArgs` for the sweep; `NO_COLOR` in the environment is unaffected and works throughout.
40
42
  - `plinth version` — print the installed version.
41
43
 
@@ -45,15 +47,20 @@ Planned (not yet implemented):
45
47
  - `plinth update` — detect install method (brew vs npm-global) and upgrade in place.
46
48
  - `plinth logout --workspace <slug>` — revoke the local PAT reference, retain mirror files.
47
49
 
48
- ## Generated into every mirror: `/plinth:start` and `/plinth:close`
50
+ ## Generated into every mirror: `/plinth:start`, `/plinth:close` and `/plinth:onboard`
49
51
 
50
- Plinth writes two Claude Code slash commands into `<mirror>/.claude/commands/plinth/` on
52
+ Plinth writes three Claude Code slash commands into `<mirror>/.claude/commands/plinth/` on
51
53
  every sync and on `plinth refresh-context`, the same way it writes `CLAUDE.md` and
52
54
  `.claude/settings.json`. They are the session boundary: `/plinth:start` opens a session
53
55
  (reads `workspace-memory.md` **and** `user-memory.md`, pulls what is unread, briefs in
54
56
  three lines, asks what you want to work on), and `/plinth:close` ends one (verifies each
55
57
  write landed, routes follow-ups to a durable surface, banks memory, and runs
56
- `plinth session end` as its last step).
58
+ `plinth session end` as its last step). `/plinth:onboard` sets a new workspace up by
59
+ conversation — connectors one at a time, each proven by a real read; a roster proposed
60
+ from what those reads saw; the professional team as contacts; then an inbox review that
61
+ becomes projects and tasks — and nothing is created without a yes. A workspace with no
62
+ artists is offered it by `/plinth:start`, and by one grounding line that ships only while
63
+ the roster is empty.
57
64
 
58
65
  **Why generated and not synced.** The memory layer is bidirectional and nothing was
59
66
  writing it — on 19 Aug 2026 the live `this-fiction` mirror held 9 artist folders, 9
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha": "4c34a869837b50a27a80e0a4adbf349d146298fa",
2
+ "sha": "c2f7a82d90bb492d81a36384093430a58cd7a912",
3
3
  "dirty": false,
4
- "built_at": "2026-08-25T14:49:33.553Z"
4
+ "built_at": "2026-08-26T22:13:39.014Z"
5
5
  }