@plinth-music/cli 0.12.0 → 0.13.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 +60 -0
- package/README.md +10 -6
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +713 -118
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@plinth-music/cli`. Grouped by what a user notices, not by PR.
|
|
4
4
|
|
|
5
|
+
## 0.13.0 - 2026-08-30
|
|
6
|
+
|
|
7
|
+
Four code PRs since `0.12.0` (`v0.12.0..07a74f8`, #144-#147; #143 is 0.12.0's own History row).
|
|
8
|
+
One new sync surface, which is why this is a minor: a file the cli refuses to upload is now
|
|
9
|
+
visible to the whole team. The rest is a data-loss fix on task conflicts, a wider deny family,
|
|
10
|
+
and a `/plinth:close` that writes less. The desktop app pins `0.12.0` exact and picks none of
|
|
11
|
+
this up until a desktop release repins it. Server prerequisite: the rejection route shipped in
|
|
12
|
+
`plinth` on 30 Aug and is live; a 0.12.0 client keeps working against it, it simply does not post.
|
|
13
|
+
|
|
14
|
+
### A file one machine refuses is visible to every machine in the workspace
|
|
15
|
+
|
|
16
|
+
Until now an over-ceiling file (`over_cap`, 4.0 MB) or a symlink the cli refused showed only in
|
|
17
|
+
`plinth status` on the machine that refused it. A site plan for a show the next day sat on one
|
|
18
|
+
laptop while everyone else saw the folder without it and no sign anything was missing.
|
|
19
|
+
|
|
20
|
+
The cli now posts each file refusal to Plinth once, reads the workspace's refusals on every
|
|
21
|
+
sync into `state.json`, and `plinth status` shows the team's rows under your own, each with who
|
|
22
|
+
reported it, when it was first seen, and when the list was last read ("start the daemon for a
|
|
23
|
+
fresher list" - `status` never touches the network). Only the machine-neutral half of the
|
|
24
|
+
sentence travels; the remedy stays with the machine that can act on it. A refusal clears itself
|
|
25
|
+
when the file syncs - the server retires the row on that upload - and deleting a refused file
|
|
26
|
+
withdraws its row rather than orphaning it. Two things were wrong before this and are fixed by
|
|
27
|
+
it: a file shrunk while the daemon was stopped pushed cleanly and kept its `over_cap` record,
|
|
28
|
+
and deleting a refused file destroyed the only record that this machine had reported it.
|
|
29
|
+
Entity refusals (`bad_request`, `invalid_frontmatter`, `cloud_row_deleted`) stay local. Two
|
|
30
|
+
machines belonging to one person are indistinguishable to the server, so a row your laptop
|
|
31
|
+
reported reads as "this machine" on your desktop too. (#147)
|
|
32
|
+
|
|
33
|
+
### A task edited in Plinth and locally in the same window keeps the losing side
|
|
34
|
+
|
|
35
|
+
The conflict stash used to hold a byte-copy of the side that WON, and the cloud's pre-merge
|
|
36
|
+
prose was nowhere on disk; `status` and `views/conflicts.md` described which side won
|
|
37
|
+
backwards. The stash now holds the loser - the cloud's prose on a landed merge, your copy on the
|
|
38
|
+
arms that do not land - and every stash filename carries `.cloud` or `.local`, with existing
|
|
39
|
+
stashes still parsing. The boot summary names the stash ("1 merged field by field, the cloud's
|
|
40
|
+
description stashed to ~/.plinth/_conflicts/"), and if the pre-overwrite read of the cloud row
|
|
41
|
+
fails the merge does not land at all rather than landing and recording the inverse. Structured
|
|
42
|
+
fields still resolve row-wins, as documented. (#146)
|
|
43
|
+
|
|
44
|
+
### Mail deletion joins the deny family
|
|
45
|
+
|
|
46
|
+
`trash`, `delete` and `spam` are denied by verb across every mounted mail MCP, with `mail` as a
|
|
47
|
+
third noun stem beside `message` and `email`; Drive's `trash_file` is denied on the same word.
|
|
48
|
+
61 patterns become 91. Documented holes, measured against real servers: deletion by move
|
|
49
|
+
(`outlook_move_message` - denying `move` would deny filing), folder deletion, and Microsoft's
|
|
50
|
+
`markAsJunk`. Gmail expresses trash and spam as labels, and the label tools stay allowed because
|
|
51
|
+
filing is how review works - a decision, not an oversight. In a mirror an agent can no longer
|
|
52
|
+
discard its own wrong draft; delete-then-restage is a person's move. (#144)
|
|
53
|
+
|
|
54
|
+
### `/plinth:close` writes less, places it better, and trims in the open
|
|
55
|
+
|
|
56
|
+
Anything rule-shaped is routed before it is written - a tool to refuse becomes a proposed deny,
|
|
57
|
+
a procedure a proposed skill, a stable fact goes to `workspace-rules.md`, a preference is
|
|
58
|
+
binned. A never-save list gates the memory step, and every banked fact carries a `source:` it
|
|
59
|
+
can name or `source: unknown`, never an inferred one. The trim is a list of line proposals
|
|
60
|
+
accepted one at a time; removed lines go to `artists/<slug>/memory-archive.md`, which the
|
|
61
|
+
daemon never injects. A contradiction check reads each index for two lines that disagree, and
|
|
62
|
+
the conflict order is stated once, matching the server's scaffolds. The rendered `close.md` is
|
|
63
|
+
25,509 bytes / 55 rule-shaped lines. (#145)
|
|
64
|
+
|
|
5
65
|
## 0.12.0 - 2026-08-29
|
|
6
66
|
|
|
7
67
|
Eleven PRs since `0.11.0` (`v0.11.0..a7067e1`, #131-#141; #130 is 0.11.0's own History row).
|
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
|
|
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).
|
|
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.
|
|
@@ -91,11 +91,15 @@ has already injected both indexes in full. It uses what is in the window and spe
|
|
|
91
91
|
one live call on unread notifications. If the hook did not run, it falls back to reading
|
|
92
92
|
them.
|
|
93
93
|
|
|
94
|
-
**`/plinth:close`
|
|
95
|
-
direct write
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
**`/plinth:close` banks facts and never rules, and it proposes before it trims.** `user-memory.md`
|
|
95
|
+
is a direct write - that member's own recall. `workspace-memory.md` reaches every member on
|
|
96
|
+
every machine and is injected into every future session; since 0.11.0 (#124) the close no longer
|
|
97
|
+
proposes lines for it - the routing rule in the session grounding says which index a fact belongs
|
|
98
|
+
in - and since #145 anything rule-shaped is sorted first (mechanical -> a deny or hook; procedural
|
|
99
|
+
-> a skill; workspace-wide -> `workspace-rules.md`, which the member writes and the close never
|
|
100
|
+
does; a preference -> binned) and proposed in the closing summary. A trim is a list of line
|
|
101
|
+
proposals accepted one at a time, with every removed line kept in `artists/<slug>/memory-archive.md`
|
|
102
|
+
beside the store it came from.
|
|
99
103
|
|
|
100
104
|
## Develop
|
|
101
105
|
|
package/dist/build-stamp.json
CHANGED