@plinth-music/cli 0.5.0 → 0.6.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 +76 -0
- package/README.md +46 -3
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +1959 -428
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,82 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@plinth-music/cli`. Grouped by what a user notices, not by PR.
|
|
4
4
|
|
|
5
|
+
## 0.6.0 — 2026-08-20
|
|
6
|
+
|
|
7
|
+
Eleven PRs since `0.5.0` (`v0.5.0..af8c7df`). A session in a mirror now opens and
|
|
8
|
+
closes with a ritual instead of a habit, an agent can no longer send anything from
|
|
9
|
+
one, and the contracts it needs to read are finally text on disk.
|
|
10
|
+
|
|
11
|
+
### Every mirror gets a `/plinth:start` and a `/plinth:close`
|
|
12
|
+
|
|
13
|
+
Two Claude Code slash commands, written into `<mirror>/.claude/commands/plinth/` on
|
|
14
|
+
every sync, the same way `CLAUDE.md` already is. `/plinth:start` opens a session (reads
|
|
15
|
+
both memory indexes, pulls what is unread, briefs in three lines). `/plinth:close` ends
|
|
16
|
+
one: it verifies each write landed, routes follow-ups somewhere durable, and **asks
|
|
17
|
+
before anything reaches shared memory**, because `workspace-memory.md` grounds every
|
|
18
|
+
teammate on every machine. Every decision at that gate is recorded with
|
|
19
|
+
`plinth memory-gate log`, accepts included, so *gated and proposed nothing* stays
|
|
20
|
+
distinguishable from *never gated*.
|
|
21
|
+
|
|
22
|
+
Why generated rather than documented: the memory layer has been bidirectional for
|
|
23
|
+
weeks and nothing was writing it. On 19 August the live workspace held nine artist
|
|
24
|
+
folders, nine `daily-log.md` files and **one** `memory.md`, with the instruction to
|
|
25
|
+
write memory already shipping in the generated `CLAUDE.md`. An instruction that ships
|
|
26
|
+
is not a writer.
|
|
27
|
+
|
|
28
|
+
### ⚠️ An agent in a mirror can draft and read. It cannot send.
|
|
29
|
+
|
|
30
|
+
**Behaviour change, and every user meets it.** The generated `.claude/settings.json`
|
|
31
|
+
now carries a `permissions.deny` list covering the Gmail send, reply and forward tools
|
|
32
|
+
and the Google Calendar write tools, in exact-name and server-agnostic form. Drafting
|
|
33
|
+
(`create_draft`, `update_draft`) and every read stay allowed. On the machine this was
|
|
34
|
+
verified against, a denied tool is not merely blocked when called: it is absent from
|
|
35
|
+
the session's tool surface entirely, and stays absent under a bypass permission mode.
|
|
36
|
+
|
|
37
|
+
**The voice-gate PreToolUse hook is no longer mounted, and is removed from mirrors that
|
|
38
|
+
already carry it.** Voice is carried as an instruction by `workspace-rules.md`, which is
|
|
39
|
+
already injected into every session and already distributes; the hook was enforcement
|
|
40
|
+
stacked on top of it. What did not survive that reasoning is the outbound boundary, so
|
|
41
|
+
it moved from a hook to a permission rule. The `plinth voice-gate` command still ships
|
|
42
|
+
and still works for a hand-wired mount.
|
|
43
|
+
|
|
44
|
+
The consequence is worth stating plainly: **dates go into a calendar through Plinth's
|
|
45
|
+
calendar view, by a person.** The grounding says so, and says why.
|
|
46
|
+
|
|
47
|
+
⚠️ **A daemon already running holds the previous build in memory and will re-add the
|
|
48
|
+
retired hook on every sync until it is restarted.** Its deny list survives those
|
|
49
|
+
rewrites, so nothing can send in the meantime.
|
|
50
|
+
|
|
51
|
+
### Contracts an agent can actually read
|
|
52
|
+
|
|
53
|
+
Extracted text now reaches the mirror under `views/extracted/`, where a cockpit agent
|
|
54
|
+
can read a contract instead of being handed a download URL it cannot open. Two defects
|
|
55
|
+
found on the way there, both latent rather than reported: the client declared a file's
|
|
56
|
+
download URL always present, so the first text-only row anyone wrote would have aborted
|
|
57
|
+
the pull **for the whole workspace, every poll**; and a missing boolean was read as
|
|
58
|
+
`false` on a field that gates a delete, which would have removed every extracted file
|
|
59
|
+
on the mirror silently.
|
|
60
|
+
|
|
61
|
+
### The session boundary knows who else is in the room
|
|
62
|
+
|
|
63
|
+
`plinth session register` / `list` / `end`. Four live sessions once wrote into one
|
|
64
|
+
workspace inside three minutes, two of them mid-close on the same files, and nothing in
|
|
65
|
+
the mirror, the daemon or the grounding named a sibling. Liveness is derived at read
|
|
66
|
+
time from pid **and** process start time, so a recycled pid reads dead, and `list`
|
|
67
|
+
reports what it could not check rather than letting a short list read as a full one.
|
|
68
|
+
`end` marks a record finished and, inside the desktop dock, tells the dock so.
|
|
69
|
+
|
|
70
|
+
### Also
|
|
71
|
+
|
|
72
|
+
- **A thread anchor renders its message count**, having previously looked identical at
|
|
73
|
+
forty messages and at none. An agent read one of those stubs, reported "no signal" on
|
|
74
|
+
a forty-message thread, and edited two workspace skills on the strength of it.
|
|
75
|
+
- **Three corrections to the generated context an agent reads at session start**, all of
|
|
76
|
+
the same class: it told the agent where a file goes rather than leaving it to guess,
|
|
77
|
+
and it stopped asserting two things about the workspace that were not true. One of
|
|
78
|
+
those corrections named a shell path for writing a calendar, which this release then
|
|
79
|
+
reverses outright, per the deny list above.
|
|
80
|
+
|
|
5
81
|
## 0.5.0 — 2026-08-14
|
|
6
82
|
|
|
7
83
|
Seven PRs since `0.4.0` (`v0.4.0..846b996`). Three new commands, and the generated
|
package/README.md
CHANGED
|
@@ -23,16 +23,20 @@ plinth start # run the daemon: continuous pull + watch + pu
|
|
|
23
23
|
## Commands
|
|
24
24
|
|
|
25
25
|
- `plinth login --workspace <slug>` — 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).
|
|
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)
|
|
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
27
|
- `plinth start` — run the long-running sync daemon: pull the latest, then watch the mirror for local edits and push them. Logs to `~/.plinth/daemon.log`.
|
|
28
28
|
- `plinth status` — local sync state: daemon liveness, last sync and per-type counts, and any destructive batches being held. 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
|
-
- `plinth refresh-context [--workspace <slug>]` — force-regenerate the workspace-root `CLAUDE.md` from the current Plinth schema + workspace. The user-customisable block between the `<!-- BEGIN: user-customisable -->` / `<!-- END -->` markers is always preserved verbatim; `CLAUDE.local.md` is never touched.
|
|
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
|
-
- `plinth voice-gate` — gate agent-originated copy against the workspace voice rubric. `--hook` runs it as a
|
|
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
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.
|
|
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
|
+
|
|
39
|
+
⚠️ `--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.
|
|
36
40
|
- `plinth version` — print the installed version.
|
|
37
41
|
|
|
38
42
|
Planned (not yet implemented):
|
|
@@ -41,6 +45,45 @@ Planned (not yet implemented):
|
|
|
41
45
|
- `plinth update` — detect install method (brew vs npm-global) and upgrade in place.
|
|
42
46
|
- `plinth logout --workspace <slug>` — revoke the local PAT reference, retain mirror files.
|
|
43
47
|
|
|
48
|
+
## Generated into every mirror: `/plinth:start` and `/plinth:close`
|
|
49
|
+
|
|
50
|
+
Plinth writes two Claude Code slash commands into `<mirror>/.claude/commands/plinth/` on
|
|
51
|
+
every sync and on `plinth refresh-context`, the same way it writes `CLAUDE.md` and
|
|
52
|
+
`.claude/settings.json`. They are the session boundary: `/plinth:start` opens a session
|
|
53
|
+
(reads `workspace-memory.md` **and** `user-memory.md`, pulls what is unread, briefs in
|
|
54
|
+
three lines, asks what you want to work on), and `/plinth:close` ends one (verifies each
|
|
55
|
+
write landed, routes follow-ups to a durable surface, banks memory, and runs
|
|
56
|
+
`plinth session end` as its last step).
|
|
57
|
+
|
|
58
|
+
**Why generated and not synced.** The memory layer is bidirectional and nothing was
|
|
59
|
+
writing it — on 19 Aug 2026 the live `this-fiction` mirror held 9 artist folders, 9
|
|
60
|
+
`daily-log.md` and one `memory.md`. The instruction to write memory already shipped in
|
|
61
|
+
the generated `CLAUDE.md`, so an instruction that ships is not a writer; what was missing
|
|
62
|
+
is a ritual that fires at a session boundary.
|
|
63
|
+
|
|
64
|
+
**The generator owns only the files it writes.** Your own commands in
|
|
65
|
+
`.claude/commands/` are never read and never touched — including a `start.md` of your
|
|
66
|
+
own, which stays `/start` while the generated one is `/plinth:start`. Inside each
|
|
67
|
+
generated file, the block between the `BEGIN: user-customisable` / `END` markers is
|
|
68
|
+
preserved verbatim across regenerations, and a file whose markers have been mangled is
|
|
69
|
+
backed up to `.bak-<stamp>` rather than clobbered. **Neither generated file is
|
|
70
|
+
gitignored**, and that is deliberate: the preserved block is user-authored content, so
|
|
71
|
+
ignoring the folder would make it the one thing in the mirror you could not commit —
|
|
72
|
+
gone on a `git clean -xdf` or a fresh clone, with the placeholder silently restored on
|
|
73
|
+
the next sync. They are `CLAUDE.md`-shaped (generated, committed, user block inside),
|
|
74
|
+
not `settings.json`-shaped (ignored because it carries machine-specific absolute paths).
|
|
75
|
+
|
|
76
|
+
**`/plinth:start` does not re-read your memory files**, because the SessionStart hook
|
|
77
|
+
has already injected both indexes in full. It uses what is in the window and spends its
|
|
78
|
+
one live call on unread notifications. If the hook did not run, it falls back to reading
|
|
79
|
+
them.
|
|
80
|
+
|
|
81
|
+
**`/plinth:close` asks before anything reaches SHARED memory.** `user-memory.md` is a
|
|
82
|
+
direct write — it is that member's own recall. `workspace-memory.md` reaches every
|
|
83
|
+
member on every machine and is injected into every future session, so candidate lines go
|
|
84
|
+
through `AskUserQuestion` and every decision is recorded with `plinth memory-gate log`,
|
|
85
|
+
including a close that proposed nothing.
|
|
86
|
+
|
|
44
87
|
## Develop
|
|
45
88
|
|
|
46
89
|
Requires [Bun](https://bun.sh/) (latest).
|
package/dist/build-stamp.json
CHANGED