@plinth-music/cli 0.21.0 → 0.21.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 +42 -0
- package/README.md +1 -1
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +375 -177
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@plinth-music/cli`. Grouped by what a user notices, not by PR.
|
|
4
4
|
|
|
5
|
+
## 0.21.1 - 2026-09-18
|
|
6
|
+
|
|
7
|
+
One code PR since `0.21.0` (`v0.21.0..80aa258`, #195; #192, #193 and #194 docs). A
|
|
8
|
+
patch: fixes only, no new command and no new sync surface.
|
|
9
|
+
|
|
10
|
+
⚠️ **Saving the same file twice in quick succession no longer costs you the second
|
|
11
|
+
edit.** Save, then save again before the daemon has finished pushing the first, and the
|
|
12
|
+
second save could be set aside in `~/.plinth/_conflicts/` and your file overwritten with
|
|
13
|
+
the copy already in Plinth — reported as a conflict with the cloud when nothing in the
|
|
14
|
+
cloud had changed. It was the daemon arguing with its own acknowledgement.
|
|
15
|
+
|
|
16
|
+
The cause was in memory, not on disk. The daemon keeps one working copy of its sync
|
|
17
|
+
state; each lane read that copy, waited on the network, then wrote its whole
|
|
18
|
+
pre-network snapshot back, reverting anything that had been committed meanwhile —
|
|
19
|
+
including the acknowledgement of the push that had just succeeded. The next save then
|
|
20
|
+
presented a superseded timestamp to the server, took a 409, and the blanket cloud-wins
|
|
21
|
+
arm stashed the local edit. The state *file* was never wrong, so it healed at the next
|
|
22
|
+
poll about fifteen seconds later, which is why this read as a cloud echo rather than as
|
|
23
|
+
lost state.
|
|
24
|
+
|
|
25
|
+
A genuinely competing cloud edit still conflicts, still stashes your copy and still says
|
|
26
|
+
so. That is asserted byte for byte, because the point is to stop stashing when nothing
|
|
27
|
+
was lost, not to stop stashing.
|
|
28
|
+
|
|
29
|
+
⚠️ **A push that declines because the pull replaced its file now stashes the local copy
|
|
30
|
+
and reports a conflict** instead of proceeding. A declined delete leaves the file for
|
|
31
|
+
the next save or the next daemon start to push, and nothing shortens that wait. A task
|
|
32
|
+
that trips this arm is stashed whole rather than merged field by field — a downgrade for
|
|
33
|
+
tasks only, and the safer of the two outcomes available.
|
|
34
|
+
|
|
35
|
+
**What is not proven.** This shipped on local evidence by decision: the suite, twenty-four
|
|
36
|
+
mutants and five independent critic passes, and **no live run against a real workspace**.
|
|
37
|
+
The daemon's pull loop cannot be driven in-process — it needs a credential from the
|
|
38
|
+
Keychain and there is no seam for one — so the five places the fix has to hold during a
|
|
39
|
+
poll are covered by a scan of the source rather than by exercising them, and the restart
|
|
40
|
+
case is reconstructed in-process rather than by restarting a daemon. Separately, the
|
|
41
|
+
ambient poll can still overwrite without stashing first: that arm stashes only when no
|
|
42
|
+
suppressor is passed and the ambient poll passes one. Unchanged here and still open.
|
|
43
|
+
|
|
44
|
+
Conflict stashes already in `~/.plinth/_conflicts/` are untouched; this changes what the
|
|
45
|
+
daemon writes from now on and prunes nothing.
|
|
46
|
+
|
|
5
47
|
## 0.21.0 - 2026-09-08
|
|
6
48
|
|
|
7
49
|
One code PR since `0.20.1` (`v0.20.1..b199749`, #190; #189 docs). A minor: `plinth
|
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 context [--workspace <slug>] [--json] [--observed <transcript.jsonl>]` — **the one complete cost account**: every source that can reach a session in this workspace, grouped by how it gets there, with Plinth's share separated from yours. `plinth status`'s **Injection** row measures the hook and nothing else, so a root `CLAUDE.md` can pass its bar and still be the largest thing a session loads — the bar counts LINES OF THE GENERATED BODY, and on a real workspace 29,055 of that file's 40,268 bytes are the preserved member block it cannot see. This reports the root and `CLAUDE.local.md` (loaded by Claude Code itself), `@`-imports, the rendered hook and each file's share of it, per-artist scoped files, demand-read stores and fact bodies, command bodies with their discovery metadata counted separately, and configuration — **including the per-artist `.claude/settings.json`, which is the one that applies inside an artist folder, because settings do not walk up**. ⚠️ **Bytes on disk, rendered hook bytes, loaded bytes and billed tokens are four different quantities and are never summed into one.** A file that reaches a session only through the hook contributes its injected share and never its size on disk, so trimming advice cannot double-charge it. There is exactly one combined figure and it prints its assumptions beside it, because by default nothing here observes a session: **actual loading, tokens and cached usage are reported as not measured, never as zero**, and the harness's own tool-schema overhead is named as unknown rather than guessed from a catalogue size. **`--observed <transcript.jsonl>` measures the first two of those, for the one session you name and no other.** It is opt-in and explicit — without it no file outside the mirror is opened, and with it nothing is discovered or crawled and no sibling transcript is read; a path that turns out not to be the file that was checked is refused rather than measured. It adds that session's recorded deliveries and its usage — fresh input, cache read, cache written and output tokens, kept as four figures because they are billed differently — beside the session id, the runtime, the interval covered and a digest of the bytes read. **A usage figure appears only if every counted request supplied it**; some-but-not-all reads `unknown`, requests with no usage record and duplicates that disagree are counted and named rather than summed, and anything partial puts PARTIAL in the headline. **No token is attributed to any file** — the transcript records the standing context as one aggregate in the first request's cache fields — and the unmeasured limits (inheritance, resume, compaction, duplicate hook delivery, tool-schema cost) still print, so supplying an observation never reads as having measured them. Missing, unreadable, refused, unresolved and unlistable are five distinct states, since all five otherwise read as "0 bytes". **Read-only**: it writes no file, starts no daemon and makes no network call — proven against a v1 `state.json`, which the ordinary state read would migrate and rewrite.
|
|
31
31
|
- `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.
|
|
32
|
-
- `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,588 bytes total.** That is 20,000 plus three measured rows for material added since — dated index lines, M5's source rule, F3's artist carve-out — each named in `grounding.ts` with the figure it was measured at, so a raise is attributable rather than absorbed. 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 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,588 bytes total.** That is 20,000 plus three measured rows for material added since — dated index lines, M5's source rule, F3's artist carve-out — each named in `src/lib/context/grounding.ts` with the figure it was measured at, so a raise is attributable rather than absorbed. 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.
|
|
33
33
|
- `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).
|
|
34
34
|
- `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.
|
|
35
35
|
- `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.
|
package/dist/build-stamp.json
CHANGED