@plinth-music/cli 0.21.0 → 0.22.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 +97 -0
- package/README.md +2 -2
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +2994 -481
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,103 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@plinth-music/cli`. Grouped by what a user notices, not by PR.
|
|
4
4
|
|
|
5
|
+
## 0.22.0 - 2026-09-22
|
|
6
|
+
|
|
7
|
+
Four code PRs since `0.21.1` (`v0.21.1..c1bb82e`, #199, #200, #201, #202; #197 and #198
|
|
8
|
+
docs). A minor: two new sync surfaces (memory markers on mirrored sources, and the
|
|
9
|
+
freshness of the fact copy) and a change to the generated workspace `CLAUDE.md`.
|
|
10
|
+
|
|
11
|
+
⚠️ **An edit you have not pushed yet is no longer overwritten by a pull without a copy
|
|
12
|
+
being kept** (#202). This closes the exposure the 0.21.1 entry left open. If you saved a
|
|
13
|
+
file under `files/` in the second or so before the daemon's poll brought a newer cloud
|
|
14
|
+
copy, the cloud copy replaced yours and nothing kept it. The watcher then saw the
|
|
15
|
+
daemon's own bytes and treated them as the pull's echo, so the log said nothing either.
|
|
16
|
+
The cloud copy still wins, but your bytes are now kept first in
|
|
17
|
+
`~/.plinth/_conflicts/<workspace>/`, listed in `views/conflicts.md` with the command that
|
|
18
|
+
restores them, and logged as `[FILE_CONFLICT … via=pull]`. A file the pull cannot keep a
|
|
19
|
+
copy of is left as it is, logged as `[FILE_STASH_FAILED …]`, and tried again on the next
|
|
20
|
+
poll; the daemon carries on. Every watched change that is deliberately not pushed now
|
|
21
|
+
leaves a `[FILE_PUSH_SKIPPED … reason=…]` line in `~/.plinth/daemon.log`, so a missing
|
|
22
|
+
edit can be traced.
|
|
23
|
+
|
|
24
|
+
**Mirrored documents and extracted file texts say whether their memory context is
|
|
25
|
+
current** (#199). The daemon asks Plinth about sources it already holds and marks each
|
|
26
|
+
one in its frontmatter: current, invalidated, refresh failed, not found, untracked or
|
|
27
|
+
unknown, with any correction shown beside the unchanged original text. Nothing is
|
|
28
|
+
re-downloaded and no source body is rewritten. The marker goes to unknown on the first
|
|
29
|
+
failed or offline poll, with no grace period. A file the daemon could not mark is listed
|
|
30
|
+
under **Memory context** in `plinth status`. The web side that stops these markers being
|
|
31
|
+
stored as a document's own data (Plinth #439) is live.
|
|
32
|
+
|
|
33
|
+
**Memory fact files carry their correction trail, and the mirror says whether its fact
|
|
34
|
+
copy is up to date** (#200). `memory/_freshness.md` and a **Memory facts** row in `plinth
|
|
35
|
+
status` say current, behind or not checked, dated to when that was established. When
|
|
36
|
+
Plinth says nothing has changed, the daemon skips downloading the fact feed. Any other
|
|
37
|
+
answer, or any failure, falls back to downloading every poll, as before.
|
|
38
|
+
|
|
39
|
+
**The generated workspace `CLAUDE.md` tells the agent where to file documents** (#201): a
|
|
40
|
+
project's files go in `files/<Artist>/<Project>/`, and the folder path links them on
|
|
41
|
+
sync. It also carries a short house-style section. A running daemon on this version
|
|
42
|
+
rewrites the file within one sync.
|
|
43
|
+
|
|
44
|
+
**What is not proven.**
|
|
45
|
+
|
|
46
|
+
- #199 and #200 are proven by the suite, mutation rounds and critic review. Neither has
|
|
47
|
+
had an installed build driven against a live workspace.
|
|
48
|
+
- #202 was reproduced before and after against a test workspace from source, with one
|
|
49
|
+
and with two daemons, but not on the packaged app. The original incident's timing (a
|
|
50
|
+
save about four seconds before the pull) was not reproduced as a loss, so its cause is
|
|
51
|
+
not confirmed; the new log lines distinguish the cases next time.
|
|
52
|
+
- Known gaps in #202, all left for a follow-up:
|
|
53
|
+
- two saves within one poll can still file a conflict when none happened, though the
|
|
54
|
+
copy is kept;
|
|
55
|
+
- a save reverted to exactly the last synced bytes within one poll can still be
|
|
56
|
+
overwritten without a copy;
|
|
57
|
+
- a file the pull cannot keep a copy of is downloaded again on every poll until it can.
|
|
58
|
+
The likeliest cause is a path over about 219 characters.
|
|
59
|
+
|
|
60
|
+
## 0.21.1 - 2026-09-18
|
|
61
|
+
|
|
62
|
+
One code PR since `0.21.0` (`v0.21.0..80aa258`, #195; #192, #193 and #194 docs). A
|
|
63
|
+
patch: fixes only, no new command and no new sync surface.
|
|
64
|
+
|
|
65
|
+
⚠️ **Saving the same file twice in quick succession no longer costs you the second
|
|
66
|
+
edit.** Save, then save again before the daemon has finished pushing the first, and the
|
|
67
|
+
second save could be set aside in `~/.plinth/_conflicts/` and your file overwritten with
|
|
68
|
+
the copy already in Plinth — reported as a conflict with the cloud when nothing in the
|
|
69
|
+
cloud had changed. It was the daemon arguing with its own acknowledgement.
|
|
70
|
+
|
|
71
|
+
The cause was in memory, not on disk. The daemon keeps one working copy of its sync
|
|
72
|
+
state; each lane read that copy, waited on the network, then wrote its whole
|
|
73
|
+
pre-network snapshot back, reverting anything that had been committed meanwhile —
|
|
74
|
+
including the acknowledgement of the push that had just succeeded. The next save then
|
|
75
|
+
presented a superseded timestamp to the server, took a 409, and the blanket cloud-wins
|
|
76
|
+
arm stashed the local edit. The state *file* was never wrong, so it healed at the next
|
|
77
|
+
poll about fifteen seconds later, which is why this read as a cloud echo rather than as
|
|
78
|
+
lost state.
|
|
79
|
+
|
|
80
|
+
A genuinely competing cloud edit still conflicts, still stashes your copy and still says
|
|
81
|
+
so. That is asserted byte for byte, because the point is to stop stashing when nothing
|
|
82
|
+
was lost, not to stop stashing.
|
|
83
|
+
|
|
84
|
+
⚠️ **A push that declines because the pull replaced its file now stashes the local copy
|
|
85
|
+
and reports a conflict** instead of proceeding. A declined delete leaves the file for
|
|
86
|
+
the next save or the next daemon start to push, and nothing shortens that wait. A task
|
|
87
|
+
that trips this arm is stashed whole rather than merged field by field — a downgrade for
|
|
88
|
+
tasks only, and the safer of the two outcomes available.
|
|
89
|
+
|
|
90
|
+
**What is not proven.** This shipped on local evidence by decision: the suite, twenty-four
|
|
91
|
+
mutants and five independent critic passes, and **no live run against a real workspace**.
|
|
92
|
+
The daemon's pull loop cannot be driven in-process — it needs a credential from the
|
|
93
|
+
Keychain and there is no seam for one — so the five places the fix has to hold during a
|
|
94
|
+
poll are covered by a scan of the source rather than by exercising them, and the restart
|
|
95
|
+
case is reconstructed in-process rather than by restarting a daemon. Separately, the
|
|
96
|
+
ambient poll can still overwrite without stashing first: that arm stashes only when no
|
|
97
|
+
suppressor is passed and the ambient poll passes one. Unchanged here and still open.
|
|
98
|
+
|
|
99
|
+
Conflict stashes already in `~/.plinth/_conflicts/` are untouched; this changes what the
|
|
100
|
+
daemon writes from now on and prunes nothing.
|
|
101
|
+
|
|
5
102
|
## 0.21.0 - 2026-09-08
|
|
6
103
|
|
|
7
104
|
One code PR since `0.20.1` (`v0.20.1..b199749`, #190; #189 docs). A minor: `plinth
|
package/README.md
CHANGED
|
@@ -25,11 +25,11 @@ plinth start # run the daemon: continuous pull + watch + pu
|
|
|
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. **Every mirrored task, project, document, artist, meeting and thread file carries its row `id:` in frontmatter**, so an agent holding a file can address the MCP write for that row directly instead of going through `search_entities` and risking a duplicate on an empty result; files already on disk when you upgrade get it from a one-time backfill, since a cursor feed never re-sends an unchanged row. The key is server-authoritative and stripped before the wire, so it moves no hash and pushes nothing. Since 0.17.0 a row **renamed in Plinth moves its mirror file** rather than leaving a second copy at the old name; a file already sitting at the new name is never overwritten, and `plinth status` names the clash until it clears. It also **scaffolds `files/` and the six entity folders**, so a new workspace arrives with somewhere to put things instead of an empty directory, and the folder map in the generated `CLAUDE.md` reads the disk rather than asserting what ought to be there.
|
|
27
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.
|
|
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. Six further rows. **Commands** warns when the customisable block in a generated command still names machinery that command retired — the block is preserved verbatim forever and renders after the generated body, so a hand-written step that runs a retired ritual wins, and the generator may never edit it. **Its verdict is derived from the command registry, not asserted**: a token that still resolves to a live subcommand is described as still working and left to you, because the one token it lists is `memory-gate`, which the grounding block hands every session. **Links** names `[[pointers]]` in `workspace-memory.md` and `artists/*/memory.md` that resolve to nothing — a memory fact is a hook plus a pointer to its source, so a dead pointer can mean the hook is the only surviving copy of the fact. It reuses `plinth backlinks`'s resolution ladder, reports prose links only (never one inside a code fence), never reports an ambiguous target, and strips anchors before resolving. **Injection** reports the session-start block's size against its 20,588-byte budget and **where those bytes go** — seven categories that sum to the block by construction, marking which are yours to change — plus how many facts of each index ride whole, how many ride as their opening line only, and which file to shorten. **Too long** names each memory file past the size it is useful at: a rule file (`voice.md`, `workspace-rules.md`) at **6,000 bytes**, a fact store (`artists/<slug>/memory.md`) at **12,000**. The two bars are not one bar: what a long rule file spends is the model's instruction-following budget, which reference facts do not touch. A file under its bar renders nothing at all. **Memory facts** says whether this machine's copy of the fact store is up to date, behind or not checked, dated to when that answer was established, and says so when the daemon is not running and nothing has been checked since. **Memory context** lists mirrored files the daemon could not mark with their current memory state; the edits in them are untouched. Since 0.16.0 it also reports master-context walls (one block past 2,000 characters), played entries still under an "upcoming" label, and a "Current state" heading with no as-of or a stale one.
|
|
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