@plinth-music/cli 0.9.1 → 0.10.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 +147 -0
- package/README.md +15 -8
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +8799 -8824
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,153 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@plinth-music/cli`. Grouped by what a user notices, not by PR.
|
|
4
4
|
|
|
5
|
+
## 0.10.0 — 2026-08-26
|
|
6
|
+
|
|
7
|
+
Eleven PRs since `0.9.1` (`v0.9.1..9e27349`, #106 and #108–#117; #107 was superseded
|
|
8
|
+
by #109). Three new surfaces — a `voice.md` in every mirror, a `/plinth:onboard`
|
|
9
|
+
command, and a `views/conflicts.md` listing every local edit the daemon set aside — and
|
|
10
|
+
three behaviour changes worth reading first: the CLI refuses a flag whose value the
|
|
11
|
+
parser lost, a workspace admits one daemon at a time, and a symlink inside the mirror
|
|
12
|
+
is refused rather than followed. The server side of `voice.md` (`/api/sync/voice`) is
|
|
13
|
+
already live and a 0.9.1 client talks to the same API. The desktop app pins `0.9.1`
|
|
14
|
+
exact and does not pick any of this up until a desktop release repins it.
|
|
15
|
+
|
|
16
|
+
### ⚠️ A flag whose value the parser lost is refused, not acted on
|
|
17
|
+
|
|
18
|
+
Three ways a value used to go missing while the command ran anyway: a value beginning
|
|
19
|
+
with `-` (`--proposed "- a line"` arrived as an empty string and the bullet was parsed
|
|
20
|
+
as six boolean flags); a `--no-` token, which the argument parser deletes before the
|
|
21
|
+
command sees it, so the flag swallows the next word — usually the next flag's name; and
|
|
22
|
+
a value simply left off, which arrived as `""`. `plinth session end --summary "--no-x"`
|
|
23
|
+
exited 0 and lost the summary; `plinth memory-gate log --proposed "--no-longer needed"
|
|
24
|
+
--member <uuid>` could file a permanent row against the wrong member.
|
|
25
|
+
|
|
26
|
+
The parser is now pinned (citty `0.2.2` exact), and every string flag on every command — 27 flags across 13 commands —
|
|
27
|
+
refuses a value shaped like a flag and names the form that cannot mis-parse:
|
|
28
|
+
`--flag=value`. Four flags carry text a person wrote and may legitimately begin with
|
|
29
|
+
`-` (`--proposed`, `--final`, `--summary`, `--label`); the rest take slugs, uuids, paths
|
|
30
|
+
and counts and refuse any leading `-`. A flag added later gets the strict rule until
|
|
31
|
+
someone classifies it. One exception, by design: `plinth session end` still ends the
|
|
32
|
+
session at exit 0 and reports the dropped value on stderr, because it is the last step
|
|
33
|
+
of every `/plinth:close` and must not fail on a malformed flag.
|
|
34
|
+
|
|
35
|
+
Also: `plinth memory-gate log --dry-run` validates and prints without writing to the
|
|
36
|
+
append-only log, and a refusal names the value that arrived rather than describing the
|
|
37
|
+
caller's job.
|
|
38
|
+
|
|
39
|
+
Not fixed: `--help` / `-h` in a value slot still prints help and exits 0. That lives
|
|
40
|
+
inside the parser, ahead of any command.
|
|
41
|
+
|
|
42
|
+
### ⚠️ One workspace, one daemon
|
|
43
|
+
|
|
44
|
+
A second `plinth start` on a workspace used to stomp the first daemon's pidfile and
|
|
45
|
+
both kept running — two writers on one mirror. The daemon now holds an exclusive kernel
|
|
46
|
+
lock on `~/.plinth/daemon-<workspace>.lock` for its whole life: a second
|
|
47
|
+
`plinth start`, or a standalone `plinth sync` while a daemon is up, exits 1 naming the
|
|
48
|
+
holder's pid. The kernel releases the lock when the holder dies, `kill -9` included, so
|
|
49
|
+
there is no stale state and no cleanup step. **Never delete the lock file**: the lock
|
|
50
|
+
lives on the inode, and removing the path is exactly what lets a second daemon in beside
|
|
51
|
+
a live one. The only file a refusal will ever tell you to delete is the legacy pidfile
|
|
52
|
+
left by a pre-0.10.0 daemon.
|
|
53
|
+
|
|
54
|
+
macOS only. Linux ignores the lock flag silently, so rather than appear to work,
|
|
55
|
+
`plinth start` says so on other platforms.
|
|
56
|
+
|
|
57
|
+
From the same change: a malformed `state.json` names the file and the recovery instead
|
|
58
|
+
of throwing a bare `SyntaxError`, and `state.json` / `config.json` writes use
|
|
59
|
+
per-process temporary names so two processes cannot rename each other's half-written
|
|
60
|
+
file into place.
|
|
61
|
+
|
|
62
|
+
### ⚠️ A symlink inside the mirror is refused, not followed
|
|
63
|
+
|
|
64
|
+
`ln -s ~/Important ~/Plinth/<workspace>/files/docs` used to be enough for the daemon to
|
|
65
|
+
write cloud rows into `~/Important`, delete real files out there on a tombstone, and
|
|
66
|
+
ship a linked private file upstream as a workspace file. Every write, delete and watch
|
|
67
|
+
path now checks each path component below the workspace folder and refuses a link. A
|
|
68
|
+
refused row is skipped, recorded in `plinth status` (reason `symlink`), and the pull
|
|
69
|
+
cursor is held back so the row is delivered the moment the link is removed; the reject
|
|
70
|
+
clears itself on that first clean apply. A linked mirror ROOT
|
|
71
|
+
(`~/Plinth -> /Volumes/Drive/Plinth`) is still allowed — that redirects the whole mirror
|
|
72
|
+
consistently and is a legitimate setup.
|
|
73
|
+
|
|
74
|
+
### `voice.md`: your voice profile, in the mirror and in every session
|
|
75
|
+
|
|
76
|
+
A new file at the mirror root beside `user-memory.md`. It round-trips to the server's
|
|
77
|
+
voice store — which the web app's 26 Aug data migration moved out of `user-memory.md`,
|
|
78
|
+
so until this release the profile was editable nowhere — and is injected into every
|
|
79
|
+
Claude Code session as a rubric the agent reads whole and cannot write
|
|
80
|
+
(`Edit(//**/voice.md)` is in the generated deny list at every depth, the per-artist
|
|
81
|
+
folders included). If the file's structure is broken the server reports `malformed`,
|
|
82
|
+
and that is shown as a `Voice` row in `plinth status`, in the daemon's output, and as a
|
|
83
|
+
caveat on the injected rubric — never written into the file itself, so the rubric's own
|
|
84
|
+
hash is undisturbed. The first pull writes a scaffold to fill in.
|
|
85
|
+
|
|
86
|
+
⚠️ An older daemon strips the field this lane adds to `state.json` — the desktop app's
|
|
87
|
+
bundled `0.9.1` included. Until the desktop is repinned, do not alternate a 0.10.0 daemon
|
|
88
|
+
and the desktop's on the same workspace.
|
|
89
|
+
|
|
90
|
+
### `/plinth:onboard`: a new workspace sets itself up by conversation
|
|
91
|
+
|
|
92
|
+
The third generated command. Connectors one at a time, each proven by a real read; a
|
|
93
|
+
roster proposed from what those reads saw, with a Spotify link per artist; the
|
|
94
|
+
professional team as contacts; then an inbox review that turns running threads into
|
|
95
|
+
projects with their tasks. Every step ends with something you can see and nothing is
|
|
96
|
+
created without a yes. A workspace with no artists finds it without being told the
|
|
97
|
+
name: the session-start grounding gains one line (only while the roster is empty, so a
|
|
98
|
+
workspace with artists pays nothing) and `/plinth:start` offers onboarding instead of
|
|
99
|
+
briefing a roster of nothing.
|
|
100
|
+
|
|
101
|
+
### `views/conflicts.md`: the local edits the daemon set aside, listed where you work
|
|
102
|
+
|
|
103
|
+
When the cloud wins a conflict the daemon stashes your version in
|
|
104
|
+
`~/.plinth/_conflicts/` and writes the cloud copy over the file. On 26 Aug that happened
|
|
105
|
+
to a 32 KB edit an agent had just read back clean and reported as banked, and nothing
|
|
106
|
+
said so for an hour. The daemon now writes `views/conflicts.md` on every sync — before
|
|
107
|
+
the network call as well as after, so a run that fails still reports — listing each
|
|
108
|
+
stash with a `cp` command that restores it. The zero case is written too, so an absent
|
|
109
|
+
file means an old daemon, not a clean one. `views/` is never pushed. `/plinth:start` and
|
|
110
|
+
`/plinth:close` both read it. The first live sync surfaced a stash from 20 June that no
|
|
111
|
+
surface had shown.
|
|
112
|
+
|
|
113
|
+
### `plinth status` no longer says "All local writes synced" over files it never pushed
|
|
114
|
+
|
|
115
|
+
With the daemon down nothing is tried, so the failure lane was empty and the line
|
|
116
|
+
printed a positive claim over edits sitting on disk — including a file a live run had
|
|
117
|
+
created hours earlier. `status` now scans the mirror against what was last acknowledged,
|
|
118
|
+
using each lane's own comparison, and lists every local write that has not reached
|
|
119
|
+
Plinth with the remedy that clears it: start the daemon, or — for an entity file created
|
|
120
|
+
while the daemon was down, which no push path reaches — open and save it once with the
|
|
121
|
+
daemon running. What cannot clear (rejected, held for confirmation, over cap) is
|
|
122
|
+
excluded, so the count can reach zero and stay believed. Adds 0.1–0.3 s.
|
|
123
|
+
|
|
124
|
+
Two more from the same pass: `plinth status` no longer crashes with a raw
|
|
125
|
+
`YAMLException` on a tracked file with malformed frontmatter, and an over-cap
|
|
126
|
+
`user-memory.md` / `workspace-rules.md` / `workspace-memory.md` is no longer counted
|
|
127
|
+
forever.
|
|
128
|
+
|
|
129
|
+
### `/plinth:close` files each fact where it belongs
|
|
130
|
+
|
|
131
|
+
Step 4 sorted by scope only, so a contact, task or document with a typed home of its
|
|
132
|
+
own went to prose, and a fact true of one artist went to the shared gate by default. It
|
|
133
|
+
now sorts by type first — a contact, task, document, artist attribute or file goes to
|
|
134
|
+
that surface and is never offered as a memory line — then by scope, artist first, with
|
|
135
|
+
only the workspace pile gated. Every `plinth` command in the generated text is pinned
|
|
136
|
+
to `--workspace <slug>`, because the CLI resolves the ACTIVE workspace from
|
|
137
|
+
`~/.plinth/config.json` and not from the folder you are in: `memory-gate show` run
|
|
138
|
+
inside one mirror returned another workspace's log. The text also now says truthfully
|
|
139
|
+
that a contact with no artist is refused by the server
|
|
140
|
+
(`Either artist_id or artist_name is required`), where an earlier draft claimed the
|
|
141
|
+
write succeeded silently.
|
|
142
|
+
|
|
143
|
+
### A live session is no longer forgotten when two terminals disagree on locale
|
|
144
|
+
|
|
145
|
+
`plinth session list` and `plinth status` compare a process's recorded start time to
|
|
146
|
+
decide whether a pid has been recycled, and the probe was locale-formatted: the same
|
|
147
|
+
live pid reads `Wed Aug 26 07:46:04 2026` in one terminal and
|
|
148
|
+
`mer. 26 août 07:46:04 2026` in another, the mismatch read as a recycled pid, and that
|
|
149
|
+
deleted a live session's record. The probe is pinned to the C locale, and a record
|
|
150
|
+
written by an older build is reported as "cannot compare" rather than reaped.
|
|
151
|
+
|
|
5
152
|
## 0.9.1 — 2026-08-25
|
|
6
153
|
|
|
7
154
|
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
|
|
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
|
|
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:
|
|
50
|
+
## Generated into every mirror: `/plinth:start`, `/plinth:close` and `/plinth:onboard`
|
|
49
51
|
|
|
50
|
-
Plinth writes
|
|
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
|
package/dist/build-stamp.json
CHANGED